React Hooks Comprehensive Guide — Best Practices and Code Snippets.
TL;DR — useState, useMemo, useRef, useEffect, useLayoutEffect, useContext, useCallback
I started my React journey with the arrival of React Functional Components (sorry Class Component lovers, I won’t be discussing them here) with the arrival of React 16.8, they provide a cleaner, a more readable and shorter way of arranging your front-end code, however it is important to use them correctly thats allows you to provide a better code, that has no performance issues, and easy to maintain.
The relation between React and Object.is()
React is awesome, one of these details is the algorithm it uses behind the scenes to check if a given state object is actually changed or not.
ReactJS uses Object.is()
to compare the useState, hooks' dependency array, instead of using ===
. It actually is a better choice.
The Object.is()
method determines whether two values are the same value.
Object.is(25, 25); // true
Object.is('foo', 'foo'); // true
Object.is('foo', 'bar'); // false
Object.is(null, null); // true
Object.is(undefined, undefined); // true
Object.is(window, window); // true
Object.is([], []); // false
var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo); // true
Object.is(foo, bar); // false
// Case 2: Signed zero
Object.is(0, -0); // false
Object.is(+0, -0); // false
Object.is(-0, -0); // true
Object.is(0n, -0n); // true
// Case 3: NaN
Object.is(NaN, 0/0); // true
Object.is(NaN, Number.NaN) // true
as you can see above, it does not do well with objects, since it is compares their address in memory rather than their values, so if your objects are plain, small and does not contains any methods USE:
Object.is(JSON.Stringify(foo), JSON.Stringify(bar))
Let’s talk about React famous Hooks
Just a reminder, Hooks are JavaScript functions, but they impose two additional rules:
- Don’t call Hooks inside loops, conditions, or nested functions.
- Only call Hooks from React function components.


