Course: Section

Get Started with Modern React: Step by Step

Episode: Title

S02・V28: Thinking in React (Part 6)

Date Created: July 17th, 2019
Last Updated: 27 days ago

Objective
  • We will add inverse data flow, by passing callbacks from the state source down the hierarchy to the components which respond to user input using event listeners.
Watch Video
Duration: 6m 24s

Step 5: Add Inverse Data Flow

So far, we’ve built an app that renders correctly as a function of props and state flowing down the hierarchy. Now it’s time to support data flowing the other way.

The form components deep in the hierarchy need to update the state in the top level FilterableProductTable component. React makes this data flow explicit to make it easy to understand how your program works, but it does require a little more typing than traditional two-way data binding.

If you try to type or check the box in the current version of the example, you’ll see that React ignores your input. We have confirmed that neither typing in the search input nor checking the input box have any effect. This is intentional, as we’ve set the value prop of the search input, and the checked prop of the checkbox, to always be equal to the state passed in from FilterableProductTable.

Let’s think about what we want to happen. We want to make sure that whenever the user changes the form, we update the state to reflect the user input. Since components should only update their own state, FilterableProductTable will pass callbacks to SearchBar, which will fire whenever the state should be updated.

Let’s show that now. In the FilterableProductTable, we will create the callback functions.

We will call the first callback handleFilterTextChange, and it will take the filterText state value as its input parameter. It will call the filterText state setter with the input value.

The second callback will be called handleInStockOnlyChange, and it will take the inStockOnly state value as its input parameter. It will call the inStockOnly state setter with the input value.

const FilterableProductTable = props => {
  const [filterText, setFilterText] = useState("");
  const [inStockOnly, setInStockOnly] = useState(false);
  const { products } = props;

  const handleFilterTextChange = filterText => {    setFilterText(filterText);  };
  const handleInStockOnlyChange = inStockOnly => {    setInStockOnly(inStockOnly);  };
  return (
    <div style={{ fontFamily: "sans-serif" }}>
      <SearchBar
        filterText={filterText}
        inStockOnly={inStockOnly}
        onFilterTextChange={handleFilterTextChange}        onInStockOnlyChange={handleInStockOnlyChange}      />
      <ProductTable
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly}
      />
    </div>
  );
};

We will now pass these callbacks to the SearchBar. The first callback will be passed as a prop called onFilterTextChange. The second callback will be passed as a prop called onInStockOnlyChange.

We will now make use of these props within the SearchBar component. Destructure onFilterTextChange and onInStockOnlyChange from props.

The input type="text" has an onChange event listener, which fires on every user keystroke within the input field. We can use the event.target.value to update the handleFilterTextChange callback, which is available here via the onFilterTextChange prop.

The input type="checkbox" has an onChange event listener, which fires whenever the user checks the box. We can use the event.target.checked to update the handleInStockOnlyChange callback, which is available here via the onInStockOnlyChange prop.

const SearchBar = props => {
  const {
    filterText,
    inStockOnly,
    onFilterTextChange,    onInStockOnlyChange  } = props;

  return (
    <form>
      <input
        type="text"
        placeholder="Search..."
        value={filterText}
        onChange={event => onFilterTextChange(event.target.value)}      />
      <p>
        <input
          type="checkbox"
          checked={inStockOnly}
          onChange={event => onInStockOnlyChange(event.target.checked)}        />
        {" "}
        <span style={{ color: "green", fontSize: "smaller" }}>
          Only show products in stock
        </span>
      </p>
    </form>
  );
};

We can now check our app in the browser. First, type “ball” into the search field. We can see that we get a filtered list, showing only the products containing the text “ball”. Next, we will click on the checkbox. We can see that we only get shown products that are in stock.

The one-way binding used in React makes it really explicit how your data is flowing throughout the app.

Here is the complete app:

import React, { useState } from "react";
import ReactDOM from "react-dom";

const ProductCategoryRow = props => {
  const { product } = props;

  return (
    <tr>
      <th colSpan="2">
        {product.category}
      </th>
    </tr>
  );
};

const ProductRow = props => {
  const { product } = props;
  const coloredName = product.stocked ?
    product.name :
    <span style={{ color: "red" }}>{product.name}</span>;

  return (
    <tr>
      <td>{coloredName}</td><td align="right">{product.price}</td>
    </tr>
  );
};

const ProductTable = props => {
  const { filterText, inStockOnly } = props;
  const { products } = props;
  const rows = [];
  let lastCategory = null;

  products.forEach(product => {
    if (product.name.indexOf(filterText) === -1) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }

    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          product={product}
          key={product.category}
        />
      );
    }
    rows.push(<ProductRow product={product} key={product.name} />);
    lastCategory = product.category;
  });

  return (
    <table width="100%">
      <thead>
        <tr style={{ color: "blue" }}>
          <th align="left">Name</th><th align="right">Price</th>
        </tr>
      </thead>
      <tbody>
        {rows}
      </tbody>
    </table>
  );
};

const SearchBar = props => {
  const {
    filterText,
    inStockOnly,
    onFilterTextChange,
    onInStockOnlyChange
  } = props;

  return (
    <form>
      <input
        type="text"
        placeholder="Search..."
        value={filterText}
        onChange={event => onFilterTextChange(event.target.value)}
      />
      <p>
        <input
          type="checkbox"
          checked={inStockOnly}
          onChange={event => onInStockOnlyChange(event.target.checked)}
        />
        {" "}
        <span style={{ color: "green", fontSize: "smaller" }}>
          Only show products in stock
        </span>
      </p>
    </form>
  );
};

const FilterableProductTable = props => {
  const [filterText, setFilterText] = useState("");
  const [inStockOnly, setInStockOnly] = useState(false);
  const { products } = props;

  const handleFilterTextChange = filterText => {
    setFilterText(filterText);
  };

  const handleInStockOnlyChange = inStockOnly => {
    setInStockOnly(inStockOnly);
  };

  return (
    <div style={{ fontFamily: "sans-serif" }}>
      <SearchBar
        filterText={filterText}
        inStockOnly={inStockOnly}
        onFilterTextChange={handleFilterTextChange}
        onInStockOnlyChange={handleInStockOnlyChange}
      />
      <ProductTable
        products={products}
        filterText={filterText}
        inStockOnly={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("root")
);

Summary

We added inverse data flow, by passing callbacks from the state source down the hierarchy to the components which respond to user input using event listeners. Hopefully, this gives you an idea of how to think about building components and applications with React.

While it may be a little more typing than you’re used to, remember that code is read far more than it’s written, and it’s extremely easy to read this modular, explicit code. As you start to build large libraries of components, you’ll appreciate this explicitness and modularity, and with code reuse, your lines of code will start to shrink.

Next Up…

In the next video, we will issue some Challenges, to test your knowledge and skills acquired during this “Step-by-Step” section.