REACT (https://www.kirupa.com/react/index.htm) BOOK (https://www.amazon.com/exec/obidos/ASIN/0134546318/kirupacom) Thinking in React by kirupa (https://www.kirupa.com/me/index.htm) | filed under Learning React (https://www.kirupa.com/react/index.htm) By now, you've seen the individual pieces of React. You know about components (https://www.kirupa.com/react/components.htm), how to pass data to them with props (https://www.kirupa.com/react/transferring_properties.htm), how to remember things with state (https://www.kirupa.com/react/dealing_with_state.htm), and how to respond to events (https://www.kirupa.com/react/events_in_react.htm). What nobody has shown you yet is how to take a blank editor and a picture of an app, and turn that into a well-organized pile of components. That's the hard part, and it's less about React's syntax than you might expect. It's a way of thinking. Experienced React developers go through roughly the same sequence of questions whenever they build something new, and once you internalize those questions, building UIs gets a lot less stressful. You stop staring at an empty file and start making a series of small decisions. In this tutorial, we'll walk through that sequence by building a small but realistic app: a searchable list of houseplants. Along the way, we'll figure out which components we need, which values should be state (and which shouldn't), and where that state should live. Onwards! The App We'll Build Imagine a plant shop has asked you for a page where customers can browse what's in stock. They want a search box, a checkbox to show only plants that are safe for pets, and a table of plants grouped by category. Clicking a plant shows its details underneath the table. Here is the finished app: [Image: A search box and an Only show pet-safe plants checkbox that is checked, followed by the text Showing 5 of 10 plants, a table of plants grouped under Succulents, Foliage, and Ferns and Palms, and a line below it saying Parlor Palm costs $24 and is safe for pets.] (https://www.kirupa.com/react/images/plant_list_final_app.png) It isn't a huge app, but it has everything that makes UIs tricky: user input, data that gets filtered, a selection, and a few places where the same information shows up in more than one spot. If you can think your way through this one, you can think your way through plenty of bigger ones. Our data is a plain array of plants. To follow along, create a new Vite project (if you don't have one, Easily Set Up Your React Dev Environment (https://www.kirupa.com/react/setting_up_react_environment.htm) will get you going) and add a file named src/plants.js with the following: export const PLANTS = [ { id: 1, category: "Succulents", name: "Aloe Vera", petSafe: false, price: 12 }, { id: 2, category: "Succulents", name: "Haworthia", petSafe: true, price: 9 }, { id: 3, category: "Succulents", name: "Jade Plant", petSafe: false, price: 14 }, { id: 4, category: "Foliage", name: "Calathea", petSafe: true, price: 22 }, { id: 5, category: "Foliage", name: "Monstera", petSafe: false, price: 28 }, { id: 6, category: "Foliage", name: "Peperomia", petSafe: true, price: 11 }, { id: 7, category: "Foliage", name: "Pothos", petSafe: false, price: 10 }, { id: 8, category: "Foliage", name: "Snake Plant", petSafe: false, price: 18 }, { id: 9, category: "Ferns and Palms", name: "Boston Fern", petSafe: true, price: 16 }, { id: 10, category: "Ferns and Palms", name: "Parlor Palm", petSafe: true, price: 24 }, ]; One quick note: the prices are made up, and the pet-safety values are only sample data for this tutorial. If you ever have a real cat or dog (or a curious toddler) near a real plant, check a trustworthy source like the ASPCA's plant list instead of trusting my array. Our plan has five steps. The first thing we'll do is look at that picture of the finished app and figure out how to cut it up. Step 1: Break the UI into a Component Hierarchy Start by drawing boxes around every piece of the UI that seems to have its own job. You can do this on paper, in a design tool, or just in your head. Each box is a candidate for a component, and the boxes that sit inside other boxes tell you how the components nest. How do you know where to draw the lines? A good rule of thumb is that a component should do one thing. If you find yourself describing a component with the word "and" ("it shows the search box and the table and the details"), it's probably a few components in a trench coat. You can also borrow some help from the data. A well-structured JSON object tends to map nicely onto a well-structured UI, because both are about the same information. Our plants are grouped by category, and so is our table. That's a hint that categories and plants each get their own piece. Here's how I'd cut up our app: [Image: On the left, a mockup of the plant list with colored boxes around the search area, the table, and the details line. On the right, a tree with FilterablePlantList at the top, branching to SearchBar, PlantTable, and PlantDetails, and PlantTable branching to PlantCategoryRow and PlantRow.] (https://www.kirupa.com/react/images/thinking_component_hierarchy_144.png) - FilterablePlantList: the whole app. It's the container for everything else. - SearchBar: the search box and the pet-safe checkbox. - PlantTable: the table. It decides which categories and plants to show. - PlantCategoryRow: a single heading row such as Succulents. - PlantRow: a single plant with its name, pet safety, and price. - PlantDetails: the line below the table that describes the selected plant. You might disagree with some of these. Maybe the search box and the checkbox should be separate components, or maybe PlantDetails is too small to bother with. That's fine, because there isn't one correct answer. If a piece will be reused, gets complicated, or is easier to reason about on its own, make it a component. If you're unsure, start with fewer and split later. Splitting a component that has grown too big is a very ordinary refactor, and our Creating Complex Components (https://www.kirupa.com/react/creating_complex_components.htm) tutorial covers how components nest inside each other if you want a refresher. Step 2: Build a Static Version Now that we know our components, we'll build a version of the app that has no interactivity at all. It will take data in and render it, and nothing else. Resist the urge to add state yet. It sounds backwards, but it's the fastest way to work. A static version is mostly typing and almost no thinking, while the interactive part is almost all thinking and very little typing. Doing them separately keeps each one manageable. In a static version, everything flows from the top down using props. Parents hand data to their children, and children render it. Replace the contents of src/App.jsx with this: import { Fragment } from "react"; import { PLANTS } from "./plants.js"; function SearchBar() { return (
); } function PlantCategoryRow({ category }) { return ( {category} ); } function PlantRow({ plant }) { return ( {plant.name} {plant.petSafe ? "Pet safe" : "Toxic to pets"} ${plant.price} ); } function PlantTable({ plants }) { const categories = [...new Set(plants.map((plant) => plant.category))]; return ( {categories.map((category) => ( {plants .filter((plant) => plant.category === category) .map((plant) => ( ))} ))}
Name Pets Price
); } export default function FilterablePlantList() { return (
); } If you run this, you'll see the whole table with all ten plants: [Image: A search box and an unchecked pet-safe checkbox above a table of all ten plants grouped under Succulents, Foliage, and Ferns and Palms, with each plant showing whether it is pet safe and its price.] (https://www.kirupa.com/react/images/plant_list_static_version.png) A few things in here are worth pointing out. First, PlantTable builds its list of categories from the plants it was given: new Set(...) removes duplicates, and the spread operator turns the result back into an array. That means the table doesn't need to know the category names in advance. Second, the Fragment lets us return a category row and its plant rows together without adding an extra element that would break the table's structure. The key goes on the Fragment (and on each PlantRow), which is what we talked about in Going from Data to UI (https://www.kirupa.com/react/from_data_to_ui_in_react.htm). Finally, the checkbox and search box do nothing yet. You can type in the search box, but that's just the browser doing its thing. React isn't involved. Notice that we haven't used useState anywhere. This is a rule I'd encourage you to follow. Get the structure right first, and then make it interactive. Step 3: Find the Minimal State To make the UI interactive, we need a way to change the data underneath it. That's what state is for. The tricky part is deciding what should be state, because the answer is usually: less than you think. Make a list of every piece of data in the app. For us, that's: - The original list of plants - The text in the search box - The value of the pet-safe checkbox - The filtered list of plants that appears in the table - The number of plants shown - The plant the user clicked on For each one, ask three questions in order: 1. Does it stay the same over time? If so, it isn't state. It's just a constant. 1. Is it passed in from a parent? If so, it's a prop, and it isn't state in this component. 1. Can you calculate it from other state or props you already have? If so, it isn't state either. It's derived. Whatever survives all three questions is real state. Let's go through our list: - The original list of plants never changes, so it's a constant. (If it were loaded from a server, it would become state in whichever component fetched it, as shown in Working with External Data (https://www.kirupa.com/react/working_with_external_data.htm). For us, it's a constant.) - The search text changes as the user types, isn't passed in, and can't be calculated from anything else. This is state. - The checkbox value is the same story. This is state. - The filtered list can be calculated from the original list, the search text, and the checkbox. Not state. - The number of plants shown is just the length of the filtered list. Not state. - The selected plant changes when the user clicks. It can't be calculated, so it needs to be remembered, and that makes it state. We'll look at exactly what to remember in a bit. So we end up with three pieces of state: the search text, the checkbox value, and which plant is selected. Everything else gets calculated on the fly. The reason to keep state this small is that every extra piece of state is a piece you have to keep in sync with the others. If you stored the filtered list as state, you would have to remember to update it every time the search text or the checkbox changed. Forget once, and your table shows something that doesn't match your search box. If you calculate it during rendering, it can never be out of date. Step 4: Decide Where the State Lives We know what the state is. Now, which component should own it? For each piece of state, find every component that uses it to render something. Then find their closest common parent. That's where the state should live. Here is how that works for us: - The search text and checkbox value are shown by SearchBar and used to decide what PlantTable displays. The closest common parent of those two is FilterablePlantList. - The selected plant is highlighted in PlantTable and described in PlantDetails. Their closest common parent is also FilterablePlantList. Everything lands in the same place, so all three pieces of state go in FilterablePlantList. This move is often called lifting state up: when two sibling components need the same data, you move that data up to the parent and pass it back down as props. It feels odd the first time, because the search box is the component the user types into, and it's tempting to let it keep the text itself. But if SearchBar owned the text, PlantTable would have no way of finding out what it was. Siblings can't talk to each other directly. They can only talk through a parent. Let's start with the search text and the checkbox. We'll add the selection when we wire up clicking in the next step. Our top-level component gets the state, calculates the list of plants to show, and passes the values down as props. SearchBar now shows the values it receives. (I've kept just the changed components here. PlantTable and the rest stay as they were.) import { useState } from "react"; import { PLANTS } from "./plants.js"; function SearchBar({ query, petSafeOnly }) { return (
); } export default function FilterablePlantList() { const [query, setQuery] = useState(""); const [petSafeOnly, setPetSafeOnly] = useState(false); const visiblePlants = PLANTS.filter((plant) => { const matchesQuery = plant.name.toLowerCase().includes(query.toLowerCase()); const matchesPets = !petSafeOnly || plant.petSafe; return matchesQuery && matchesPets; }); return (
); } Look at visiblePlants. It isn't state. It's a regular variable calculated from state every time the component runs. When the user types a letter, query changes, React runs FilterablePlantList again, and visiblePlants gets recalculated from scratch. That's the whole trick. If you run this, the inputs will feel frozen. Typing in the search box does nothing, and the checkbox won't check. You'll also see a console warning telling you that you provided a value (or checked) prop to a form field without an onChange handler, so it will render a read-only field. Our state is in charge of those inputs, and nothing is telling the state how to change. That's exactly what our last step fixes. Step 5: Add Inverse Data Flow Data flows down: parents pass props to children. But the user types into SearchBar, which is a child, while the state lives in FilterablePlantList, which is the parent. We need information to travel back up. React's answer is simple and doesn't involve anything fancy. The parent passes a function down as a prop. The child calls that function when something happens, and the function updates the parent's state. You've already seen this pattern in Events in React (https://www.kirupa.com/react/events_in_react.htm). Here, the setter functions from useState are already exactly what we need, so we can hand them straight down. This is how the data and events travel through our app: [Image: FilterablePlantList at the top holds query fern, petSafeOnly true, and selectedId 9. Arrows labeled with props point down to SearchBar, PlantTable, and PlantDetails, and PlantRow below PlantTable. Arrows labeled onQueryChange and onSelect point back up from SearchBar and PlantRow to the parent. A note says data flows down as props and events flow up as callbacks.] (https://www.kirupa.com/react/images/thinking_state_flow_144.png) Here is the finished code. The top-level component now passes callbacks down, and SearchBar, PlantTable, and PlantRow call them: import { Fragment, useState } from "react"; import { PLANTS } from "./plants.js"; function SearchBar({ query, petSafeOnly, onQueryChange, onPetSafeOnlyChange }) { return (
onQueryChange(e.target.value)} />
); } function PlantCategoryRow({ category }) { return ( {category} ); } function PlantRow({ plant, isSelected, onSelect }) { return ( {plant.petSafe ? "Pet safe" : "Toxic to pets"} ${plant.price} ); } function PlantTable({ plants, selectedId, onSelect }) { if (plants.length === 0) { return

No plants match your search.

; } const categories = [...new Set(plants.map((plant) => plant.category))]; return ( {categories.map((category) => ( {plants .filter((plant) => plant.category === category) .map((plant) => ( ))} ))}
Name Pets Price
); } function PlantDetails({ plant }) { if (!plant) { return

Pick a plant to see its details.

; } return (

{plant.name} costs ${plant.price} and is{" "} {plant.petSafe ? "safe" : "not safe"} for pets.

); } export default function FilterablePlantList() { const [query, setQuery] = useState(""); const [petSafeOnly, setPetSafeOnly] = useState(false); const [selectedId, setSelectedId] = useState(null); const visiblePlants = PLANTS.filter((plant) => { const matchesQuery = plant.name.toLowerCase().includes(query.toLowerCase()); const matchesPets = !petSafeOnly || plant.petSafe; return matchesQuery && matchesPets; }); const selectedPlant = PLANTS.find((plant) => plant.id === selectedId); return (

Showing {visiblePlants.length} of {PLANTS.length} plants

); } Run this and try it out. Type "fern" and only Boston Fern remains, and the count changes to "Showing 1 of 10 plants". Clear the box, check the pet-safe checkbox, and the table drops to five plants. Click a plant and its details appear underneath. Search for something that doesn't exist, like "xyz", and you'll see the empty message. Let's trace what happens when you type the letter "f" into the search box, because this is the heart of the whole thing: 1. The browser fires an input event on the text box, and React calls our onChange handler. 1. The handler calls onQueryChange("f"), which is really setQuery from the parent. 1. The state in FilterablePlantList changes, so React runs that component again. 1. This time query is "f", so visiblePlants is recalculated and the new value of query flows down to SearchBar and the new plant list flows down to PlantTable. 1. React updates only the parts of the page that changed. Nobody reaches into the table and removes rows. Nothing says "hide Jade Plant". We described what the UI should look like for a given set of state, and React figured out how to get there. This is the shift in thinking that the title of this tutorial is about. You describe, and React updates. Also notice that selectedId holds an id, not the plant object itself. We only store the minimum needed to find the plant, and then look it up with find. The "Showing X of Y plants" line works the same way: it just reads visiblePlants.length instead of keeping its own count. Storing an id is deliberate, and we'll see why in the mistakes section below. One more detail. Look at what happens if you click a plant like Aloe Vera and then check the pet-safe checkbox. Aloe Vera disappears from the table, but the details line still describes it. That's because selectedPlant looks the plant up in the full PLANTS list, not in visiblePlants. Whether that's what you want is a design call. If you'd rather clear the selection when its row disappears, you could look up the plant in visiblePlants instead. The point is that it's a one-line decision, because the selection and the filter are independent pieces of state. When useState Isn't Enough Our app has three pieces of state and a handful of handlers, and useState is perfect for it. As an app grows, though, you'll hit two kinds of friction. Knowing what they look like helps you reach for the right tool at the right time. Lots of Related State: Try useReducer Suppose we add a "Clear filters" button. Now there's an action that changes two pieces of state at once, and the more actions you add, the more your event handlers turn into little collections of setter calls. The useReducer hook lets you move all of that logic into one function that takes the current state and an action, and returns the new state. Here is the plant list rewritten that way (I've simplified the markup so we can focus on the state): import { useReducer } from "react"; import { PLANTS } from "./plants.js"; const initialState = { query: "", petSafeOnly: false, selectedId: null }; function reducer(state, action) { switch (action.type) { case "query_changed": return { ...state, query: action.query }; case "pet_safe_toggled": return { ...state, petSafeOnly: action.checked }; case "plant_selected": return { ...state, selectedId: action.id }; case "filters_cleared": return { ...state, query: "", petSafeOnly: false }; default: throw new Error(`Unknown action: ${action.type}`); } } export default function FilterablePlantList() { const [state, dispatch] = useReducer(reducer, initialState); const { query, petSafeOnly, selectedId } = state; const visiblePlants = PLANTS.filter((plant) => { const matchesQuery = plant.name.toLowerCase().includes(query.toLowerCase()); return matchesQuery && (!petSafeOnly || plant.petSafe); }); return (
dispatch({ type: "query_changed", query: e.target.value })} />
); } The component now describes what happened (query_changed, filters_cleared), and the reducer owns how the state responds. Everything we've learned in this tutorial still applies. There's still one owner for the state, and visiblePlants is still calculated instead of stored. We only changed how the updates are written. If you want to go deeper, Managing State with useReducer (https://www.kirupa.com/react/managing_state_with_usereducer.htm) has the full story. For an app this size, plain useState is still the simpler choice. A reasonable rule: start with useState, and switch when you notice many handlers changing several values together, or when the rules for how state can change are getting hard to follow. Passing Props Through Too Many Layers: Try Context The other kind of friction shows up in deep trees. Imagine selectedId was needed by something five levels below FilterablePlantList. You would have to pass it as a prop through four components that don't care about it, just to get it where it's going. That's called prop drilling, and it's tedious and noisy. Context lets a component provide a value that any component beneath it can read, without it being passed through every level. It's a good fit for things that lots of components need, such as the current theme, the signed-in user, or the language. Sharing Data with Context (https://www.kirupa.com/react/sharing_data_with_context.htm) covers how it works. One thing to keep in mind is that context doesn't replace the thinking we did in this tutorial. You still need to decide what the state is and who owns it. Context only changes how the value gets down to the components that need it. If you are unsure which to use, here's a quick guide: - useState and props: your default. Use it until something hurts. - useReducer: when several pieces of state change together, or the update rules are getting complicated. - Context: when the same value is needed by many components at different depths, and passing props would be a chore. - A custom hook: when the same state and logic are repeated in more than one component. See Creating Custom Hooks (https://www.kirupa.com/react/custom_hooks.htm). Common Mistakes Most of the bugs people hit while building React UIs come from a handful of mistakes, and nearly all of them are about step 3 (finding the minimal state). Here are the ones I see most often. Copying Props into State It's tempting to do this: function PlantName({ plant }) { const [name] = useState(plant.name); return

{name}

; } The argument you give useState is only used the very first time the component renders. After that, React ignores it. So if the parent later passes a different plant, this component keeps showing the old name. I tried this with a "Next plant" button that moves from Aloe Vera to Haworthia, and the copy kept saying Aloe Vera while a version that just used {plant.name} updated correctly. If a value comes from props, use the prop directly. Storing Values You Can Calculate This one is the most common by far. It looks like this: const [query, setQuery] = useState(""); const [visiblePlants, setVisiblePlants] = useState(PLANTS); useEffect(() => { setVisiblePlants( PLANTS.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())) ); }, [query]); This does work, which is what makes it sneaky. But it's more code, it forces React to render once with the stale list and then again with the new one, and it creates two values that have to stay in sync. Linters that include React's rules will flag a state update inside an effect like this. The fix is what we did in our app: a plain variable, const visiblePlants = PLANTS.filter(...), calculated during rendering. Effects are for syncing with things outside of React, which we cover in Effects with useEffect (https://www.kirupa.com/react/effects_with_useeffect.htm). They're not for shuffling data between your own pieces of state. Storing a Copy of an Object What if our app kept the whole selected plant in state instead of its id? const [plants, setPlants] = useState(PLANTS.slice(0, 3)); const [selected, setSelected] = useState(null); function markSafe() { setPlants(plants.map((p) => (p.id === 1 ? { ...p, petSafe: true } : p))); } // ...

{selected ? String(selected.petSafe) : "none"}

Click "Select first", and the paragraph says false. Click "Mark first as safe", and the plant in plants is updated, but the paragraph still says false. The selected value is a copy of the old object, and nothing updates it. The fix is to store the id and look the plant up each time, like PLANTS.find(...) in our app. Then there is only one copy of the plant, and everyone sees the same one. Putting State in the Wrong Place There are two flavors of this. If state lives too low, siblings that need it can't get to it, and you end up needing to move it up later. If it lives too high, unrelated components rerender whenever it changes, and a component near the top of the tree ends up knowing about details that only matter to one small piece. The common-parent rule from step 4 keeps you honest. Put state at the closest component that is above everything that needs it, and no higher. If rendering speed ever becomes a real problem, Avoiding Unnecessary Renders (https://www.kirupa.com/react/avoiding_unnecessary_renders.htm) is the place to go, but don't worry about that until you've measured something slow. Deeply Nested State Updating a value several levels deep in an object means copying every level on the way down, and it's easy to get wrong. When you can, keep state flat: separate values for separate things, like our query, petSafeOnly, and selectedId. They were three small variables instead of one nested filters object with a selection object inside it, and that kept every update to a single, readable line. Conclusion Whenever you build something new in React, you can run through the same five steps. Break the UI into a component hierarchy. Build a static version with props. Find the minimal state, and calculate everything else. Decide which component owns each piece of state by finding the closest common parent. Then add callbacks so that events can flow back up. None of those steps are tricky on their own. The skill comes from doing them in order and resisting the urge to jump ahead. If you take one idea away from this, make it this one: store as little state as you can, and derive the rest. Most of the confusing bugs in React apps come from two pieces of state that are supposed to agree and don't. If you only have one source of truth, they can't disagree. You now have a good set of tools to start building real things. In the next tutorial, we'll put what we've learned to work by building a to-do app from scratch. Pay attention to the steps as you go. You'll see this exact sequence of thinking show up again. Next tutorial: Building a Simple To-Do List App in React (https://www.kirupa.com/react/simple_todo_app_react.htm)