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:
srcFunctionComponent.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:
By hardcoded value or smth – which will be called on each render
const [value, setValue] = useState(0);
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!
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
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:
without second argument – each render
useEffect(() => {
doSomething();
});
empty array – only on first render
useEffect(() => {
doSomething();
}, []);
array with dependencies – every time the value in the dependency array changes
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.
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?
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.
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:
Created context – data
Context Provider – provide context to all children
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.
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
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
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:
If you want to prevent calling a complex code on each render;
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:
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
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:
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.
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…
In conclusion, React hooks have revolutionized the way React developers approach building web applications . By providing a more intuitive and efficient way to manage state and lifecycle in functional components, hooks have become an integral part of React development .
Whether you’re a seasoned developer or just starting with React, understanding the most popular hooks and their use cases is crucial. With hooks like useState, useEffect, useContext, and more, React components can be built with cleaner and more reusable code. Moreover, the ability to create custom hooks allows developers to encapsulate and share logic across multiple components, promoting code reusability and modularity. As React continues to evolve and introduce new features, hooks will undoubtedly play a central role in leveraging the full potential of the framework.
So, whether you’re working on a small function app or a large-scale web application, embracing React hooks will enhance your development workflow and unlock a plethora of possibilities for creating robust and feature-rich React applications .