UseState
Basic Usage:
import React, { useState, useEffect } from "react";
function EffectsDemo() {
const [isOpen,setIsOpen] = useState(false);
}
and how about use useState with expensive Calculations?
import React, { useState, useEffect } from "react";
function EffectsDemo({arg}) {
const [result] = useState(expensiveCalculation(arg));
}
You may think that since we’re dealing with state here that is shared between subsequent renders, the expensive calculation is only carried out on the first render, just like with class components. You’d be wrong.
Three ways to memoize expensive calculations
Fortunately, React Hooks provide us with three options to handle state that are just as performant as class components.
1.useMemo
function EffectsDemoMemoHook({arg}) {
const result = useMemo(() => expensiveCalculation(arg), [arg]);
}
as a rule of thumb,
useMemo
will only carry out the expensive calculation again if the value ofarg
changes. This is only a rule of thumb though, since future versions of React may occasionally recalculate the memoized value.
2.Passing functions to useState (My Favorite)
function EffectsDemoPassFunctionHook({arg}) {
const [result] = useState(() => expensiveCalculation(arg));
}
This function is only invoked on the first render. That’s super useful.
3.useRef
function RefHooksComponent(props) {
const ref = useRef(null);
if (ref.current === null) {
ref.current = expensiveCalculation(props.arg);
}
const result = ref.current;
}
This one is a bit weird, but it works and it’s officially sanctioned.
useRef
returns a mutable ref object whosecurrent
key points to the argument thatuseRef
is invoked with. This ref object will persist in subsequent renders. So if we setcurrent
lazily like we do above, the expensive calculation is only carried out once.
UseEffect
Always use
useEffect
for asynchronous tasks
This hook will occur after the render phase and he has 3 usages:
- execute code that only runs once, when the component has been rendered, since the dependency array is empty ([]).
import React, { useState, useEffect } from "react";
function EffectsDemoNoDependency() {
const [title, setTitle] = useState("default title");
useEffect(() => {
console.log("this will occur only once");
},[]);
return (
<div>
<h3>{title}<h3>
</div>
);
}
Note: if we are dropping the dependency array, the effect will re-run every single time the component is re-rendered.
- same as above, but will be re-rendered every-time the variables defined in the dependency array will be changed.
import React, { useState, useEffect } from "react";
function EffectsDemoWithDependency({defaultTitle}) {
const [title, setTitle] = useState(defaultTitle);
useEffect(() => {
console.log("this will occur once, and every-time the 'defaultTitle' is changed");
},[defaultTitle]);
return (
<div>
<h3>{title}<h3>
</div>
);
}
- same as above, but we can add a cleanup function right before the component has removed from the DOM.
import React, { useState, useEffect } from "react";function EffectsDemoWithCleanup({prev}) {
useEffect(() => {
const [count, setCount] = useState(0);
const interval = setInterval(function () {
setCount((prev) => prev + 1);
}, 1000); // return optional function for cleanup
// in this case acts like componentWillUnmount
return () => clearInterval(interval);
}, []);
Ok, Got it, and how do I fetching data from the backend with useEffect?!
useEffect(() => {
const url = "https://api.adviceslip.com/advice";const fetchData = async () => {
try {
const response = await fetch(url);
const json = await response.json();
console.log(json); //{"slip": { "id": 135, "advice": "I.."}}
} catch (error) {
console.log("error", error);
}
};fetchData();
}, []);
UseLayoutEffect
The signature is identical to useEffect
, but it fires synchronously after all DOM mutations. Use this 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.
Prefer the standard
useEffect
when possible to avoid blocking visual updates.
UseContext
if you’re not familiar with React Context please read it here.

import { useContext, createContext } from 'react';
const UserContext = createContext('Unknown');function App() {
const userName = "John Smith";return (
<UserContext.Provider value={userName}>
<Layout>
Main content
</Layout>
</UserContext.Provider>
);
}function Layout({ children }) {
return (
<div>
<Header />
<main> {children} </main>
</div>
);
}function Header() {
return (
<header>
<UserInfo />
</header>
);
}function UserInfo() {
const userName = useContext(UserContext);
return <span>{userName}</span>;
}
useCallback
Before diving into useCallback()
usage, let's distinguish the problem useCallback()
solves — the functions equality check.
Functions in JavaScript are first-class citizens, meaning that a function is an object. The function object can be returned by other functions, be compared, etc.: anything you can do with an object.
Let’s write a function getSumFunc()
that returns functions that sum numbers:
function getSumFunc() {
return (a, b) => a + b;
}const sum1 = getSumFunc();
const sum2 = getSumFunc();sum1(2, 3); // => 5
sum2(3, 2); // => 5sum1 === sum2; // => false
sum1 === sum1; // => true
Object.is(sum1, sum2) // => false
//why?!
sum1
and sum2
are functions that sum two numbers.
The functions sum1
and sum2
share the same code source but they are different function objects. Comparing them sum1 === sum2
evaluates to false
.
That’s just how JavaScript objects works. An object (including a function object) equals only to itself.
Different function objects sharing the same code are often created inside React components
function MyComponent() {
// handleClick is re-created on each render
const handleClick = () => {
console.log('Clicked!');
};//handleClick is a different function object on every rendering of
MyComponent.
}
That’s when useCallback(callbackFun, [dep1,dep2,...])
is set to the rescue : given the same dependency values, the hook returns the same function instance between renderings (aka memoization):
import { useCallback } from 'react';
function MyComponent() {
// handleClick is the same function object
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []);
}
Use Case:
Imagine you have a component that renders a big list of items:
The list could be big, maybe thousands of items. To prevent useless list re-renderings, you wrap it into React.memo()
.
The parent component of MyBigList
provides a handler function to know when an item is clicked:onItemClick
callback is memoized by useCallback()
. As long as term
is the same, useCallback()
returns the same function object.
When MyParent
component re-renders, onItemClick
function object remains the same and doesn't break the memoization of MyBigList
.
That was a good use case of useCallback()
.
import { useCallback } from 'react';
import useSearch from './fetch-items';export function MyParent({ term }) {
const onItemClick = useCallback(event => {
console.log('You clicked ', event.currentTarget);
}, [term]);return (
<MyBigList term={term} onItemClick={onItemClick} /> );
}function MyBigList({ term, onItemClick }) {
const items = useSearch(term);
const map = item => <div onClick={onItemClick}>{item}</div>;
return <div>{items.map(map)}</div>;
}
Wrap Up
Thanks for reading this article, don’t forget to clap if you got something out of it!
Don’t hesitate to ping me if you have any questions about the article or ideas for how to improve it.
Thank you.