Ever since the release of React 0.14 in October 2015, developers have the ability to choose between the two types of components: functional and class ones.

The rules for picking the right one to use are pretty simple: you should always favour functional components, unless you need to make use of one (or both) of the following:

  • component state
  • lifecycle methods (such as componentDidMount, componentWillUnmount etc.)

The functional components have some benefits, when compared to their class counterparts:

  • more compact code

    const Label = ({ firstName, lastName }) => {firstName} {lastName};

  • smaller size after the code is transpiled

  • they’re very easy to test in isolation

  • performance - since React 0.16 you can mark functional components as pure using React.memo to optimize their performance

These are all good reason to use functional components. However, the fact that you can’t have stateful functional components, or use lifecycle methods inside them means that you most likely still need to use both types of components in your codebase interchangeably. This will change with the introduction of React Hooks in React v16, which might render classes in React obsolete. Let’s have a look at the new functionality and see what new possibilities it brings to the table.

Overview

The Hooks aim at solving some of the problems that many of the React developers have encountered in their daily work. The authors list 3 main motivations:

  • sharing stateful logic between components

    One of the most known and widely accepted rules of writing maintainable software is DRY - Don’t Repeat Yourself. If there’s some non-trivial logic that you need to apply to multiple areas in your codebase, you’ll almost always want to find a way to extract it into some unit of code that can be then reused, instead of repeating the same code over and over.

    In case of React, this is usually pretty straightforward if the logic doesn’t depend on, or modify, the component’s state - you can just put the logic in a separate function and then import and call it in any component that needs it.

    However, things get more complicated when the logic needs to access the component’s state. There are some good patterns that we can use in such cases, such as Higher-Order Components or Render Props. The problem with them is that, while they solve the issue of sharing stateless behavior between components, they also introduce a problem of their own. Specifically, they increase the code complexity, which makes it harder to understand and modify. In addition to that, they pollute the outputted HTML with wrapper elements, which don’t really belong there, as they don’t add anything to the document’s structure.

  • components become harder to understand and maintain as they keep growing.

    The lifecycle methods often end up containing code that does a number of unrelated things. The code that is related to the same functionality ends up splitted between a number of lifecycle methods. This becomes hard to follow as the component grows.

    Hooks will let us extract a bunch smaller functions from our component. Such functions would group together code based on the concept it’s related to instead of the lifecycle method it belongs to. For example, all the code related to setting up some 3rd party library and then handling the cleanup once it’s no longer needed would live together in the same function.

  • the use of classes makes the code harder to work with.

    Now, this is the point that some might disagree with. If you’re used to object oriented programming, you might have welcomed addition of classes in JavaScript with open hands. The fact that classes result in a more verbose code might not bother you.

    There are, however, some problems with the classes in JS that are not a matter of taste. To name some of them: they don’t minify as well as their functional counterparts, they make hot reloading unreliable and they can diminish the benefits of some performance optimizations that are likely to land in future versions of React.

Hooks in practice

Now that we know why Hooks are useful addition to the library, it’s time to finally see some of them in action.

State hook

The first type of hook that we’re going to examine is a state hook. As the name suggests, it allows us to turn functional components into stateful ones. All that, using a syntax that is both very compact and easy to understand:

import React, { useState } from 'react';

const Echo = () => {
  const [text, setText] = useState("");

  return [
    <div key="text">{text}</div>,
    <input key="input" value={text} onChange={(e) => setText(e.target.value)} />
  ];
}

Let’s look at the elements that might be new to you:

  • we need to import the useState hook from the react package
  • we use this hook directly in our functional component, by calling the imported useState hook, just like a normal function
  • the useState accepts a single argument - the default state
  • the hook returns two-element array; the first element is the current state, the second is a setter function for the state

In the example above the state consists of a single string called text. What if our component needs more complex state? In that case we need to call useState more than once - each time specifying a different substate. Each invocation of the hook will return an array with current value of the substate and a setter for this particular part of the state.

Let’s extend our sample component a bit to include additional variable in the state:

import React, { useState } from 'react';

const Echo = () => {
  const [text, setText] = useState("");
  const [count, setCount] = useState(0);

  return [
    <div key="text">{text}</div>,
    <div key="counter">Clicks: {count}</div>,
    <input key="textInput" value={text} onChange={(e) => setText(e.target.value)} />,
    <button key="counterInput" onClick={() => setCount(count + 1)}>Increase counter</button>,
  ];
}

