React Beyond Basics
React Hooks Deep Dive

React Hooks Deep Dive

In this chapter, I’ll start things off with a simple TODOs app built (and a bit optimized) with React classes. I’ll explain the core concepts used in that version.

I will then convert the app to use React function components with hooks instead of classes following a simple syntax conversion strategy.

After that, I’ll tell you why simple syntax conversion is not enough and how some optimizations in the class version might stop working and how to fix that.

Why A TODOs App?

Because it’s a realistic non-abstract example with a decent set of features that’s not too small or too big. It’s really the new "Hello World" example when it comes to a web UI.

Chances are you’re familiar with the features that are usually presented in it but here’s a recap:

  • Show a list of TODOs

  • Have a checkbox to check a TODO as complete

  • Have a way for the user to delete a TODO and a way to delete all completed TODOs

  • Render a simple form with an input box and a button to add a new TODO to the list

  • Show the count of active TODOs in the UI and as the title of the app

  • Show filtering buttons to make the UI show all TODOs, just the active ones, or just the completed ones

I’ve also added a few constraints to the app:

  • Use an object to manage the list of TODOs (instead of an array for example)

  • Support initial data (to server-render TODOs for example)

  • Optimize component render calls (what React does in memory) and only have React renders what needs to be rendered (when props/state changes)

Please note that the conversion we’re going to do in this guide covers only the common patterns in React like state, props, and mount/update lifecycle methods. It does not cover any "advanced" patterns like context, higher order components, children as a function, etc. There is also no "data fetching" in this conversion.

The class version

Here’s the code and preview for the class version. Give the UI a spin (it’s all interactive) and scan through the code and familiarize yourself with its main parts but don’t worry about the details just yet. I’ll explain the main concepts used here shortly.

Please note that I tried to keep the code simple but realistically close to what you’ll see in actual React apps out there. For example, I used some modern JS features like class fields and arrow functions and avoided manual binding of handlers.

Designing the components tree

When building React Apps, the first step is to think about the components tree. Here are the 5 components I picked for this example:

TodosApp        // Rendered once, top-level
  TodoItem      // Rendered for each TODO
  AddTodoForm   // Rendered once
  FilterButton  // Rendered 3 times for filtering
  TodosLeft     // Rendered once

The components I picked for this example are minimal. You can certainly add more components if you want to. For example, you can group the list of TODOs under a TodosList component or group all action buttons under an ActionButtons component. You might think of adding a Layout component to control the layout of the main elements in the App.

Adding more components for readability is often good but keep in mind that adding components that depend on the App state requires having these components receive that state somehow. This often means more complexity and a negative effect on readability.

I often start with the minimal components I can think of and extract new ones as I see the need for them. I think it’s totally okay to start with even a single component and add more only when you have good reasons to do so. However, let me give you some clues to help you come up with an initial design for your components tree.

Planning for components

There are many reasons to make a part of your app into its own component. Many of these reasons fall generally under the readability and reusability categories but some of them are also about maintainability.

A popular reason to introduce a component is to avoid duplication of sub-trees in the main tree or to make parts in the tree reusable in general. Another popular reason is to split big component into smaller ones. However, these are the clues you can use to decide whether or not to extract components from other components.

What clues can you use to plan for the initial shape of your components tree?

Repetition and behavior

One good clue to look for when designing your minimal components is whether an element is going to be repeated (with a loop for example) and especially if the repeated elements are going to have some shared UI behavior. In the simple TODOs example, the TODO items are repeated and they share the UI behavior to flag a TODO as complete or delete it. The filter buttons are also repeated and they share the behavior to modify what TODOs are displayed in the list. These are good clues to decide on a TodoItem component and a FilterButton component.

Responsibility isolation

Another clue to look for is whether a certain part of the code is logically grouped with a certain responsibility. In this App, the part where the user needs to type in a new TODO item and submit it to the list is a good example of this. This logically-grouped responsibility is a good candidate to have as a component (which I named AddTodoForm).

It’s all about "encapsulation"

Note how both the repetitions and responsibility clues are in a way related to the general concept of encapsulation: having a feature represented by a single object (component in this case) and having that object reveal what is absolutely necessary for other objects of the application to use it. The clue is to look for any parts in your app that can have self-contained pieces of tasks. Another clue is to identify any component that has too much responsibility in the App and is being used as a dumping ground for many things that do not belong together. Optimize your component design for "future change". Future improvements should ideally require changes in a few places and not have a cascading effect of requiring many changes in many places.

Optimizing renders and computations

Sometimes we introduce a component to optimize the number of times a section of the code is run as a form of "caching" (AKA memoizing) it. You can make a component extend React.PureComponent instead of React.Component to make React automatically skip rendering that component if its props and state elements have not changed during a re-render of the parent containing component. If you design your pure component to only depend on the absolute minimum data/state elements that it needs to perform computations, then React’s not re-rendering it would be a form of a one-time cache. That’s the exact thing I did in TodosLeft to cache the computation of the number of active TODOs left.

When we convert the app to use function components instead of class components we can use the React.memo function to have a behavior similar to PureComponent but we’ll need to do a few extra things to exactly match the caching you’re going to see in the class version.

If you use React.PureComponent (or React.memo) it is extremely important that you do not directly modify (mutate) any arrays or objects which are used by these components. You’ll run into surprising bugs if you mutate them directly.

