Recommended practices for programming React continue to change. Major changes have happened within the past year, and no doubt more will come. For the most part, these changes have moved React from a class-based approach, with heavy reliance on mutable state, to a function-based approach with immutable objects. The goal is to make writing React applications simpler and less error-prone.

To illustrate the differences, this page rewrites the class-based code for the Thinking in React tutorial (as of April, 2019) using more modern JavaScript and React functional style.

Class vs Functional Components

Here are the class-based components defined in step 2 of the tutorial, except for one component that we'll cover in the next section.

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 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>
    );
  }
}

These are all exactly the kinds of components that don't need to be classes. All they do is render some data into HTML. That's not a lucky coincidence. The Thinking in React process starts by first building a static display of data, with no state. Hence classes are not needed.

The above components are fairly easy to redefine as stateless functional components. Instead of a class with a render() method that returns HTML, we define a function that returns HTML directly. The props are passed as a parameter to the function.

Even more simplification can come by taking advantage of modern JavaScript's arrow function syntax and destructuring assignment:

const ProductCategoryRow = ({category}) => (
  <tr>
    <th colSpan="2">
      {category}
    </th>
  </tr>
);

const 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>
  );
};

const SearchBar = () => (
  <form>
    <input type="text" placeholder="Search..." />
    <p>
      <input type="checkbox" />
      {' '}
      Only show products in stock
    </p>
  </form>
);

const FilterableProductTable = ({products}) => (
  <div>
    <SearchBar />
    <ProductTable products={products} />
  </div>
);

The main benefit is reducing the boilerplate. No extends React.Component, no return render, no this.props. The last is an important maintainability point. With old React, the only way to know what attributes a component expected was to scan all the code in a component for this.props. With the object-destructuring parameter syntax, you see what's passed right up front.

Mapping vs For Loops

There's one other component defined in step 2 of the tutorial:

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>
    );
  }
}

This is very sequential imperative code. It uses conditional push() calls in a forEach() to add a new "product category" row whenever the product category changes.

There are better ways to organize the data in the first place, and this code only works if the data has been sorted to put all categories together. But let's stick with the same input, and improve this code using a more functional approach. First, we collect a list of the categories. To avoid duplicates, we can create a Set, then create an array from that. Here's a function to do that:

const getCategories = products => (
  Array.from(new Set(products.map(product => product.category)))
);

We can get a list of the products for each category using filter.

const getCategoryProducts = (category, products) => (
  products.filter(product => product.category === category)
);

Now to define functions to render category and product information as React components.

const categoryRow = (category) => (
  <ProductCategoryRow category={category} key={category} />
);

const productRow = (product) => (
  <ProductRow product={product} key={product.name} />
);

We can define a function that takes a category and list of all products and returns a list of the HTML rows needed for that category. We'll use JavaScript's spread operator to make one flat list of rows.

const categoryRows = (category, products) => ([
  categoryRow(category),
  ...getCategoryProducts(category, products).map(productRow)
]);

Finally, we define ProductTable to collect a list of all the rows for all the categories using flatMap.

const ProductTable = ({products}) => {
  const categories = getCategories(products);
  const rows = categories.flatMap(category => categoryRows(category, products));

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
};

CodePen for the above code is below. Note that, as in the tutorial, filtering is not implemented yet.

See the Pen Thinking In React: Step 2 by Chris Riesbeck (@criesbeck) on CodePen.

Class State vs useState()

The next section of the Thinking in React tutorial introduces two pieces of local state:

  • the variable inStockOnly, if true, means that only items marked in stock should be shown; default is false
  • the variable filterText contains a string; only items that contain that string should be shown; default is the empty string

These variables are changed by the form in the search bar component, and used by the code that displays the list of products. Here's the class-based code that creates that state and passes it to those components:

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>
    );
  }
}

With the release of React 16.8, the useState() hook function is available for creating trackable state in a functional component. To use it, normally you would import it at the top of your code file, e.g.,

import React, { useState } from 'react';

We will write React.useState() here, so that the CodePen can run without imports.

useState(initial-value) returns an array of two elements: initial-value and a function to use to change the internal state to a new value. It's common to use array destructuring to assign these two values to some well-named variables, e.g.,

const [counter, setCounter] = React.useState(0);

So, to get our two pieces of state for filtering, we could do this:

  const [inStockOnly, setInStockOnly] = useState(false);
  const [filterText, setFilterText] = React.useState('');

We could then pass inStockOnly and filterText to ProductTable, and all four variables to SearchBar.

But whenever you find yourself passing many properties to components, consider packaging related properties together into a new object. In this case, both items of state are used together to decide what products to show. A product should be shown only if it includes the filter text and either inStockOnly is false, or the product is in stock. Therefore, let's define a function to create a product filter. The filter will have the state variables, methods for getting and setting those variables, and a method to filter the list of products.

An important React convention is to give a functions that create state a name that starts with use. This is so linting programs can check that your code obeys the rules of hooks .
const useFilter = () => {
  const [inStockOnly, setInStockOnly] = React.useState(false);
  const [text, setText] = React.useState('');
  const test = product => (
    (!inStockOnly || product.stocked) && product.name.indexOf(text) !== -1
  );

  return {
    filter(products) { return products.filter(test); },
    text,
    inStockOnly,
    setInStockOnly,
    setText
  };
}

The filtering part is easy with JavaScript's filter() function.

Now we only need to make some simple additions to our functional FilterableProductTable.

const FilterableProductTable = ({products}) => {
  const filter = useFilter();
  return (
    <div>
      <SearchBar
        filter={filter}
      />
      <ProductTable
        products={filter.filter(products)}
      />
    </div>
  );
}

The original version of SearchBar with filter state looked like this:

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>
    );
  }
}

With our functional version and a filter object, it looks like this

const SearchBar = ({filter}) => (
  <form>
    <input
      type="text"
      placeholder="Search..."
      value={filter.text} />
    <p>
      <input
        type="checkbox"
        checked={filter.inStockOnly} />
      {' '}
      Only show products in stock
    </p>
  </form>
);

When the tutorial added filtering to the original ProductTable, tests were added to the forEach() loop, along with the tests for adding a category row.

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>
    );
  }
}

All that logic is now encapsulated in the product filter hook, and called very simply in FilterableProductTable. We don't need to add anything to our ProductTable component at all!

CodePen for the above code (not yet interactive):

See the Pen Thinking In React: Step 2 by Chris Riesbeck (@criesbeck) on CodePen.

Changing State

The last section of the Thinking in React tutorial added the event handlers to the search bar form to change the state variables. That section changed the search bar component to this:

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>
    );
  }
}

There's a lot of boilerplate here, because the code is using a class component. This code becomes much simpler with functional components. We just get the new values from the change event and store them. No playing with this at all.

const SearchBar = ({filter}) => {
  const handleFilterTextChange = e => filter.setText(e.target.value);
  const handleInStockChange = e => filter.setInStockOnly(e.target.checked);
  
  return (
    <form>
      <input
        type="text"
        placeholder="Search..."
        value={filter.text}
        onChange={handleFilterTextChange}
      />
      <p>
        <input
          type="checkbox"
          checked={filter.inStockOnly}
          onChange={handleInStockChange}
        />
        {' '}
        Only show products in stock
      </p>
    </form>
  );
}

CodePen for the above code (finally interactive!):

See the Pen Thinking In React: Step 2 by Chris Riesbeck (@criesbeck) on CodePen.

© 2024 Chris Riesbeck
Template design by Andreas Viklund