Here we’ve added a simple clicks counter: we call the hook and pass it a default value of 0. In return, we receive a current value of the counter in the count constant and a setCount function (of course we can decide to use any names we want for these two values). The component now renders two additional elements: the counter value and a button which increases the value every time we click on it.

Effect hook

It is a well established good practice in functional programming, that you should write your functions as pure functions. That is, given the same input, they should always produce the same output and nothing more. In other words, they should have no side effects - things like sending HTTP requests, manipulating the DOM, etc.

This is a great rule to follow - it will make your application behave in a much more predictable way, saving you from a large number of potential bugs. However, you can’t completely run away from the side effects. A typical web application won’t be of much use if it can’t fetch data from the server. So instead of avoiding the side effects completely, we just need to keep an eye on them - which means putting them in a well defined place, so that they can’t leak outside and spread across the whole codebase.

The common pattern is to split components into two types: container components, which do all the heavy-lifting, and pure presentational components, which get all of their data passed to them in props and therefore don’t need to be bothered with the impure task of handling the side effects. In case you’re not familiar with the concept, here you can find a great introduction written by Dan Abramov.

We can’t handle the side effects during the rendering, and therefore we need to plug into the lifecycle of our components. Traditionally, the only way we could do it was with the lifecycle methods, exposed to us by the class components. Here’s where the Effect Hooks come into play: we no longer need to use classes for our container components.

Let’s see a code sample:

import React, { useState, useEffect } from 'react';
import MessagesChannel from 'channels/messages';

const Message = ({ text, author }) =>
  <li>
    { author }: { text }
  </li>

const Messenger = ({ topic }) => {
  const [messages, setMessages] = useState([]);

  const handleIncomingMessage = (msg) => setMessages([...messages, msg]);

  useEffect(() => {
    MessagesChannel.subscribe(topic, handleIncomingMessage);

    return () => {
      MessagesChannel.unsubscribe();
    };
  });

  return (
    <ul>
    { messages.map((msg) => <Message {...msg} />) }
    </ul>
  );
}

The above code renders a component that displays a list of messages coming from an external channel. For the sake of brevity, we don’t cover sending messages. We also won’t examine the implementation details of the API client.

Just like in previous examples, we need to import the hook from the react package. This time, we need the useEffect hook. Again, the hook is called just like a regular function. In case of useEffect it expects us to pass it a function. The function is called after every render (including the first one). In our case, we subscribe to the MessagesChannel and pass it a topic and a function that will update the component’s state whenever new message arrives. The function can return another function, which will be called when component unmounts. We can use that to clean things up - in our case we unsubscribe from the channel.

This code will work, but it’s not optimal. As mentioned before, the default behavior of effect hooks is to be called for each re-render. It means that every time the component is rendered, we would first unsubscribe from the channel, only to subscribe to it again the moment later. We can optimize that. useEffect accepts a second parameter: an array of values that is compared between re-renders. The effect will be ‘skipped’ if none of the elements have changed since the previous render.

Let’s update our code to make use of this fact:

import React, { useState, useEffect } from 'react';
import MessagesChannel from 'channels/messages';

const Message = ({ text, author }) =>
  <li>
    { author }: { text }
  </li>


const Messenger = ({ topic }) => {
  const [messages, setMessages] = useState([]);

  const handleIncomingMessage = (msg) => setMessages([...messages, msg]);

  useEffect(() => {
    MessagesChannel.subscribe(topic, handleIncomingMessage);

    return () => {
      MessagesChannel.unsubscribe();
    };
  }, [topic]);

  return (
    <ul>
    { messages.map((msg) => <Message {...msg} />) }
    </ul>
  );
}

As long as we’re subscribed to the same topic, React will skip the effect and avoid the unnecessary re-subscribing.

Context hook

Another type of hook available to us is the useContext hook. Contexts are relatively new addition to the React developer’s toolbox. Describing them in detail is out of the scope of this article, but to sum it up in one sentence: contexts allow us to pass data down the component tree, without the need to manually pass the props between all of the parent components. If you’re not familiar with them, I strongly encourage you to take a look at the official guide. useContext hook gives us a convenient way to use contexts in our components.

First, let’s introduce an example of using context without hooks:

import React from "react";

const TextContext = React.createContext();
const NumberContext = React.createContext();

const App = () =>
  <TextContext.Provider value="ABC">
    <NumberContext.Provider value={42}>
      <Box />
    </NumberContext.Provider>
  </TextContext.Provider>

