Software Development
Pawel Rybczynski
Pawel Rybczynski
Software Engineer
2021-12-07

A Deeper Look at the Most Popular React Hooks

In the course of many interviews, I noticed that even experienced programmers have a problem with distinguishing Hooks, not to mention their more advanced capabilities. So, I will try to explain in this article how Hooks should be used.

The most important things you need to remember about Hooks:

  • they can be used only in function components - class components have own lifecycle implementation;
  • they always start with use;
  • you can use as may Hooks as you want, but you need to remember that using them has an impact on the overall performance;
  • they must be executed in the same order on each render...but why? Let’s take a look at an example:
import { useState,useEffect } from "react";

export default function FunctionComponent() {
    const [value, setValue] = useState(1);
    const [doubleValue, setDoubleValue] = useState(1);
  
    if (value > 3) {
      useEffect(() => setDoubleValue(value * 2),[value]);
    }
  
    return (
      <>
        <p>{`Single ${value} Double ${doubleValue}`}</p>
        <button onClick={() => setValue(value + 1)}>Check</button>
      </>
    );
  }

At first, you will receive a warning from eslint:

src\FunctionComponent.js
  Line 11:5:  React Hook "useEffect" is called conditionally. <strong>React Hooks</strong> must be called in the exact same order in every component render  .eslintreact-hooks/rules-of-hooks

As you can see, it’s only an eslint warning so you can disable it by adding a command from below at the top of the FunctionComponent

/* eslint-disable react-hooks/rules-of-hooks */

and it will work but only till we fulfill the condition that runs our Hook. The very next thing which we will see is this error.

Uncaught Error: Rendered more hooks than during the previous render.
    React 5
    FunctionComponent FunctionComponent.js:11
    React 12
    unstable_runWithPriority scheduler.development.js:468
    React 17
    js index.js:7
    js main.chunk.js:905
    Webpack 7
react-dom.development.js:15162

Why this happens? React relies on the order in which the Hooks are called as React wouldn’t know what to return for the useEffect because there was no such Hook in the line to check.

Remember, eslint is a powerful tool, it helps us catch a lot of potential bugs and errors. Disabling its warnings is a dangerous thing, always check if ignoring the warning can cause an app crash.

useState

You probably know how it looks ;)

const [value, setValue] = useState(0);

So, you have 4 elements: state (reactive value), update function (setter), actual hook (function) and optional initial value. Why it returns an array? Because we can restructure it as we like.

Now I want to focus on the last element - the initial value. There are two ways to pass the initial state:

  1. By hardcoded value or smth - which will be called on each render
const [value, setValue] = useState(0);
  1. By a function version. It`s really helpful if we want to run the initial state only once, on the very first render. Maybe you need to make a lot of complex calculations to receive the initial state? It will nicely decrease the resource cost, yay!
const [value, setValue] = useState(() => {
  console.log("INIT");
  return 0;
});

How to check that the first way is really called on each render? Create a function and pass it as an initial state:

const checkInit = () => {
  console.log("INIT");
  return 0;
};

const [value, setValue] = useState(checkInit());

And now pass it using the second way:

const checkInit = () => {
  console.log("INIT");
  return 0;
};

const [value, setValue] = useState(() => checkInit());

Cool, right?

noice.png

Another thing which can create bugs in the app flow: you probably know how to update a state, right?

setValue(1);

Right... but what if I want to update the state based on a previous state?

setValue(value + 1);

Yes... But no... What if you'll try to call the setter function twice, one after another? The recommended way to update a state based on the previous state is to use a function. It guarantees that you are referring to the previous state

setValue((prevState) => prevState + 1);
// with objects:
setUser((prevState) => ({ ...prevState, lastName: "Brzeczyszczykiewicz" }));

useEffect

This Hook take 2 arguments (the second one is optional) and we use it to handle side effects. And depending on what we pass as a second argument, the Hook will be called differently:

  1. without second argument - each render
useEffect(() => {
  doSomething();
});
  1. empty array - only on first render
useEffect(() => {
  doSomething();
}, []);
  1. array with dependencies - every time the value in the dependency array changes
useEffect(() => {
  doSomething(value);
}, [value]);

Cleanup

With useEffect, we can use something what is called cleanup. What is it for? It is very useful, but I think it’s best for cleaning event listeners. Let’s say you want to create an event listener which depends on some state. You don`t want to add a new event listener on each state change, because after a few renders there will be so many listeners that it will affect the app’s performance. A great way to avoid such things is to use the cleanup feature. How to do it? Just add a return function to the useEffect.

