React patterns: How to think in React
Note: taken mostly from the official docs.
Suppose you’ve been given a new design mock-up like the below and tasked with building it out as an interactive web page.
Here are the steps you can take to turn that mock-up into an interactive web page using React.
1. Break the UI into a component hierarchy
You can either:
- Draw boxes around each component and name them.
- Create a bullet point list of all the components. Nest the bullet points if needed.
You want to end up with a nested list like this:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
2. Build a static version in React
Build your components using static / hard-coded data. At this stage, you want to focus on building reusable components that can render the static data. You can pass data from parent to child using props, but don’t worry about state or interactivity at this point. You shouldn’t use useState
at this point.
You can take one of two approaches:
- Top-down: Start with building the components higher up in the hierarchy (e.g.,
FilterableProductTable
). This approach is usually easier for simpler UIs. - Bottom-up: Start from components lower down (e.g.,
ProductRow
). Usually more suitable for more complex UIs.
If you take the bottom-up approach, your code at this stage might look like this:
function ProductCategoryRow({ category }) {
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
function ProductRow({ product }) {
const name = product.stocked ? product.name :
<span style={{ color: 'red' }}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
function ProductTable({ products }) {
const rows = [];
let lastCategory = null;
products.forEach((product) => {
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category} />
);
}
rows.push(
<ProductRow
product={product}
key={product.name} />
);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
function SearchBar() {
return (
<form>
<input type="text" placeholder="Search..." />
<label>
<input type="checkbox" />
{' '}
Only show products in stock
</label>
</form>
);
}
function FilterableProductTable({ products }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
);
}
const PRODUCTS = [
{category: "Fruits", price: "$1", stocked: true, name: "Apple"},
{category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
{category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
{category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
{category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
{category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];
export default function App() {
return <FilterableProductTable products={PRODUCTS} />;
}
Note how you first create the components lower down in the hierarchy and work your way up to the top. At this stage, it can be helpful to create all these components in the same file. You can extract them to separate files later on, once you’ve developed a better feel for the component hierarchy.
Usually, you’ll want to have your component at the top of the hierarchy (e.g., FilterableProductTable
) take your data model as a prop, and pass that data down to components further down.
This is called one-way data flow because the data flows from the top-level components down to the components at the bottom of the tree.
3. Find the minimal but complete representation of UI state
To make the UI interactive, you need to let users change your underlying data model. You’ll need state for this.
Think of state as the minimal set of changing data that your app needs to remember. Figure out the minimal representation of state that your UI needs and compute everything else on-demand.
For instance, for the products table example, you can store the list of products in state and then compute other data based on that (e.g., number of products). The less state you manage, the simpler your code will likely be.
List all the pieces of data in the UI
For example:
- The original list of products
- The value in the search box
- The value of
Only show products in stock
checkbox - The filtered list of products
Identify which pieces of data are state
For each item from your data list, ask yourself:
- Does it remain unchanged over time? If so, it’s not state.
- Is it passed in from a parent component via props? If so, it isn’t state.
- Can you compute it based on other state or props? If so, it isn’t state.
What is left is probably state. Here is how you’d apply these questions to the products table UI:
- The original list of products – It’s passed in as props, so it’s not state.
- The value in the search box – It changes over time, it’s not passed in via props, and it can’t be computed based on other state or props. It is state.
- The value of the
Only show products in stock
checkbox – It changes over time, it’s not passed in via props, and it can’t be computed based on other state or props. It is state. - The filtered list of products – It can be computed by filtering the original list of products according to the search box and checkbox inputs. It is not state.
4: Identify where your state should live
Once you’ve identified your UI’s minimal state, you need to identify which component is responsible for changing the state, or owns the state.
For each piece of state in your UI:
- Identify every component that renders something based on that state.
- Find their closest common parent component – a component above them all in the hierarchy.
- Decide where to put the state:
- Often, you can put the state in the common parent component.
- You can also put the state into some component above the common parent component.
- If it’s not clear what common parent component should own the state, create a new component solely for holding the state and place it somewhere above the common parent component.
For the example products table UI, you have the searchbox value and the checkbox as state, so you can take the following steps to identify where that state should live:
- Identify components that use state
ProductsTable
needs to filter the product list based on both the search text and checkbox value).SearchBar
needs to display the value of the search text and checkbox value.
- Finder their closest common parent
- The closest common parent they share is
FilterableProductTable
.
- The closest common parent they share is
- Decide where to put the state
FilterableProductTable
Now that you know what state your UI needs and where it should live, you can use useState
to add them to FilterableProductsTable
and to define the initial states:
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
Then, embracing React’s one-way data flow, you can pass these state variables down to ProductsTable
and SearchBar
as props:
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>
At this point, your UI renders correctly with state and props flowing down the hierarchy. But to change the state (e.g., search text or checkbox value), you’ll need to support data flowing the other way. The form components deep in the hierarchy need to update the state that lives in FilterableProductsTable
.
For example, the SearchBar
input needs to have an onChange
handler that calls setFilterText
, but at the moment it doesn’t:
function SearchBar({ filterText, inStockOnly }) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."/>
5: Add inverse data flow
The filterText
and inStockOnly
states are owned by FilterableProductTable
, so only it can call setFilterText
and setInStockOnly
. To allow SearchBar
or other child components update the state in FilterableProductTable
, you must pass the state updating functions (setFilterText
and setInStockOnly
) down to SearchBar
.
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />
Inside SearchBar
, you can add an onChange
event handler that uses those state-updating functions to update the parent state:
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)} />
Now, you’ve successfully turned the design mock into an interactive UI.
Sources
Thanks for your comment 🙏. Once it's approved, it will appear here.
Leave a comment