React Hooks Deep Dive
Converting From Classes To Hooks

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.