useEffect(() => {
  console.log("side effect 1", count);
  return () => {
    console.log("DESTROYED 1");
  };
});

useEffect(() => {
  console.log("side effect 2", count);
  return () => {
    console.log("DESTROYED 2");
  };
}, []);

useEffect(() => {
  console.log("side effect 3", count);
  return () => {
    console.log("DESTROYED 3");
  };
}, [count]);

Because it`s inside the useEffect Hook, the return is called depending on the dependency array - on each render, only on the first render, or when the value in the dependency array changes. But when the component is unmounted, cleaning will be called on the second argument no matter what. The return code is called before the actual code from Hook. It's very logical - at first clean the old one, then create a new one. Right?

useEffect(() => {
  // addEventListener
  console.log("Add");
  return () => {
    // removeEventListener
    console.log("Remove");
  };
}, [value]);

So, first, you will receive a remove message, then Add.

There is one thing to be look out for when using useEffect and asynchronous code inside it. Take a look at the code below:

useEffect(() => {
  fetch("https://picsum.photos/5000/5000").then(() => {
    setValue((prevState) => prevState + 1);
  });
}, []);

At first, it looks ok. You are fetching some data, and when the data comes, update the state. And here’s the trap:

Sometimes you will receive such a warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

The reason is that the component can be unmounted in meantime, but the app will still try to update the state of that component after the promise was fulfilled. How to deal with it? You need to check whether the component exists.

useEffect(() => {
  let mounted = true;
  fetch("https://picsum.photos/5000/5000").then(() => {
    if (mounted) {
      setValue((prevState) => prevState + 1);
    }
  });

  return () => {
    mounted = false;
  };
}, []);

Note: There is a very similar Hook => useLayoutEffect() – the callback runs after rendering the component but before the dom is visually updated. It’s useful when working with getBoundingClientRect(), but you should use useEffect by default. Why? Because it can block visual updates - when you have a complex code inside your effect Hook.

useContext

What is it for? Sharing data without passing props. Consists of the following elements:

  1. Created context - data
  2. Context Provider - provide context to all children
  3. Passed value - data you want to share
  4. Hook - to read shared data
const user = {
  name: "Adam",
  lastName: "Kowalski",
};

export const UserContext = createContext(user);

<UserContext.Provider value={user}>{children}</UserContext.Provider>;

In child, you need to import the context and call the useContext Hook and pass that context as an argument.

import { UserContext } from "./App";

const { name } = useContext(UserContext);

return <h1>Hi {name}<>

Voilà. Looks cool. Mostly for passing global data like themes, etc. Not recommended for using in tasks with very dynamic changes.

Of course, we can create a custom context provider and a custom Hook to reduce the boilerplate instead... but I will deal with custom Hooks in the next article.

useReducer

It allows us to manage the state and re-render when the state changes - like useState. It`s similar to the redux reducer. This one is better than useState when state logic is more complicated.

const [state, dispatch] = useReducer(reducer, initialArg);
  • Returns actual state with a dispatch method.
  • Differently than in redux, the initial value is specified when the Hook is called.

There is also a third argument which can be passed to the useReducer - the init function.

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

What is it for? It can be used when we want to reset the state to its initial value. Below you can find a nice example:

// Parent
<ChildComponent initialNumber={1} />
// Child
function init(initialNumber) {
  return { number: initialNumber };
}

function reducer(state, action) {
  switch (action.type) {
    case "change":
      return { number: Math.random() };
    case "reset":
      return init(action.payload);
    default:
      throw new Error();
  }
}

