Renato Pozzi's Blog

Renato Pozzi's Blog

React Hooks - A Definitive Guide

React Hooks - A Definitive Guide

Subscribe to my newsletter and never miss my upcoming articles

Nowadays hooks are a fundamental part to know for a React developer. What you will find in this article, is an in-depth, detailed guide of what hooks are, and how they work.

What is a Hook?

Hooks are the new feature introduced in the React 16.8 version. It allows you to use state and other React features without writing a class. Hooks are the functions which "hook into" React state and lifecycle features from function components. It does not work inside classes.

Hooks are 100% backward-compatible, which means it does not contain any breaking changes. Also, it does not replace your knowledge of React concepts.

There are many hooks provided by React by default, and also so many community hooks available open-source. Since Hooks are regular JavaScript functions, you can combine built-in Hooks provided by React also into your own “custom Hooks”.

This is a simple example of the useState hook:

const [name, setName] = useState('');

What Problems Does a Hook Solve?

The main goal of Hook is to decouple stateful logic from rendering logic. Hooks let us organize the logic inside a component into reusable isolated units, applying also the React philosophy (explicit data flow and composition) inside a component, rather than just between the components.

Nerd stuff, the Hooks support increases React only by ~1.5kB (min+gzip)

So, looking at this custom useUser hook:

function useUser (id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}

This custom hook, wrap the user fetching logic in order to be reused every time we need the user information:

const { user, isLoading, isError } = useUser(uid);

You can do this because Hooks are fully encapsulated, each time you call a Hook, it gets isolated local state within the currently executing component.

Before the hooks, the only way to do this was to create an extra component to wrap this stateful logic, now you can do it only with a function, so unlike render props or higher-order components, Hooks don’t create a “false hierarchy” in your render tree.

Hooks Rules and Conventions.

Although hooks are very flexible, there is a couple of rules that must be kept in mind:

  • You must use a Hook only in a function component, or in another hook, not in a class component.

  • The Hook definition order matters, so you can't declare hooks in an if statement, for loop, or a nested function.

  • Hooks must be declared at the top level of our function component, they should never be called in such a way that the order in which those hooks are called might ever be different between different renders.

  • Every hook should start with the use prefix, this helps the developers understand what functions are in pure vanilla javascript, and what instead are hooks which I repeat, can be called only in function components.

You can enforce these rules by using the eslint-plugin-react-hooks.

If you wanna go deeper into these rules reason, at the end of the article you can find a reference to a very good article about that.

Do I have to rewrite all the class components of my app to use Hooks?

Absolutely not, you can opt-in with hooks in your new components, and keep using the older as class components.

How component life-cycle is managed with Hooks?

Considering that as the rule says, you can use hooks only in function components, these are the old class components life-cycle methods compared to the new one function components using hooks:

  • constructor: Function components don’t need a constructor. You can initialize the state in the useState call. If computing the initial state is expensive, you can pass a function to useState.

  • getDerivedStateFromProps: Schedule an update while rendering instead.

  • shouldComponentUpdate: Use React.memo.

  • render: This is the function component body itself.

  • componentDidMount, componentDidUpdate, componentWillUnmount: The useEffect Hook can express all combinations of these (including less common cases).

  • getSnapshotBeforeUpdate, componentDidCatch and getDerivedStateFromError: There are no Hook equivalents for these methods yet, but they will be added soon.

Now, hook by hook.

So after this introduction, it's time to explore all the out-of-the-box hooks included in React!

useEffect

This hook works similarly to componentDidMount and componentDidUpdate of the class components. Also, a cleanup function is available which works like the componentWillUnmount on the class components.

const App = () => {
  useEffect(() => {
    // mount function!
    console.log('Component Mounted!');
    // cleanup function!
    return () => console.log('Component Unmounted!');
  })
}

Tips

  • You can use more than one useEffect hook in the same component, in order to organize different logic better!
  • By default, effects run every time the component is re-rendered, in order to avoid this, you can optimize performance by skipping effects!