I’ve actually used React.PureComponent for all components except the root one. Using PureComponent on the top-level root component is often not necessary especially if the app state is managed on that level. However, the children of a stateful component like TodosApp (which will re-render frequently) can benefit from being pure. For example, if the state of one TODO item changes from active to complete then the only 2 child components that need to be re-rendered are the TodoItem component and the TodosLeft component. We don’t need to re-render the FilterButton or AddTodoForm components. If the filter state has changed from "show all" to "show only active" then only the FilterButton component needs to be re-rendered (to mark the button as active) and nothing else in the app needs to be re-rendered (other than the top-level root component that’s rendering anyhow). We don’t even need to re-render the TodoItem component in that case (but we do need the root component to unmount some of them). In the next lesson, after explaining the code of the class version, I’ll tell you how you can verify all of this "purity".

Analyzing the class version

Let’s take a look at the class version and understand the concepts used in it. I’ll start with the generic parts of the code that are not about the core app logic and will then explain the main components with inline code comments.

Telling React where to start

The top-level root component for this app is TodosApp. This is what we hand to ReactDOM to make it mount the UI it represents in the browser’s DOM. It’s the starting point of the app.

This root component receives an initialData prop:

// The beginning of the story. Show something in the browser.

ReactDOM.render(
  <TodosApp initialData={initialData} />,
  document.getElementById('mountNode')
);

The ReactDOM.render method takes 2 arguments:

  1. The first argument is the "WHAT". It’s the top-level React element to be rendered to the browser. It’s an instance of TodosApp for this example.

  2. The second argument is the "WHERE". It needs to be a reference to a DOM element that’s already rendered in the browser. React will take over that target DOM element and render the source React element (the first argument) inside it.

Using realistic test data

I made the TodosApp component receive an initialData prop because the requirement specified that the TODO app has to support rendering an initial list of TODOs. To make things a bit more interesting and closer to reality, I made the initialData element an object of todos indexed by their IDs (instead of using arrays).

Here is a mock variable to represent that initial data object:

// Seed test data

const initialData = {
  todos: {
    A: {
      body: 'Learn React Fundamentals',
      done: true,
    },
    B: {
      body: 'Build a TODOs App',
      done: false,
    },
    C: {
      body: 'Build a Game',
      done: false,
    },
  },
};

Each TODO item needs a unique id property, a body text property, and a done Boolean property to indicate its complete status.

Don’t use short or random data to test your app. Try to use realistic data. That will help you design your UIs better.

I like to use string values for IDs instead of numeric values. JavaScript is not the best language when it comes to working with numbers.

Bringing external modules

Chances are your React app will need external functions that are not part of any component but rather imported for the app to use. Together they form what we call the app dependencies. This TODOs example app has one utility function that is external to it: uniqueId.

Since we will be adding new TODOs, we need a way to generate unique id properties for them. This is not something that the user will provide. We can use a simple counter but that’s boring. Here’s a function that uses the current time concatenated with a random value to generate a unique id:

const uniqueId = () =>
  Date.now().toString(36) + Math.random().toString(36);
You can also import and use an npm package for the "unique-id" dependency. For example, shortid.

Whenever we need to add a new TODO in the UI we can use this uniqueId function to generate a unique id property for it. Note that this is usually something that a "todo-save" API endpoint would provide. However, for this example, new TODOs will only be saved locally in the application memory to keep things simple.

The core concepts in the class version

Let’s now take a detailed look at the code for the class version and understand the main concepts in it.

I’ll use inline comments to explain concepts after they’re used in the code. If you want to experiment with the code, the interactive version is included at the end of this lesson:

class TodosApp extends React.Component {
/* The component class definition is the blueprint used every time
   an "element" from this component is to be rendered in the browser.
   React instantiates an object from this class when we do:
   <TodosApp ... />
   and that object gets displayed in the browser.
   React associates this instance with the DOM-rendered element.
   This instance can be accessed using the "this" keyword
   within the class definition here. */

  state = {
    todos: this.props.initialData.todos,
    filterLabel: 'All',
  };
  /* Initialize the special state property on the instantiated object
     This is a "class field" syntax that's equivalent to doing
     "this.state = { .... }" in the class constructor method.

     A class component state is a plain-old JS object. This one has a
     "todos" property that gets its initial value from the props input
     and a "filterLabel" string. You can have as many properties on
     the state object as you need but you should keep what you put
     on the state to the absolute minimum. */

  /* As you'll see in the many methods below, class fields can also
    be used with arrow functions and because of how closures work for
    arrow functions, we can safely use the "this" keyword within them
    and we'll be correctly using the instantiated instance of TodosApp. */

  addNewTodo = newTodoBody =>
    this.setState(prevState => ({
      todos: {
        ...prevState.todos,
        [uniqueId()]: {
          body: newTodoBody,
          done: false,
        },
      },
    }));

  markTodoDone = (todoId, newDoneValue) =>
    this.setState(prevState => ({
      todos: {
        ...prevState.todos,
        [todoId]: {
          ...prevState.todos[todoId],
          done: newDoneValue,
        },
      },
    }));

  deleteTodo = todoId =>
    this.setState(prevState => {
      const { [todoId]: _, ...todos } = prevState.todos;
      return { todos };
    });

  deleteAllCompleteTodos = () =>
    this.setState(prevState => ({
      todos: Object.entries(prevState.todos).reduce((acc, [todoId, todo)] => {
        if (!todo.done) {
          acc[todoId] = todo;
        }
        return acc;
      }, {}),
    }));

  setFilter = newFilterLabel => this.setState({ filterLabel: newFilterLabel });

  /* The 5 methods above are mutations to be done on React's state for
     this component (they have nothing to do with the DOM).
     Also, except for the "setState" method, all the logic here is
     not really React but rather pure JavaScript.

     setState is React’s way to keep track of the state changes since
     React needs to “react” to all of these changes.

     The whole TodosApp component will get re-rendered along with all of
     its children every time any of these methods is called. However, React
     will still only take to the browser the parts that get different
     content based on the new modifications to the state. React uses
     a special "reconciliation algorithms" to do that. */