export default function ChildComponent({ getFactorial }) {
  const [state, dispatch] = useReducer(reducer, initialNumber, init);

  return (
    <>
      <h2>Number: {state.number}</h2>
      <button
        onClick={() => dispatch({ type: "reset", payload: initialNumber })}
      >
        Reset
      </button>
      <button onClick={() => dispatch({ type: "change" })}>Draw</button>
    </>
  );
}

ReducerInit.png

useCallback

When to use it? When we want to achieve referential equality (thereby reducing the number of created functions). This Hook returns the function, unlike useMemo which returns the value.

Example: Create a function in the parent component and then pass it via props

// Parent 
const getSquaredValue = () => count * count;
...
return (
  <ChildComponent getSquaredValue={getSquaredValue}>
)

Then check in child component how many times the effect Hook will be called after adding that function to the dependency array:

// Child
useEffect(() => {
  console.log("getSquaredValue", getSquaredValue());
}, [getSquaredValue]);

It will log to the console on every render! Even if the values inside the getSquaredValue() function didn't change. But we can avoid this by wrapping that function in useCallback

const getSquaredValue = useCallback(() => count * count, [count]);

We also can pass some parameters to this function:

const getSquaredValue = useCallback(
  (multiplier) => count * count * multiplier,
  [count]
);

useMemo

const memoizedValue = useMemo(() => {
  return doSomething(value);
}, [value]);
  • It is not neutral when looking at the costs of resources - useMemo must be called on every render, is saves the value in memory and compares (memory overhead),
  • uses Memoization - the optimization technique, specific form of caching.

You should use it in 2 scenarios only:

  1. If you want to prevent calling a complex code on each render;
  2. If you want to achieve referential equality.

Let`s look a little bit closer at the second case. We want to use useEffect with an object as a dependency. Because objects are compared by their reference, useEffect will be called on each render. To avoid such things, we can combine useEffect with useMemo to memoize such objects and then pass that memoized objects to the dependency array. Short example:

First try to do it without useMemo:

const hobbit = { name: "Bilbo" };

useEffect(() => {
  console.log("Hello ", hobbit.name);
}, [hobbit]);

Also, you will receive a warning:

The 'hobbit' object makes the dependencies of the useEffect Hook (line 49) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'hobbit' in its own useMemo () Hook.eslintreact-hooks/exhaustive-deps

Then try with useMemo:

  const hobbit = useMemo(() => {
    return { name: "Bilbo" };
  }, []);

  useEffect(() => {
    console.log("Hello ", hobbit.name);
  }, [hobbit]);

useRef

The most important thing: useRef does not trigger re-rendering (like useState) because it`s not connected to the render cycle - it keeps the same reference between renders.

const ref = useRef(0);

To call the saved value, you need to use a current property (ref is an object) - ref.current

The second case for which we can use that Hook is to reference elements inside HTML. Each element has a ref attribute. So, we can handle focus, events etc.

The third case is that we can use refs to handle uncontrolled components. You can read more about them in react docs, but in short, it looks like this:

export default function UncontrolledForm() {
  const input = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(input.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={input} />
      <button type="submit">Submit</button>
    </form>
  );
}

As you can see, there is no event handler, it just remembers the typed value. It's great for handling basic forms when you just want to read saved values when you need them (like when submitting).

Bonus: It`s great when you need to remember previous state values. You can use for it the useEffect Hook, just pass the state to the ref.

const [value, setValue] = useState("");

let prevValue = useRef("");

useEffect(() => {
  prevValue.current = value;
}, [value]);

<input value={value} onChange={(e) => setValue(e.target.value)}></input>;

As you can see, Hooks are not that obvious. We can combine them to solve many problems. You will surely benefit greatly from studying this topic.

And there are also custom hooks...

End of Part 1

Product development consulting

Read more:

JavaScript Is Totally Dead. Some Dude on the Internet

Deploy GraphQL/MongoDB API Using Netlify Functions

How to Kill a Project with Bad Coding Practises