Thinking in React: State Management and Data Flow
React's "Thinking in React" methodology is a five-step design process for building applications: break the UI into components, build a static version, identify minimal state, locate the state owner, and implement inverse data flow. This article completes the process by showing you how to identify which component should own your application's state and how to pass callback functions down to child components so they can update that shared state. Following this pattern produces applications with a unidirectional data flow, making them predictable, maintainable, and easy to debug.
Key Takeaways
- The state owner is the closest common ancestor of all components that need to read or update that state
- Inverse data flow means passing callback functions from parent to child, allowing children to request state updates without directly modifying parent state
- Always keep state in the component where it's needed; lift state up only when multiple components need it
- This top-down architecture makes data flow explicit and prevents the complexity of passing state through many intermediate components
Step 4: Identify Where Your State Should Live
From Part 1, we established our application's minimal state:
searchText: the current search queryinStockOnly: whether to show only in-stock products
Now we must decide which component will own and manage this state. The rule is straightforward: find the closest common ancestor of all components that use that state.
Finding the State Owner
For searchText:
- The
SearchBarcomponent needs to display it and allow users to update it - The
ProductTablecomponent needs it to filter the product list
The closest common ancestor is FilterableProductTable (the root component in this example).
For inStockOnly:
- The
SearchBarcomponent needs to display the checkbox and handle user input - The
ProductTablecomponent needs it to filter results
Again, FilterableProductTable is the state owner.
The rule: If two child components both need access to state, that state belongs in their shared parent. Never put state in a child when its parent or sibling needs it. This creates a single source of truth and prevents state synchronization bugs.
Why This Matters
When state lives in the correct component, the data flow is clear and unidirectional. Every component receives the state and callbacks it needs through props. No component secretly modifies state behind the scenes. This clarity makes applications easier to understand, test, and debug.
Step 5: Implement Inverse Data Flow
With the state owner identified, we implement inverse data flow: passing callback functions from the parent down to children, allowing children to request state updates. This completes the unidirectional cycle.
Setting Up the State Owner
The FilterableProductTable component initializes state and defines callback functions:
import React, { useState } from 'react';
import SearchBar from './SearchBar';
import ProductTable from './ProductTable';
export default function FilterableProductTable({ products }) {
// Initialize state in the common ancestor
const [searchText, setSearchText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<h1>Searchable Product Table</h1>
{/* Pass state and callbacks down to SearchBar */}
<SearchBar
searchText={searchText}
inStockOnly={inStockOnly}
onSearchTextChange={setSearchText}
onInStockOnlyChange={setInStockOnly}
/>
{/* Pass state down to ProductTable for filtering */}
<ProductTable
products={products}
searchText={searchText}
inStockOnly={inStockOnly}
/>
</div>
);
}
Key points:
-
State Initialization:
useStatecreates the state variables in the parent component. -
State Values Down: The current state values (
searchText,inStockOnly) are passed to both child components, ensuring they all display consistent data. -
Callbacks Down (Inverse Data Flow): State setter functions (
setSearchText,setInStockOnly) are passed toSearchBarvia callback props (onSearchTextChange,onInStockOnlyChange). When the user interacts with the form, these callbacks are invoked, updating the parent's state.
Building a Controlled Child Component
The SearchBar component is now a controlled component: it receives its values from the parent via props and notifies the parent of changes via callbacks.
export default function SearchBar({
searchText,
inStockOnly,
onSearchTextChange,
onInStockOnlyChange
}) {
return (
<form>
<div>
<label htmlFor="searchInput">Search:</label>
<input
id="searchInput"
type="text"
placeholder="Search products..."
value={searchText}
onChange={(e) => onSearchTextChange(e.target.value)}
/>
</div>
<div>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={(e) => onInStockOnlyChange(e.target.checked)}
/>
{' '}
Only show products in stock
</label>
</div>
</form>
);
}
How inverse data flow works here:
-
The input's
valuecomes from thesearchTextprop—the parent controls what's displayed. -
When the user types, the
onChangehandler callsonSearchTextChange(e.target.value), invoking the parent'ssetSearchTextfunction with the new value. -
The parent's state updates, causing a re-render of both
SearchBar(which displays the new value) andProductTable(which filters based on it). -
The same pattern applies to the checkbox:
checkedcomes from the prop,onChangecalls the callback, which updates the parent's state.
Using State to Filter Data
The ProductTable component receives the state values and uses them to filter and display products:
export default function ProductTable({
products,
searchText,
inStockOnly
}) {
const rows = [];
let lastCategory = null;
products.forEach((product) => {
// Filter 1: Match search text
if (
product.name.toLowerCase().indexOf(searchText.toLowerCase()) === -1
) {
return; // Skip this product
}
// Filter 2: Filter out-of-stock items if the checkbox is checked
if (inStockOnly && !product.stocked) {
return; // Skip this product
}
// Add category header if this is a new category
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category}
/>
);
lastCategory = product.category;
}
// Add the product row
rows.push(
<ProductRow product={product} key={product.name} />
);
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
The filtering logic:
-
For each product, check if its name includes the search text (case-insensitive). If not, skip it.
-
If the "in stock only" checkbox is checked and the product is out of stock, skip it.
-
Otherwise, add the product to the displayed list.
As the user types or toggles the checkbox, the parent's state updates, ProductTable re-renders with new props, and the list instantly filters. This is the power of React's declarative model combined with unidirectional data flow.
Complete Example: From User Input to Filtered Display
Let's trace a single user interaction through the complete flow:
- User types "Apple" in the search input
- SearchBar's onChange fires and calls
onSearchTextChange('Apple') - Parent's setSearchText('Apple') is invoked
- FilterableProductTable re-renders with new state
- SearchBar receives new prop
searchText="Apple"and displays it - ProductTable receives new prop
searchText="Apple"and re-filters - Only products matching "Apple" are displayed in the table
- User clicks "in stock only" checkbox
- SearchBar's onChange fires and calls
onInStockOnlyChange(true) - Parent's setInStockOnly(true) is invoked
- FilterableProductTable re-renders, both children receive new props
- ProductTable applies both filters: search text AND stock status
- UI instantly updates to show only in-stock products matching "Apple"
This entire flow happens in milliseconds, with no manual DOM manipulation. React's declarative model ensures that the UI always matches the current state.
The Complete Data Flow Pattern
Parent Component (FilterableProductTable)
|
+-- owns state: searchText, inStockOnly
|
+-- passes down to child 1:
| ├─ searchText (value)
| ├─ inStockOnly (value)
| ├─ onSearchTextChange (callback)
| └─ onInStockOnlyChange (callback)
|
├─ Child 1 (SearchBar)
| └─ receives values and callbacks
| └─ calls callbacks when user interacts
| └─ parent state updates
| └─ parent re-renders
|
+-- passes down to child 2:
| ├─ products (data)
| ├─ searchText (value for filtering)
| └─ inStockOnly (value for filtering)
|
└─ Child 2 (ProductTable)
└─ receives values and products
└─ filters and displays based on values
Frequently Asked Questions
What if three components need the same state?
Find their closest common ancestor and put the state there. If components A, B, and C all need a count value, and their closest common ancestor is a component D, put the state in D and pass it down to A, B, and C. If D's parent E also needs count, put the state in E instead. Always lift state to the least common component that needs it.
Can I put state in multiple places?
Yes, but be intentional. Some state belongs in one component, other state in another. For instance, a SearchBar might own its own isFocused state (no other component needs it), while the table owns searchText and inStockOnly (because both the search bar and table need them). Keep state as close as possible to where it's used.
What if a component deep in the tree needs a callback?
Pass it down through intermediate components even if they don't use it directly. This is called "prop drilling." For very deep trees, React's Context API can reduce prop drilling, but for most applications, prop drilling is acceptable and keeps data flow visible and explicit.
Is inverse data flow the only way to update parent state?
In React, it's the idiomatic way. The parent owns the state and passes callbacks to children. Children call callbacks when they want to update. This unidirectional flow makes applications predictable. You could theoretically pass the entire state object and a setter function, but passing specific callbacks (like onSearchTextChange) is clearer about what the child can do.
How do I know if I've identified the state owner correctly?
Ask: "Does this component own the data it needs to render?" If a component receives state through props, the parent is the owner. If it initializes and manages state with useState, it's the owner. For each piece of state, trace which components read it—their closest common ancestor is the owner.
Glossary
State Owner: The component that initializes state with useState and manages it. This is typically the closest common ancestor of all components that need that state.
Inverse Data Flow: Passing callback functions from parent to child components, allowing children to request state updates without directly modifying parent state.
Controlled Component: A component that receives its current value through props and notifies its parent of changes through callback props. The parent manages the state; the child is "controlled" by the parent.
Lift State Up: The process of moving state from a child component to a parent component when multiple siblings need that state. This creates a single source of truth.
Unidirectional Data Flow: Data flows downward through props, and events flow upward through callbacks. This one-way pattern prevents circular dependencies and makes the application's behavior predictable.