  shouldShowTodo = todo => {
    const { filterLabel } = this.state;
    return (
      filterLabel === 'All' ||
      (filterLabel === 'Active' && !todo.done) ||
      (filterLabel === 'Completed' && todo.done)
    );
  };

  /* The shouldShowTodo method perform "computations".
     Computing values is a common thing to do before the render method.
     Computation methods can be called directly in the render method below.
     They are NOT "handlers" like the mutation methods above. */

  render() {
    return (
      <>
        <header>TODO List</header>
        <ul>
          {Object.entries(this.state.todos).map(
            ([todoId, todo]) =>
              this.shouldShowTodo(todo) && (
                <TodoItem
                  key={todoId}
                  id={todoId}
                  todo={todo}
                  markTodoDone={this.markTodoDone}
                  deleteTodo={this.deleteTodo}
                />
              )
          )}
          {/* We take the entries of the todos object and map every entry
              to a <TodoItem /> element passing it the props it needs.

              An entry is a key/value pair. The key is the id of a todo
              and the value is the todo object itself (body and done).

              Before rendering a TodoItem element, we invoke a function
              to check if that element should be shown in the UI based
              on the current filtering state. This use of the "&&"
              operator is called "short-circuit evaluation". If the
              shouldShowTodo function call is false, React will render
              nothing. If it's true, React will render the second part
              of the Boolean check, which is the TodoItem element itself.

              The "key" attribute is a React internal thing. It helps React
              identify dynamic children and what to do when they change.
              Your code can't depend on it. An "id" prop was defined
              to pass the same value used as "key". */}
        </ul>
        <AddTodoForm onSubmit={this.addNewTodo} />
        <div className="actions">
          Show:{' '}
          <FilterButton
            label="All"
            onClick={this.setFilter}
            active={this.state.filterLabel === 'All'}
          />
          <FilterButton
            label="Active"
            onClick={this.setFilter}
            active={this.state.filterLabel === 'Active'}
          />
          <FilterButton
            label="Completed"
            onClick={this.setFilter}
            active={this.state.filterLabel === 'Completed'}
          />
        </div>
        <div className="actions">
          <button onClick={this.deleteAllCompleteTodos}>
            Delete All Completed
          </button>
        </div>
        <footer>
          <TodosLeft todos={this.state.todos} />
        </footer>
      </>
    );
  }
  /* The render method is what React uses to determine the shape
     this element should take in the real browser's UI.
     It returns JSX which gets compiled into React API calls.

     The <></> syntax is sugar for "React.Fragment"
     which is a way to group React elements without
     introducing an unnecessary "wrapping" parent element. */
}

class TodoItem extends React.PureComponent {
/* We're not managing any state or side effects for a TodoItem.
   Each TodoItem gets purely rendered based on its props input
   (which contains both data and behavior).
   This purity makes the component a candidate to extend
   React.PureComponent instead of React.Component. This signals
   to React that if the parent component re-renders and
   nothing has changed in the props of a rendered TodoItem
   then don't bother re-rendering that TodoItem (in memory).
   It's pure! React can re-use the previously rendered version. */

  handleCheckboxChange = event => {
    const newDone = event.target.checked;
    /* React gives access to the native DOM "event" associated
       with any handler. You can use event.target to access the
       DOM element on which the event occurred. This target is
       used here to read the checkbox's "checked" state */

    this.props.markTodoDone(this.props.id, newDone);
  };

  handleXClick = () => this.props.deleteTodo(this.props.id);

  /* The 2 local handlers above just invoke the behaviors
     that are pre-defined by the parent. The TodoItem
     component has no control over these behaviors except
     when to invoke them and what arguments they receive.*/

  render() {
    const { todo } = this.props;

    const todoStyle = { textDecoration: todo.done ? 'line-through' : 'none' };
    /* This is a computed style object. This is a common way
       to represent "conditional" styles. It's used below
       with the todo.body span */

    return (
      <li>
        <input
          type="checkbox"
          checked={todo.done}
          onChange={this.handleCheckboxChange}
        />
        <span style={todoStyle}>{todo.body}</span>
        <span role="link" onClick={this.handleXClick}>
          X
        </span>
      </li>
    );
  }
}

class AddTodoForm extends React.PureComponent {
  /* The AddTodoForm component depends on a single prop,
     which is the onSubmit behavior and it invokes it when
     the Add TODO button is clicked. It passes what the user
     types in an input box using the DOM directly to read that
     typed value. */

  handleSubmit = event => {
    event.preventDefault();
    this.props.onSubmit(event.target.todoBody.value);
    event.target.todoBody.value = '';
  };
  /* This is another example where the handled event's target
     attribute is used to read a user interaction. React is not
     aware of the UI state changes when the user types or check
     a checkbox. It just asks the DOM API for these value when
     it needs them. This is the "uncontrolled input" pattern. */

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="text" name="todoBody" placeholder="What TODO?" />
        <button type="submit">Add TODO</button>
      </form>
    );
  }
}

class FilterButton extends React.PureComponent {
  handleClick = () => this.props.onClick(this.props.label);
  render() {
    const buttonStyle = {
      fontWeight: this.props.active ? 'bold' : 'normal',
    };
    /* Another example of conditional styling */

    return (
      <button onClick={this.handleClick} style={buttonStyle}>
        {this.props.label}
      </button>
    );
  }
}

class TodosLeft extends React.PureComponent {
  activeTodosCount = () =>
    Object.values(this.props.todos).filter(todo => !todo.done).length;

  /* This is a practical reason to have this component as "pure".
     When the "filterLabel" state changes in the parent TodosApp
     component (but the todos array is exactly the same). We do NOT
     need to recompute the active TODOs count. It did not change.
     The purity of this component will make React
     skip that computation for that case. */

