A blog about React and related technologies

A high level overview of React Hooks

I started using React Hooks for my new projects and I love the simplicity of code and increased productivity. In this post I'll give you a high level overview of Hooks and how you can start using them now without having to learn everything there is to know about them.

What are Hooks

Hooks allow us to use state and other React features in function based components, you no longer need to write class based components. It's not that there's anything wrong with classes, but Hooks will most likely make your code much simpler and improve reusability.

React provides some built-in Hooks and you can also create your own.

Hooks included in React

useState

The useState Hook can be called inside a function component to add some local state to it. React will preserve this state between renders. useState returns an array with two items:

  • the current state value
  • a function that lets you update it (e.g. to be called from an event handler)

The only argument to useState is the initial state.

One important difference between this.setState in class components is that with Hooks updating a state variable always replaces it instead of merging it.

Here's an example of a simple search input:

import React, { useState } from 'react';

const Search = () => {
  const [searching, setSearching] = useState(false);
  const [keywords, setKeywords] = useState('');

  return (
    <div className="search">
      <input type="search" value={keywords} onChange={e => setKeywords(e.target.value)} />
      <button onClick={() => setSearching(true)}>Search</button>
    </div>
  );
};

export default Search;

We're using the useState hook twice, to set the keywords to state as the user types in, and set searching boolean to true once the user clicks the search button.

But how do we perform an actual search?

useEffect

The useEffect Hook adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount, componentDidUpdate and componentWillUnmount in class based components, but unified into a single API.

By default, React runs the effects after every render, including the initial one. React guarantees the DOM has been updated by the time it runs the effects.

For our search example, we can use the useEffect Hook to perform the search, but we need to check whether the user clicked the search button before searching, otherwise the search would be performed every time the component rerenders (in this case every time the user types in something into the search input).

import React, { useState, useEffect } from 'react';

const Search = () => {
  const [searching, setSearching] = useState(false);
  const [keywords, setKeywords] = useState('');

  useEffect(() => {
    if (searching && keywords != '') {
      console.log('Search for keywords: ', keywords);
      // this is where we would call an API to perform the search
      return () => {
        setSearching(false);
      };
    }
  });

  return (
    <div className="search">
      <input type="search" value={keywords} onChange={e => setKeywords(e.target.value)} />
      <button onClick={() => setSearching(true)}>Search</button>
    </div>
  );
};

export default Search;

useEffect has a nice feature to perform a cleanup after running an effect. If an effect returns a function, React will run it when it's time to clean up. In our search example, we use it to set searching back to false.

React allows us to skip an effect unless a prop changes. We don't need this feature on our search example, but it would work something like this:

useEffect(() => {
  // call some API
}, [props.id]); // run the effect only if the id changes

Passing an empty array [] of inputs tells React that your effect doesn't depend on any values from the component, so it would run only on mount and clean up on unmount, it wound't run on updates.

Note: You can include multiple useEffect functions in a component and React will run each one, in the order they were specified.

useLayoutEffect

The useLayoutEffect Hook is identical to useEffect, but it fires synchronously after all DOM mutations. You can use it to to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

Basically, useEffect fires after layout and paint, during a deferred event and doesn't block the browser from updating the screen, while useLayoutEffect fires before and you should use it only if you need to ensure that DOM mutations fire synchronously before the next paint so that the user does not perceive a visual inconsistency.

useContext

The useContext Hook lets us subscribe to React context without introducing nesting:

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
}

useContext accepts a context object (the value returned from React.createContext) and returns the current context value, as given by the nearest context provider for the given context.

When the provider updates, the useContext Hook will trigger a rerender with the latest context value.

useReducer

The useReducer Hook lets us manage local state of complex components with a reducer:

function Example() {
  const [todos, dispatch] = useReducer(todosReducer);
}

It's an alternative to useState.

useReducer accepts a reducer of type (state, action) => newState and returns the current state paired with a dispatch method.

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. It also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

useCallback

The useCallback Hook returns a memoized callback:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

You pass an inline callback and an array of inputs. useCallback will return a memoized version of the callback that only changes if one of the inputs has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

useMemo

The useMemo Hook returns a memoized value. You pass a create function and an array of inputs.

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

useRef

The useRef Hook returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

const refContainer = useRef(initialValue);

A common use case is to access a child imperatively. useRef is useful for more than the ref attribute, it is handy for keeping any mutable value around similar to how you'd use instance fields in classes.

useImperativeHandle

The useImperativeHandle Hook customizes the instance value that is exposed to parent components when using ref.

useImperativeHandle(ref, createHandle, [inputs]);

useDebugValue

The useDebugValue Hook can be used to display a label for custom hooks in React DevTools:

useDebugValue(value);

It's not recommended to add debug values to every custom Hook. It's most valuable for custom Hooks that are part of shared libraries.

Hooks Rules

There are two important rules with Hooks:

  • Only call Hooks at the top level - don’t call them inside loops, conditions, or nested functions.
  • Only call Hooks from React function components - don’t call them from regular JavaScript functions. (There is just one other valid place to call Hooks — your own custom Hooks)

Creating custom Hooks

By creating custom Hooks you can extract component logic into reusable functions.

Custom Hooks are more of a convention than a feature. If a function's name starts with use and it calls other Hooks, React will know that it's a custom Hook.

Let's refactor our original search example to remove the search button and instead search when the visitor presses the *Enter key on their keyboard.

First let's create a custom useKeyPress hook that will check which keyboard key the visitor pressed:

function useKeyPress(targetKey) {
  const [keyPressed, setKeyPressed] = useState(false);

  function downHandler({ key }) {
    if (key === targetKey) {
      setKeyPressed(true);
    }
  }

  const upHandler = ({ key }) => {
    if (key === targetKey) {
      setKeyPressed(false);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', downHandler);
    window.addEventListener('keyup', upHandler);
    return () => {
      window.removeEventListener('keydown', downHandler);
      window.removeEventListener('keyup', upHandler);
    };
  }, []);

  return keyPressed;
}

Our custom hook accepts a single parameter, the key that we're expecting to be pressed. We created two event handler functions and added event listeners inside the useEffect Hook. Notice that we passed an empty array [] as the second argument to useEffect to ensure it only runs on mount and unmount. We're also removing event listeners on cleanup.

Now we can use the useKeyPress Hook in our Search component:

const Search = () => {
  const [keywords, setKeywords] = useState('');
  let pressedEnter = useKeyPress('Enter');

  useEffect(() => {
    if (pressedEnter && keywords != '') {
      console.log('Search for keywords: ', keywords);
      // this is where we call an api to perform the search
    }
  }, [pressedEnter]);

  return (
    <div className="search">
      <input type="search" value={keywords} onChange={e => setKeywords(e.target.value)} />
    </div>
  );
};

export default Search;

We removed the search button and searching state variable and only checking whether the visitor pressed Enter. We also added pressedEnter to the array of inputs to skip the effect unless the pressedEnter variable changes.

I hope this was helpful to get you started with React Hooks. Next check out the official Hooks documentation and other Hooks tutorials.