Here is an example of useEffect with this optimization, only when the id changes, the useEffect will be called again.

const App = ({ id }) => {
  useEffect(() => {
    // mount function!
    console.log('Component Mounted!');
    // cleanup function!
    return () => console.log('Component Unmounted!');
  }, [id]) // effect will run a second time only when id will change!
}

useState

This hook is used to deal with the state in React. The definition returns a stateful value, and a setter function is used to update it.

const App = () => {
  const [name, setName] = useState("");
  const [age, setAge] = useState(24);
  const [animals, setAnimals] = useState(["dog", "cat", "whale"]);
  const [objects, setObjects] = useState({ hello: "Hashnode!" });
};

If you wanna update the state, you can simply call the setter function. You can do it in two ways:

// classic way
const [name, setName] = useState('A Bad Blog');
setName("Hashnode");

// callback way
const [count, setCount] = useState(0);
setCount(count => count + 1);

If you wanna read the current state, you can simply use the stateful value.

const App = () => {
  const [name, setName] = useState("Renato");
  const [age, setAge] = useState(24);

  return (
    <div>
      My name is {name} and i am {age} years old.
    </div>
  );
};

Tips

  • As you can see, you can set an initial value to the useState function, this value will be assigned to the stateful variable.

  • Unlike with classes, the state doesn’t have to be an object. We can keep a number or a string if that’s all we need.

  • If you need to increment a counter like the shown example, do it using the callback way, avoid using the classic way.

useContext

This hook allows you to use the React Context in a more synthetic way.

A quick reminder of what context is used for from the React documentation:

Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult.

The context hook can be used this way:

const theme = useContext(ThemeContext); // ThemeContext must exists.

This hook is particularly useful when you have a lot of wrapped consumers, (the wrapper hell problem), something like this:

  render() {
    return (
      <AuthenticationContext.Consumer>
        {user => (
          <LanguageContext.Consumer>
            {language => (
              <StatusContext.Consumer>
                {status => (
                  ...
                )}
              </StatusContext.Consumer>
            )}
          </LanguageContext.Consumer>
        )}
      </AuthenticationContext.Consumer>
    )
  }

This is pretty awful. Better to do something like this:

const user = useContext(AuthenticationContext);
const language = useContext(LanguageContext);
const status = useContext(StatusContext);

Tips

  • You have to pass the whole context object to useContext the consumer is not enough.

useRef

This hook returns a mutable ref object where the .current property is initialized with the passed argument initialValue.

We can use it this way:

const App = () => {
  const renderTimes = useRef(1);

  return (
    <div>
      Oh yes, the rendering is the number: {renderTimes.current}
    </div>
  );
};

This hook is also used to access a DOM element or a React Component.

const App = () => {
  const btn = useRef();

  useEffect(() => {
    const buttonElement = btn.current;
    console.log(buttonElement); // <button>HELO</button>
  }, []);

  return (
    <button ref={btn}>
     HELO
    </button>
  );
}

By using the .currrent property, we can do actions and changes to DOM elements imperatively using some node instances, such as .focus, .contains, .cloneNode, etc..

Tips

  • On the initial rendering, the value of the useRef connected to a DOM element is null

  • Between the component re-renderings, the value of the reference is persistent.

  • Updating a reference, contrary to updating state, doesn’t trigger component re-rendering.

useReducer

This hook is an alternative to the useState hook, is used to deal with complex state logic, and works similarly to the Redux library.

const [state, dispatch] = useReducer(reducer, initialArg, init);

In reality, Redux has a lot more features than useReducer, they're not exactly the same, but the basic concepts are similar.

Let's make an example by creating a userReducer function with two simple actions:

const userReducer = (state, action) => {
  switch (action.type) {
    case "CREATE_USER":
      return [...state, { username: action.username }];
    case "DELETE_LAST_USER":
      return state.slice(0, -1);
    default:
      throw new Error();
  }
};