  componentDidMount() {
    document.title = Active TODOs: ${this.activeTodosCount()};
  }

  componentDidUpdate() {
    document.title = Active TODOs: ${this.activeTodosCount()};
  }

  /* The 2 lifecycle methods present an example of a "side effect".
     Whenever this component is re-rendered in memory it'll update
     the main document title to show the current active TODOs count. */

  render() {
    return <div>TODOs left: {this.activeTodosCount()}</div>;
  }
}
Verifying minimum renders

The embedded interactive version of the app above counts and displays the number of times each child component gets rendered (the root component is not included as it gets re-rendered on each interaction because it has the top-level state).

Interact with the UI a few times and make sure components "purity" is correctly making React skip re-rendering what does NOT need to be re-rendered. On initial mount (for the test data) 3 TODO items and 3 filter buttons are rendered. The AddTodoForm and TodosLeft components are rendered once.

You can run the code again (CTRL+Enter) to reset the state and counts.

Test #1
Mark a TODO as complete and make sure only 2 re-renders occur: one that updates the TodoItem that was changed, and one to update the TodosLeft count.

Test #2
Click the "Active" filter button and make sure only 2 re-renders occur. Only the 2 FilterButton elements that changed their UI (to indicate which one is current) should be re-rendered. Anything that does not depend on the filterLabel state (the other 3 child components) should not be re-rendered.

Converting from classes to hooks

We have a TODOs App written entirely with React classes. Let’s convert it to use React function components and manage the state and side effects with React hooks instead.

Step #1: Move class methods to the render function

Instead of converting a class into a function in one swoop, I find it helpful to first extract all the methods defined on the class and convert them into regular functions within the render method. All occurrences of this.methodName becomes methodName in the body of the render method. This move would include any code that’s defined in the constructor function for the component.

The only exceptions to this move are the state initializing line (this.state=) and any lifecycle methods like componentDidMount. React hooks use a different approach to managing state and lifecycle methods need special conversions because they are called internally by React. Do these last.

For example, to convert the FilterButton class in this App, here’s what it would look like after this first step:

FilterButton, V1: Converting class methods into regular functions
class FilterButton extends React.Component {
  render() {
    const handleClick = () => this.props.onClick(this.props.label);
    const buttonStyle = {
      fontWeight: this.props.active ? 'bold' : 'normal',
    };
    return (
      <button onClick={handleClick} style={buttonStyle}>
        {this.props.labellabel}
      </button>
    );
  }
}

I just moved the handleClick method to the render method’s scope and used it directly (without this). The new local handleClick function WILL be re-created each time this FilterButton component re-renders.

If this makes you uncomfortable you are not alone ;) One of the common arguments against hooks is how it requires the creation and re-creation of many functions. You need to make peace with that. It’s not really a big deal. However, there are ways to mirror the class-based approach in function components if you need to do that.

Step #2: Destructure props/state

After you move what can be moved make a note of everything in the render method (and all its new local functions) that makes a call to this.props or this.state.

Introduce 2 new lines as the first thing in the render method, one line to destructure all used props from this.props and another for this.state. Then change anything that uses this.props or this.state to use the destructured values directly instead.

In the FilterButton class, only this.props is used in render (it’s a stateless component). There are 3 props used in this component: label, active, and onClick. Introduce a new line to destructure them and get rid of everything this.props inside render:

FilterButton, V2: Getting rid of this.props (changes in bold)
class FilterButton extends React.Component {
  render() {
    const { label, active, onClick } = this.props;

    const handleClick = () => onClick(label);
    const buttonStyle = {
      fontWeight: active ? 'bold' : 'normal',
    };
    return (
      <button onClick={handleClick} style={buttonStyle}>
        {label}
      </button>
    );
  }
}

The only this.X calls that should now exist in the render method are a single this.props (the new one in the first line) and a single this.state if the component has state elements (and there will also be calls to this.setState in handler functions).

Having this intermediate version of the converted component is handy, especially in bigger and more complex components. It’ll help you uncover issues you need to deal with early in the conversion process. You also get to test some things incrementally instead of one final test at the end.

Step #3: Convert render to a function

After the first 2 steps, simple components like FilterButton (which has no state or side effects) have just one more step to go through. The code in the render method becomes the code for the new function component. We just need to move the destructuring line into destructuring within the new function component’s argument:

FilterButton, V3: Converting render into a function
const FilterButton = ({ label, active, onClick }) => {
  const handleClick = () => onClick(label);
  const buttonStyle = {
    fontWeight: active ? 'bold' : 'normal',
  };
  return (
    <button onClick={handleClick} style={buttonStyle}>
      {label}
    </button>
  );
};

Did you notice how the body of the FilterButton function in V3 matches the body of the render method in V2? Only the destructuring line was moved.

At this point, the only this.X calls you have in your components should be this.state and this.setState. This is where you need to introduce a hook.

The only stateful component in this app is the root one (TodosApp). Let’s convert that next.

Step #4: Convert state values and updaters

If a class component uses state elements, then you’ll need to replace each property on that state to use the new useState hook function. Before we do that for TodosApp, let’s take it through the first 3 simple steps. Here is what TodosApp would look like after them:

TodosApp After the first 3 simple steps
const TodosApp = ({ initialData }) => {
  // state = {
  //   todos: this.props.initialData.todos,
  //   filterLabel: 'All',
  // };
  let todos, filterLabel, setState;
  /* These are temp variables so that we can get
     rid of all "this." in the code. They need to be
     replaced with React.useState calls. */

  const addNewTodo = newTodoBody =>
    // setState(prevState => ({
    //   todos: {
    //     ...prevState.todos,
    //     [uniqueId()]: {
    //       body: newTodoBody,
    //       done: false,
    //     },
    //   },
    // }));

  // ... 4 more functions ...

  /* Every setState call will need to be replaced above
     but everything below this point can stay exactly the same */

  const shouldShowTodo = todo => {
    return (
      filterLabel === 'All' ||
      (filterLabel === 'Active' && !todo.done) ||
      (filterLabel === 'Completed' && todo.done)
    );
  };

  return (
    <>
      <header>TODO List</header>
      <ul>
        {Object.entries(todos).map(
          ([todoId, todo]) =>
            shouldShowTodo(todo) && (
              <TodoItem
                key={todoId}
                id={todoId}
                todo={todo}
                markTodoDone={markTodoDone}
                deleteTodo={deleteTodo}
              />
            )
        )}
      </ul>
      <AddTodoForm onSubmit={addNewTodo} />
      <div className="actions">
        Show:{' '}
        <FilterButton
          label="All"
          onClick={setFilter}
          active={filterLabel === 'All'}
        />
        <FilterButton
          label="Active"
          onClick={setFilter}
          active={filterLabel === 'Active'}
        />
        <FilterButton
          label="Completed"
          onClick={setFilter}
          active={filterLabel === 'Completed'}
        />
      </div>
      <div className="actions">
        <button onClick={deleteAllCompleteTodos}>
          Delete All Completed
        </button>
      </div>
      <footer>
        <TodosLeft todos={todos} />
      </footer>
    </>
  );
};

The TodosApp component uses 2 properties on its state: todos and filterLabel. We’ll need to use either a single useState call that manages both of them (as an object) or two useState calls for each.

To make that decision, look at the patterns where setState is used with these state elements. Are they ever updated together? If so, then there might be a value in grouping them under one object.

In the 5 functions we have that update the state, 4 of them update only the todos part and 1 of them updates only the filterLabel part. I think it’s safe to have them as separate state elements in the new hook version. This is the hooks-equivalent to initializing and reading the state:

// Class version

  // 1) Initialize
  state = {
    todos: this.props.initialData.todos,
    filterLabel: 'All',
  };

  // 2) Get values and updater function
  const { todos, filterLabel } = this.state;
  const { setState } = this;


// Hooks version

  // Initialize and get values and updater functions
  const [todos, setTodos] = useState(initialData.todos);
  const [filterLabel, setFilterLabel] = useState('All');

Each call to useState returns an array with 2 items:

  • The first is the current value of the used state

  • The second is an updater function to change the value of the used state

The argument to useState is the initial value assigned to the requested state.

React uses the "order" of the useState calls (and other hooks) to preserve and identify requested states and effects. This allows for very useful features like custom hooks and a few other things. However, it means that you can’t invoke React hooks inside if statements or within loops. You can only invoke them on the top-level of a React function (which is either a component or a custom hooks that’s used in a component). You should use the eslint-plugin-react-hooks to make sure you follow these rules (and get other helpful hints). See the Rules of Hooks for more details on this.

The grouping of many state elements to be updated together is better done with React.useReducer instead of React.useState. If you do it with useState, be aware that unlike this.setState the new state updater functions provided by useState do not "merge" their arguments with the old state. They just set the state.

After that, we replace each setState call that updates the todos property with setTodos passing the new value of todos for it directly:

// Class version

  addNewTodo = newTodoBody =>
    this.setState(prevState => ({
      todos: {
        ...prevState.todos,
        [uniqueId()]: {
          body: newTodoBody,
          done: false,
        },
      },
    }));

// Hooks version

  const addNewTodo = newTodoBody =>
    setTodos(prevTodos => ({
      ...prevTodos,
      [uniqueId()]: {
        body: newTodoBody,
        done: false,
      },
    }));

Note how in the hooks version the "previous state" argument for the setTodos callback argument is the prevTodos object itself. It’s a function that updates only a todos object state (and not a general object state that has a todos property).

Similarly, for the setFilter function we replace setState with a call to setFilterLabel:

// Class version

  setFilter = newFilterLabel => this.setState({ filterLabel: newFilterLabel });

// Hooks version

  const setFilter = newFilterLabel => setFilterLabel(newFilterLabel);

The hooks version setFilter function can actually be eliminated because it just calls another function with its exact same argument. We can use the setFilterLabel directly as the handler for onClick properties on the 3 FilterButton elements:

        <FilterButton
          label="All"
          onClick={setFilterLabel}
          active={filterLabel === 'All'}
        />

        {/* ... 2 more FilterButton ... */}

That’s it. Since this component did not use any lifecycle methods, it’s now ready. See the full version below.

Step #5: Convert lifecycle methods

The TodosLeft component makes use of both the componentDidMount and componentDidUpdate methods to keep the app title in sync each time the number of active TODOs left is re-computed.

To do that with a function component, we need to use the React.useEffect hook. This hook function takes 2 arguments: a callback function and an array of dependencies.

useEffect(() => {
  // Do something after each render
  // but only if dep1 or dep2 changed
}, [dep1, dep2]);

Here’s how the useEffect callback is handled:

  • The first time React renders a component that has a useEffect call it’ll invoke its callback.

  • After each new render of that component if the values of the dependencies are different from what they used to be in the previous render React will invoke the callback function again.

Furthermore, if the component is re-rendered or removed from the DOM, React can also invoke a "cleanup" function. That cleanup function can be returned from the useEffect callback function. We don’t need a cleanup function for this particular case.

We would need a cleanup function if we want to "revert" the title to its initial value before the app is rendered. I’ve done exactly that in this article.

Setting the title value for this app is a side effect that needs to be invoked each time the pure TodosLeft component is re-rendered. Its only dependency is the count of active TODOs which is computed on each render.