const Box = () =>
  <div>
    <Text />
  </div>

const Text = () =>
  <TextContext.Consumer>
    {text =>
      <NumberContext.Consumer>
        { number => `${text}: ${number}` }
      </NumberContext.Consumer>
    }
  </TextContext.Consumer>

The above code will result in the text “ABC: 42” being rendered. It works as expected, but when the component receives data from multiple contexts, it quickly becomes unreadable due to excessive nesting. An alternative approach is to utilize useContext hook:

...

const Text = () => {
  const text = useContext(TextContext);
  const number = useContext(NumberContext);
  return `${text}: ${number}`;
}

The rendered content is exactly the same, but the code is arguably tidier.

Reducer and Ref hooks

Reducer hook is one that has a chance of greatly affecting the React ecosystem. I think it’s fair to say that to anyone with some experience in React, the word “reducer” immediately brings Redux to mind. The state management library introduced in 2015 is now a de facto standard for application state management. Of course there are alternatives, but none of them gets close to the popularity of Redux. Introduction of the useReducer hook can render Redux obsolete for many applications, especially if combined with contexts.

Here’s an example:

import React, { useReducer, useRef } from 'react';

const todosReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [action.payload.todo, ...state];

    default:
      return state;
  }
}

const TodoList = () => {
  const inputRef = useRef();
  const [todos, dispatch] = useReducer(todosReducer, []);

  const handleAddTodo = (e) => {
    e.preventDefault();
    const todo = inputRef.current.value;
    dispatch({ type: 'ADD_TODO', payload: { todo }})
    inputRef.current.value = '';
  }

  return (
    <div>
      <form onSubmit={handleAddTodo}>
        <input ref={inputRef}/>
      </form>
      <ul>
        { todos.map((todo) => <li>{todo}</li>) }
      </ul>
    </div>
  );
}

Let’s quickly go through the code.

First, we define the todosReducer. It’s a function that accepts the current state and the action - it’s literally the same as Redux reducers. It returns the updated state, after modifying it according to the action type and additional payload, if necessary. Inside our component, we call useReducer and pass it two parameters: the reducer and initial state. The returned array consists of two elements: the current state (which we assign to a constant named todos and the dispatch function.

Next, we define the handleAddTodo function which dispatches an ADD_TODO action with the new todo in its payload and resets the input. Finally, we render the form with an input and a list of todos.

One more thing worth noting is the useRef hook. We use it to access the form input from within the handler function, however it can be used to hold any value, much like the instance variable would in the class component. You can read more about it here.

Other hooks

There are a few more hooks that you should know about:

The callback hook accepts a function and an array of inputs and returns a memoized callback. It will only be called again if any of the inputs changes.

import { useCallback } from 'react';

const memoizedCallback = useCallback(() => someFunction(a, b, c), [a, b, c]);

Similar to that is the useMemo hook, which returns a memoized value.

import { useMemo } from 'react';

const memoizedValue = useMemo(() => someFunction(a, b, c), [a, b, c]);

The future


So, how will the introduction of hooks affect the way we write React applications?

The first change they might bring is the demise of class components. Functions are already a preferred way of writing components, and the only reason to use class-based ones was the lack of support for state and lifetime methods in functional components. It no longer remains true, thanks to the hooks. That being said, it’s important to note that you are not, in any way, forced to use hooks. They are completely opt-in - any code that is currently working, will works just as well once Hooks land in the production release of React. You can just start using them in the new components you write, while leaving all the existing ones untouched, if you wish - the React team doesn’t plan to remove classes from React anytime soon.

The second big change might be decrease in Redux popularity - useReducer hook in combination with useContext can fulfill many of the state management needs without relying on additional library. However, I don’t think Redux will loose it’s utility completely, at least in the near future. There are still valid reasons to use it, especially for larger applications. It has a whole ecosystem of libraries and dev tools built around it. Of course many of them will be updated to work with the reducer hook, but for now Redux still keeps some advantages.

At the time of writing, the plans are for Hooks to be released in version 16.7 scheduled for the first quarter of 2019. You can already start using them in the alpha version (but be warned that the API is still a subject to change). If you decide to use it, be sure to also check out the ESLint plugin.

Post by Kamil Bielawski

Kamil has been working with AmberBit since 2012, and is an expert on JavaScript and Ruby, recently working with Elixir code as well.