This reducer will be used by our Users component to display and manage a users list:

const Users = () => {
  const [state, dispatch] = useReducer(userReducer, []);

  const handleCreate = () => {
    const name = sample(["FOO", "BAR", "BAZ"]);
    dispatch({ type: "CREATE_USER", username: name });
  };

  const handleDelete = () => {
    dispatch({ type: "DELETE_LAST_USER" });
  };

  return (
    <div>
      <button type="button" onClick={handleCreate}>
        Dispatch CREATE_USER
      </button>

      <button type="button" onClick={handleDelete}>
        Dispatch DELETE_LAST_USER
      </button>

      <ol>
        {state.map((el, index) => (
          <li key={index}>{el.username}</li>
        ))}
      </ol>
    </div>
  );
};

The sample function is from the lodash package.

useMemo

Needs to memoize heavy computed value? This hook is for you. It will take a create function and a dependency array as input, the value will be re-calculated only if the dependency has changed.

const memo = useMemo(() => expensiveFn(a, b), [a, b]);

Consider this dummy example with a simple filter function, the same concept is applied with more complex functions.

const App = () => {
  const [values, setValues] = useState([2, 3, 5]);
  const [name, setName] = useState("");

  const oddNumbers = useMemo(() => {
    console.log("called oddNumbers");
    return values.filter((el) => el % 2 === 0);
  }, [values]); // Try to remove values and see what change!

  const changeValues = () => {
    const randomValues = Array.from({ length: 3 }, () => {
      return Math.floor(Math.random() * 10);
    });
    setValues(randomValues);
  };

  const changeName = () => {
    console.log("called changeName");
    setName(sample(["Greg", "Hola", "Minum"]));
  };

  return (
    <div>
      <div>
        <div>All values {values.join(", ")}</div>
        <div>Odd values {oddNumbers.join(", ")}</div>
      </div>
      <div>Current name: {name}</div>

      <button type="button" onClick={changeValues}>
        Change Values.
      </button>

      <button type="button" onClick={changeName}>
        Change Name.
      </button>
    </div>
  );
};

I invite you to remove the values array and see the difference. In the first case, the value will be memorized and the filter is called once until values change, in the second case, removing the dependency array, the filter will be called every time.

Tips

  • If no array is provided, a new value will be computed on every render.
  • Write your code so that it still works without useMemo — and then add it to optimize performance.

useCallback

This hook works the same way as useMemo does, but is used to memoize callbacks.

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

Again, this example may help you to understand what the use case is:

const App = () => {
  const [fruits, setFruits] = useState(["banana", "apple", "orange", "kiwi"]);
  const [number, setNumber] = useState(0);

  const memoizedCb = useCallback(() => {
    return [sample(fruits), sample(fruits)];
  }, [fruits]); // Try to remove this one!

  const changeFruits = () => {
    const reversed = fruits.map((f) => f.split("").reverse().join(""));
    setFruits(reversed);
  };

  const increment = () => {
    setNumber((number) => number + 1);
  };

  return (
    <div>
      <button type="button" onClick={changeFruits}>
        Reverse Fruits
      </button>

      <button type="button" onClick={increment}>
        Increment Number
      </button>
      <div>Current increment: {number}</div>
      <Fruits get={memoizedCb} />
    </div>
  );
};

const Fruits = ({ get }) => {
  const [fruits, setFruits] = useState([]);

  useEffect(() => {
    setFruits(get());
    console.log("Called useEffect from Fruits.");
  }, [get]);

  return fruits.map((fruit, index) => <div key={index}>{fruit}</div>);
};

As you can see in the example, using this hook we can prevent the Fruit component to be re-rendered if we don't change the fruits variable in the App component.

Tips

  • The tips are exactly the same as the useMemo, please check it again.

useLayoutEffect

This hook is identical to useEffect, but it only fires synchronously after all the DOM mutations.