Here’s how the useEffect function can be used to implement the side effect of syncing the title:

// Class version

  activeTodosCount = () =>
    Object.values(this.props.todos).filter(todo => !todo.done).length;

  componentDidMount() {
    document.title = Active TODOs: ${this.activeTodosCount()};
  }

  componentDidUpdate() {
    document.title = Active TODOs: ${this.activeTodosCount()};
  }

// Hooks version

  const activeTodosCount =
    Object.values(todos).filter(todo => !todo.done).length;

  useEffect(() => {
    document.title = Active TODOs: ${activeTodosCount};
  }, [activeTodosCount]);

Note how in the hook version there is only one place for what was 2 places in the class version. The side effect is invoked after both the initial mount and after each update. We did not need a function to compute the active TODOs count. It’s only computed once, used in the returned output, and then used in the body of the callback function for the side effect hook (thanks to JavaScript function closures).

Convert the other 2 components in the App on your own. You can see the full converted version below.

The core concepts in the hooks version

Here’s the full version of the example after converting all components with the simple strategy we used above. I used inline comments to further explain the new concepts used in this version. If you need to experiment with the code, the interactive version is included at the end of the lesson:

const TodosApp = ({ initialData }) => {
/* There is no "instance" concept when using React function components.
   React just calls the functions each time it needs to render them.
   The "props" data is passed to the functions as its only argument. */

  const [todos, setTodos] = useState(initialData.todos);
  const [filterLabel, setFilterLabel] = useState('All');

  /* The state is externally managed in memory and
     made accessible through the useState hook. */

  const addNewTodo = newTodoBody =>
    setTodos(prevTodos => ({
      ...prevTodos,
      [uniqueId()]: {
        body: newTodoBody,
        done: false,
      },
    }));

  const markTodoDone = (todoId, newDoneValue) =>
    setTodos(prevTodos => ({
      ...prevTodos,
      [todoId]: {
        ...prevTodos[todoId],
        done: newDoneValue,
      },
    }));

  const deleteTodo = todoId =>
    setTodos(prevTodos => {
      const { [todoId]: _, ...todos } = prevTodos;
      return todos;
    });

  const deleteAllCompleteTodos = () =>
    setTodos(prevTodos =>
      Object.entries(prevTodos).reduce((acc, [todoId, todo)] => {
        if (!todo.done) {
          acc[todoId] = todo;
        }
        return acc;
      }, {})
    );

  /* The 4 functions above are mutation functions. They update
     state elements using their updater functions (setTodos).
     By using a function as the argument to setTodos,
     these functions don't need access to "current" value of the
     todos state. This is better than using a direct value
     for setTodos because it allows these mutation functions to be
     memoized.
     Note how the setFilter operation did not need a function here
     because the setFilterLabel updater function can simply be used
     directly in component JSX. 1 less function for the win. */

  const shouldShowTodo = todo =>
    filterLabel === 'All' ||
    (filterLabel === 'Active' && !todo.done) ||
    (filterLabel === 'Completed' && todo.done);

  /* This function is a computation function. It currently depends
     on reading the filterLabel state element directly in its
     scope (using closures). This is not ideal as the function
     itself need to be recreated every time the component renders.
     It can be memoized to only get re-created when the filterLabel
     element changes but since it is not passed down to any child
     elements in JSX a memoization in this case is not necessary. */

  return (
    <>
      <header>TODO List</header>
      <ul>
        {Object.entries(todos).map(
          ([todoId, todo]) =>
            shouldShowTodo(todo) && (
              <TodoItem
                key={todoId}
                id={todoId}
                todo={todo}
                markTodoDone={markTodoDone}
                deleteTodo={deleteTodo}
              />
            )
        )}
      </ul>
      <AddTodoForm onSubmit={addNewTodo} />
      <div className="actions">
        Show:{' '}
        <FilterButton
          label="All"
          onClick={setFilterLabel}
          active={filterLabel === 'All'}
        />
        <FilterButton
          label="Active"
          onClick={setFilterLabel}
          active={filterLabel === 'Active'}
        />
        <FilterButton
          label="Completed"
          onClick={setFilterLabel}
          active={filterLabel === 'Completed'}
        />
      </div>
      <div className="actions">
        <button onClick={deleteAllCompleteTodos}>
          Delete All Completed
        </button>
      </div>
      <footer>
        <TodosLeft todos={todos} />
      </footer>
    </>
    /* What's returned here is exactly equivalent to the class version except
       instead of "this.something", we just use "something" directly. */
  );
};

const TodoItem = ({ todo, markTodoDone, deleteTodo }) => {
  const handleCheckboxChange = event => {
    const newDone = event.target.checked;
    markTodoDone(todo.id, newDone);
  };

  const handleXClick = () => {
    deleteTodo(todo.id);
  };

  /* Both of the handlers above depend on "todo" which is a prop for
     this component. This also means they have to be recreated every time
     this component is re-rendered by its parent. */

  const todoStyle = { textDecoration: todo.done ? 'line-through' : 'none' };
  /* Style computing is done exactly the same except its now part of the
     function component body. In class components, this line was in the
     render function itself. */

  return (
    <li>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={handleCheckboxChange}
      />
      <span style={todoStyle}>{todo.body}</span>
      <span role="link" onClick={handleXClick}>
        X
      </span>
    </li>
  );
};

const AddTodoForm = ({ onSubmit }) => {
  const handleSubmit = event => {
    event.preventDefault();
    onSubmit(event.target.todoBody.value);
    event.target.todoBody.value = '';
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="todoBody" placeholder="What TODO?" />
      <button type="submit">Add TODO</button>
    </form>
  );
};

