This is the 25th day of my participation in Gwen Challenge
Introduce the React philosophy
One of the best parts of React is that it leads us to think about how to build an application. In this article, we will use React to build a searchable table of product data to get an insight into the React philosophy.
Start with the design
- Suppose we already have the following things
- An API that returns JSON data
[
{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"}];Copy the code
- Component design draft provided by the designer
Step 1: Divide your designed UI into component levels
- Circle each component on the design sheet and name it appropriately.
- In principle, a component is responsible for only one function. If it needs to be responsible for more functions, consider splitting it into smaller components.
The following application consists of five components, each color-coded
- FilterableProductTable (orange): The overall component of the entire example.
- SearchBar (blue): Component that receives user input.
- ProductTable (green): a component that displays data content and filters results based on user input.
- ProductCategoryRow (sky blue): Displays components for each product category.
- ProductRow (red): Component of each product.
Interpretation of the
Why not set the header of ProductTable to a component? In this example, the header only renders the data set and is not complicated, so it is not set to a separate component.
Now the component hierarchy looks like this:
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
Step 2: Create a static version of React
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>); }}class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}} >
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>); }}class ProductTable extends React.Component {
render() {
const rows = [];
let lastCategory = null;
this.props.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>); }}class SearchBar extends React.Component {
render() {
return (
<form>
<input type="text" placeholder="Search..." />
<p>
<input type="checkbox" />
{' '}
Only show products in stock
</p>
</form>); }}class FilterableProductTable extends React.Component {
render() {
return (
<div>
<SearchBar />
<ProductTable products={this.props.products} />
</div>); }}const PRODUCTS = [
{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'}]; ReactDOM.render(<FilterableProductTable products={PRODUCTS} />.document.getElementById('container'));Copy the code
Official advice: First render a UI without interaction with existing data. Separate UI rendering from adding interaction, because you don’t have to worry about too much interaction detail when writing static pages, and a lot of detail when adding interaction.
- Try to use props to build a static version of the application, not props.
- Applications can be built top-down or bottom-up, and bottom-up means writing from the most basic components, which is appropriate for larger projects.
- React’s idea of one-way data flows makes components modular and easy to develop quickly.
Step 3: Determine the minimum complete representation of UI State
In order for the UI to be interactive, it needs to be able to change the basic data model. React uses state to accomplish this task. In order to properly build your application, you need to find the minimum representation of state required by your application.
- The sample should contain the following data
- A list of all products
- The search term entered by the user
- Check whether the check box is selected
- A list of products filtered by search
Ask yourself three questions to see if the target data should be placed in state
- If the data is passed through the props of the parent component, it should not be placed in state.
- Does the data remain the same over time? If so, the data should still not be placed in state.
- Data that can be extrapolated from existing state or props should not be in state.
- According to the three filtering rules above, we think that as long as the search term entered by the user and the check box is checked, it is put into state.
Step 4: Determine where to place state
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>); }}class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}} >
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>); }}class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) = > {
if (product.name.indexOf(filterText) === -1) {
return;
}
if(inStockOnly && ! product.stocked) {return;
}
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>); }}class SearchBar extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
return (
<form>
<input
type="text"
placeholder="Search..."
value={filterText} />
<p>
<input
type="checkbox"
checked={inStockOnly} />
{' '}
Only show products in stock
</p>
</form>); }}class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: ' '.inStockOnly: false
};
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>); }}const PRODUCTS = [
{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'}]; ReactDOM.render(<FilterableProductTable products={PRODUCTS} />.document.getElementById('container'));Copy the code
-
Step 3 has determined what data should go into state. Next we need to determine which component can change state.
-
Which component should have state is something we need to understand, and we can use the following steps to determine
- Find all components rendered according to this state.
- Find the common ancestor component of these components.
- The common ancestor component should have state.
- If you can’t find the component, you can create it yourself and then put state into it.
-
Based on the strategy we analyzed above, we should set state like this
- ProductTable needs to filter product lists based on state. The SearchBar needs to show the status of search terms and check boxes.
- The common ancestor of the above two components is FilterableProductTable.
- So the search term and check box values should naturally reside in the common ancestor component.
Official description: Good, we’ve decided to store these states in FilterableProductTable. First, add the instance attribute this.state = {filterText: “, inStockOnly: false} to the constructor of FilterableProductTable to set the initial state of the application; Next, pass filterText and inStockOnly as props to the ProductTable and SearchBar; Finally, use these props to filter product information in the ProductTable and set the form values for the SearchBar.
Step 5: Add the reverse data flow
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>); }}class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}} >
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>); }}class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) = > {
if (product.name.indexOf(filterText) === -1) {
return;
}
if(inStockOnly && ! product.stocked) {return;
}
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>); }}class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(e) {
this.props.onFilterTextChange(e.target.value);
}
handleInStockChange(e) {
this.props.onInStockChange(e.target.checked);
}
render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
onChange={this.handleFilterTextChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
onChange={this.handleInStockChange}
/>
{' '}
Only show products in stock
</p>
</form>); }}class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: ' '.inStockOnly: false
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(filterText) {
this.setState({
filterText: filterText
});
}
handleInStockChange(inStockOnly) {
this.setState({
inStockOnly: inStockOnly
})
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextChange={this.handleFilterTextChange}
onInStockChange={this.handleInStockChange}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>); }}const PRODUCTS = [
{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'}]; ReactDOM.render(<FilterableProductTable products={PRODUCTS} />.document.getElementById('container'));Copy the code
- Now that we have passed the data from top to bottom through the previous steps, let’s try to get the lower-level component to update the higher-level state.
The official explanation
Since states can only be changed by the components that own them, FilterableProductTable must pass a callback function (callback) that can trigger state changes to the SearchBar. We can use the onChange event of the input box to monitor changes in user input and notify the callback function that FilterableProductTable passes to the SearchBar. The callback then calls setState() to update the application.
Interpretation of the
If a child component wants to change the parent component’s state, it can have the parent component pass a callback function that can change the parent component’s state through the props function. The child component can change the parent component’s state through this callback function.