Thinking in React
See Thinking in React doc.
Steps
Follow this when setting up a React app.
Break the UI into a component hierarchy
- Draw boxes around every component in the visual mock. The design of the UI might even have name for the components which you can use in your model.
- A component should do one thing. If it does more, break it up into subcomponents.
- The data and UI should adhere to the same information architecture. One component for each piece of the data model.
- Set up a hierarchy of your components, based on the UI.
Build a static version in React
- Render the data as UI. But no state - all static (does not change over time), no interactivity.
- This requires a lot of code but no thinking, while the later around UI state is a lot of thinking and little code changes.
- Don’t use any methods to add events or state, for now just render.
- What order to build components?
- Build components top-down (useful in simple apps).
- Or bottom-up, with low-level components and tests first (useful in larger projects).
- Data flows one way, which is fast and modular. Change your data model and render again such as with page refresh to see the UI changes.
Identify the minimal (but complete) representation of UI state
- Avoid duplication. Use the DRY principle.
- Think of the minimal set of mutable state. And compute everything else.
- e.g. Don’t keep the length of the array in a variable as a separate count in state, just compute it from the array.
- The following apply to state and if one does not apply then it probably is not state.
- Set intenerally (data passed in externally from a parent via
props
is not state) - Changes over time
- Cannot be computed (state is original data. If you can compute a filtered list from original data, that has some unnecessary duplication if you were to make the filtered data state)
- Set intenerally (data passed in externally from a parent via
Identify where the state should live
Now wee need to identify which component mutates, or owns, the state we decided on in the previous step.
Note that state changes are internal to a component and we use props
to change a component from the outside.
For each piece of state:
- Identify components which render using that state
- Find a common owner component. If you can’t find one that makes sense, you can make a new component just for holdign state and add it above the common owner component.
Add inverse data flow
So far we change can props and state values which are passed down.
Now we need add interactivity for nested components to change the higher component state.
Apply steps to an example
See my Thinking in React pen.
Source data
Here we use some data from the tutorial linked at the top and this will be used to render the UI.
This could come from an API but can just be a plain JS variable.
[
{ category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football" },
{ category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball" },
{ category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball" },
{ category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch" },
{ category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5" },
{ category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7" }
];
Design
Here the given design is:
Break the UI into a component hierarchy
Figure out components and hierarchy
Components:
FilterableProductTable
(orange): containerSearchBar
(blue): receive all user inputProductTable
(green): display and filter the data collection based on user inputProductCategoryRow
(turquoise): display a heading for each categoryProductRow
(red): displays a row for each product
The docs say, you could make the table heading a separate component as well as ProductTableHeader
if your situation was complex enough, like if you had sorting.
And the hierarchy:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
Identify the minimal (but complete) representation of UI state
Here are the pieces of data:
Data | State | Reason |
---|---|---|
Original product list | No | Passed in via props |
User’s search text. | Yes | Change over time and cannot be computed |
Checkbox value. | Yes | Same as above |
Filtered product list. | No | It can be computed by combining the three pieces above |
Identify where the state should live
- The
SearchBar
needs to display the search text state and checked state. - The
ProductTable
needs to filter the product list based on state set inSearchBar
. - The common owner component is
FilterableProductTable
. Conceptually it makes sense for the search text and checked state to live there.
Set up some initial values as state. e.g.
this.state = {
filterText: "",
inStockOnly: false
};
Then change them to other hardcoded values to see how the app appears.
Here focusing on the user inputs with read-only values:
class SearchBar extends React.Component {
render() {
return (
<div className="SearchBar-component">
<p>
<input
type="text"
value={this.props.filterText}
placeholder="Search..."
readOnly={true}
/>
</p>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
readOnly={true}
/>
<label>Only show products in stock</label>
</p>
</div>
);
}
}
Here passing down state and modified state values to test with:
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: "ball",
inStockOnly: true
};
}
render() {
return (
<div className="FilterableProductTable-component">
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
<br />
<ProductTable
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
products={this.props.products}
/>
</div>
);
}
}
Add inverse data flow
Change the inputs to be mutable and to pass values back up with events.
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: "",
inStockOnly: false
};
this.onFilterTextChange = this.onFilterTextChange.bind(this); // <--
this.onInStockOnlyChange = this.onInStockOnlyChange.bind(this); // <--
}
onFilterTextChange(value) { // <--
this.setState({ filterText: value });
}
onInStockOnlyChange(value) { // <--
this.setState({ inStockOnly: value });
}
render() {
return (
<div className="FilterableProductTable-component">
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextChange={this.onFilterTextChange} // <--
onInStockOnlyChange={this.onInStockOnlyChange} // <--
/>
<br />
<ProductTable
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
products={this.props.products}
/>
</div>
);
}
}