const FilterButton = ({ label, active, onClick }) => {
  const handleClick = () => onClick(label);
  const buttonStyle = {
    fontWeight: active ? 'bold' : 'normal',
  };
  return (
    <button onClick={handleClick} style={buttonStyle}>
      {label}
    </button>
  );
};

/* Not much has changed for the AddTodoForm and FilterButton components
   except for omitting this.props. The props are now destructured and
   used directly in the body of the components' functions. */

const TodosLeft = ({ todos }) => {
  const activeTodosCount =
    Object.values(todos).filter(todo => !todo.done).length;

  useEffect(() => {
    document.title = Active TODOs: ${activeTodosCount};
  }, [activeTodosCount]);

  return <div>TODOs left: {activeTodosCount}</div>;

  /* Each time the TodosLeft component is re-rendered, it'll compute
      the new value of activeTodosCount and use it in the output.
      The side effect hook function will also update the document
      title with the new count on each render. \*/

};

This hooks version is about 150 LOC (compared to 180 LOC for the class version). HOWEVER, this is a VERY wrong metric to use to say that the hooks version is better. In fact, this hooks version introduced a few issues that were not in the class version. The good thing about this hooks version is that there is no confusing "this", which makes the code a little bit easier to deal with. However, the hooks version heavily depends on JavaScript closures and if you’re not comfortable with them you will write buggy React function components.

Check out the jsComplete interactive lab on closures at jscomplete.com/closures.

The issues this hooks version introduced might not be significant for this little app but the transformation we’ve done so far is not complete. We will have to optimize function creation in the hooks version to make it completely equivalent to the class version. Let’s do that next.

Analyzing the hooks version

We converted the TODOs app from using class and made it use functions and hooks. This simple conversion comes with a small penalty. All functions that were previously part of a component "instance" will now be recreated on each and every render of a function component. The instance in class-based components preserves the identity for these functions.

Similarly, the PureComponent class is another form of caching/memoizing a section of a class-based React app.

Optimizing the hooks version to match the class version is the tricky part of this conversion. The way functions and hooks work is vastly different from how classes and instances work. There is no easy way to compare the 2 versions performance without measuring. However, there are a couple of things we can do right away. We can use React.memo to replace the PureComponent concept and we can use React.useCallback to preserve the identity of functions that need to be passed down to children props.

Pure function component with React.memo

To make a React function component a pure one, you just wrap it with a React.memo call:

// Class version

class TodosLeft extends React.PureComponent {
  // ...
}

// Hooks version

const TodosLeft = React.memo(function TodosLleft ({ todos }){
  // ...
});

The React.memo function takes a function component as an argument and returns its result after memoizing it (for the used input props). When it’s time to re-render this component, if the component props are exactly the same (using a shallow compare) React will skip rendering the component and just reuse the last rendered result.

Note how I used JavaScript regular function syntax for the TodosLeft function instead of the arrow function I used before. This is just to give the memoized component a name (while defining it inline). If you pass an anonymous arrow function to React.memo it’ll report the component as just "Memo" in React’s dev tools. Using a named regular function it’ll report the component as Memo(TodosLeft), which is much better (in development).

We can use a React.memo wrapping call for all 4 child components that used to extend React.PureComponent in the class version. I’ve done this in hooks version used below.

Preserving function identity with React.useCallback

When React renders a component, all functions defined within that component will get a different identity (place in memory). The old functions will be removed and new ones will be defined.

For example, on each render of the AddTodoForm component, a new handleSubmit function will be created. This was not the case in the class version where we stored handleSubmit on the instance of AddTodoForm. The class version of AddTodoForm will create the handleSubmit function on the first render and then re-use the same function on each re-render.

In most cases, this is not a big deal. A JavaScript engine like V8 will take care of creating and discarding functions and the overhead of creating new functions is minimal. However, when the functions being created are passed down to pure children component they will cause the pure component to re-render even when nothing has changed in its props. Just the fact that we have a new function will invalidate the pure component memoization.

To see this problem in action, test the app interactions in both versions below. The first one is the class version, and the second one is the hooks version so far after memoizing all pure components. Both of them will keep track of how many times the child components are rendered.

Just mark a TODO as done in both versions and check the render counts:

The class version:

The hooks version:

Marking a TODO as done in the class version caused 2 components to re-render. The TodoItem component that changed the UI for the newly done TODO and the TodosLeft component to update the active TODOs count. This makes sense.

Marking a TODO as done in the hooks version caused 5 components to re-render! All TodoItem elements used were re-rendered in addition to AddTodoForm and TodosLeft. This is what’s known in React as "wasteful renders". 3 of these 5 re-renders were not necessary. Let’s focus on the AddTodoForm. Why exactly was it re-rendered when me marked a TODO as done? Didn’t we memoize the thing!?

The reason is, when the TodosApp component re-renders when we changed the state of one TODO, the addNewTodo function was re-created. It now has a new identity that’s different from the one used in the first render. This means the onSubmit prop for the AddTodoForm is different now (because it uses a new addNewTodo version) so React will not use the memoized version of AddTodoForm. This can be avoided because the newly created addNewTodo function is exactly the same as the old one. It does need to be "refreshed" on each render. It does not depend on any local variables. The only variable it depends on is the setTodos updater function and that function is guaranteed to be the exact same value for all renders.

This can be solved by preserving the identity of the first created addNewTodo function using the React.useCallback hook function. Also, If you need to preserve the identity of a computed scalar value (like an object or an array), you can use the React.useMemo hook function.

These 2 hooks are similar in signature but they have different purposes. They take the same 2 arguments:

  1. The first argument is a function. In useMemo that function is usually referred to as the "create" function while with useCallback it’s usually referred to as the "callback" function.

  2. The second argument is an array of dependencies (sometimes referred to as inputs and it’s similar to the one we’ve seen for the useEffect hook function).