const App = () => {
  useLayoutEffect(() => {
    console.log("All the DOM mutation finished!");
    return () => console.log("Cleanup Function!");
  });
};

When do I need to use this instead of useEffect?

99% of the time, useEffect is the solution to your problem, but you can use this one if for example your component is flickering when the state is updated, or you have a multi-step update phase.

If you want a component flickering example, you can try this one:

const App = () => {
  const [effectValue, setEffectValue] = useState(0);

  useEffect(() => {
    if (effectValue === 0) {
      setEffectValue(10 + Math.random() * 200);
    }
  }, [effectValue]);

  const reset = () => {
    setEffectValue(0);
  };

  return (
    <div>
      <button type="button" onClick={reset}>
        ResetVal
      </button>
      <div onClick={reset}>effectValue: {effectValue}</div>
    </div>
  );
};

If you change useEffect with useLayoutEffect you will see some differences, the component will no longer flicker.

Tips

  • This hook can be useful if you need to make DOM measurements and then make DOM mutations.

  • When you can, it is better to use useEffect because this hook will block the visual updates and may cause some performance issues.

useDebugValue

This hook is pretty simple, can be used to display a label for custom hooks in React DevTools.

This example came from React Docs, but is pretty explicative:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // Show a label in DevTools next to this Hook
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

There is also a second parameter from the function signature which is a function that can be used for performance reasons. This function is called only if the Hook is inspected. It receives the debug value as a parameter and should return a formatted display value.

useDebugValue(date, date => date.toDateString());

In this example, a custom Hook that returned a Date value could avoid calling the toDateString function unnecessarily passing the formatting function as a second parameter.

Tips

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

useImperativeHandle

This is a pretty rare hook. I'll try to do my best to explain to you how it works.

According to the definition on React's official website, this hook customizes the instance value that is exposed to parent components when using ref.

The function signature is this:

useImperativeHandle(ref, createHandle, [deps])

There are two things you can do with this hook:

  • Using useImperativeHandle instead of a simple ref allow us to control the value that is returned.

  • Instead of returning native functions like a blur, focus, etc, we can also return custom-defined functions via the createHandle function.

This way you can achieve a bidirectional flow, without using Redux or the Context APIs.

As I said, this hook is for very uncommon use cases so use it with caution.

An example may be something like this:

const Counter = forwardRef((props, ref) => {
  const [counter, setCounter] = useState(0);

  const increment = () => {
    setCounter((counter) => counter + 1);
  };

  useImperativeHandle(ref, () => ({
    increment,
    dumbFn: () => alert("Intrusion!"),
  }));

  return (
    <div>
      <div>The Counter is {counter}</div>
      <button type="button" onClick={increment}>
        Increment (In Component)
      </button>
    </div>
  );
});

const App = () => {
  const counterRef = useRef();
  const handleClick = () => {
    counterRef.current.increment();
  };

  return (
    <div>
      <button onClick={handleClick}>Increment (Out Component)</button>
      <Counter ref={counterRef} />
    </div>
  );
};

As you can see in the code, there are two buttons, one inside the Counter component, and another outside, using useImperativeHandle i can access the increment function (which I passed from the hook) also in the current property of the counterRef, so I can increment the counter from inside and outside the component without passing a prop following the classing one-way data binding.

The cool thing about this hook is that the dumbFn declared in the Counter component, actually cannot be accessed by the Counter component, but only from the counterRef in the parent!

If you want to try, copy the code and change the function called by the handleClick to dumbFn.

Wrapping Up

Ok so, after this article, you should have a great comprehension of what hooks are and how it works, also you have some knowledge about every out-of-the-box hook currently integrated into React.

If there are any doubts or something is incorrect, please leave a comment under the article. I will do my best to answer or correct the bad parts.

Have a nice day!

Let's Connect!: Twitter | Linkedin

References

The reference for the article mentioned before is here

 
Share this