React Hooks Deep Dive
Analyzing Class Version

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.