useCallback(function callback() { }, [dep1, dep2, ...]);
    useMemo(function   create() { }, [dep1, dep2, ...]);

The first time React invokes these functions (within a component’s render cycle), they return different things:

  • useCallback returns its callback function. It does not invoke it.

  • useMemo invokes its create function and returns whatever that create function returns.

So if you have the following:

const C = useCallback(() => [1, 2, 3], []);

const M = useMemo(() => [1, 2, 3], []);

What is the value of C and M?

  • C is a function that returns [1,2,3]

  • M is [1,2,3]

useCallback returns a memoized function reference while useMemo returns a memoized computed value. If the value you want to cache is itself a function (say fn) then you can think of

useMemo(() => fn, deps)

as being equivalent to:

useCallback(fn, deps)

Further invocations to the useCallback and useMemo functions in subsequent renders will only make these functions "redo their thing" if and only if a value in their array of dependencies has changed. So if all the values in the array of dependencies are exactly the same (or if the array of dependencies was empty) then all subsequent invocations to a useCallback function will return the exact same reference to the callback function that was defined on the first render, and all subsequent invocations to a useMemo function will return the exact same reference to the object that was created on the first render.

The useMemo hook is useful when you have expensive computations that you want React to skip doing when you know their result will be exactly the same. Both useMemo and useCallback are useful when you need to include functions or computed values as dependencies for other hooks (like useEffect). They are also often used with React.memo to avoid wasteful renders, which is what we need for this example.

You can wrap any function defined in a React component with a useCallback call and React will preserve its identity in the next render if its list of dependencies are exactly the same. Note that both useCallback and useMemo functions HAVE to be used with a list of dependencies as otherwise they are useless. If what you’re trying to memoize has no dependencies at all, you need to pass an empty array as the list of dependencies. This is the case for the addNewTodo function. It does not have any dependencies. Here’s how we memoize it:

// In the TodosApp function component:

  const addNewTodo = useCallback(
    newTodoBody =>
      setTodos(prevTodos => ({
        ...prevTodos,
        [uniqueId()]: {
          body: newTodoBody,
          done: false,
        },
      })),
    []
  );

With that change, the onSubmit prop for AddTodoForm will be exactly the same when TodosApp re-renders and because AddTodoForm function is itself memoized, React will skip re-rendering it.

Is this better though?

Maybe…​ The useCallback and useMemo hooks come with some penalties. First, they make the code harder to read but you can probably get over that fairly quickly. However, keep in mind that they will make your code do a few extra things. They’ll make it allocate arrays in memory, do a new function call that loops over an array of values and compare things, and hold on to references in memory (blocking things from being garbage collected).

You should only use these optimization hooks if you know that what you’re trying to optimize is far worse than all of the penalties they will introduce. The only way to know for sure whether introducing them is better or worse is to measure things before and after using them.

For example, it’s probably not a good idea to memoize the handleSubmit function inside AddTodoForm as it’ll just add extra work for React. This function is not passed down to any child components and therefore not responsible for any wasteful renders. That’s generally a good rule of thumb. If a function is passed as a prop to a memoized component then preserving its identity might be good especially if the memoized component makes any expensive computations.

The functions that are passed as props to memoized components in the hooks version are addNewTodo, markTodoDone, deleteTodo, and deleteAllCompleteTodos. These should probably be memoized but all the other functions like shouldShowTodo and the event handlers should not be memoized at all.

However, since all 4 functions to be memoized share the fact that they don’t depend on any local variables, instead of using 4 useCallback functions (and dealing with the penalties 4 times per render) we can simply group them under an "actions" object and memoize that object with useMemo (and get the penalties just ONE time per render).

const {
  addNewTodo,
  markTodoDone,
  deleteTodo,
  deleteAllCompleteTodos,
} = useMemo(
  () => ({
    addNewTodo: newTodoBody =>
      setTodos(prevTodos => ({
        ...prevTodos,
        [uniqueId()]: {
          body: newTodoBody,
          done: false,
        },
      })),
    markTodoDone: (todoId, newDoneValue) =>
      setTodos(prevTodos => ({
        ...prevTodos,
        [todoId]: {
          ...prevTodos[todoId],
          done: newDoneValue,
        },
      })),
    deleteTodo: todoId =>
      setTodos(prevTodos => {
        const { [todoId]: _, ...todos } = prevTodos;
        return todos;
      }),
    deleteAllCompleteTodos: () =>
      setTodos(prevTodos =>
        Object.entries(prevTodos).reduce((acc, [todoId, todo]) => {
          if (!todo.done) {
            acc[todoId] = todo;
          }
          return acc;
        }, {})
      ),
  }),
  []
);

Not only is this better in terms of the number of times your code has to deal with the memoization complexities but it also just makes sense to have these actions grouped together. They operate on the same object. In fact, this move should hint that maybe this actions object along with a list of todos should be extracted into some sore of "list manager". We can use the same actions object with other lists that has a structure similar to the TODOs list.

Checkout this article where I do exactly that using a React "custom hook":

Here’s the final version of the hooks way after memoizing the 4 state mutating action functions. Compare the version below with the class version above and see how the number of render React is now doing is exactly the same.

But remember, even after all these optimizations for the hooks version, we cannot and should not tell which version is performing better without measuring. For example, use Chrome DevTools performance tab to record both CPU and memory usage in both versions while doing exactly the same steps in the app and compare memory usage, time spent scripting, time spent rendering, and the many other measures this tool report. Only then you can make an educated decision on which version is better.

I did a few quick measurements and the hooks version slightly out-performed the class one ;)