Demystifying Advanced React Hooks

Coding Canard
The Startup
Published in
14 min readSep 4, 2020

This is not an introductory article. Let’s be clear about that. I am assuming you have experience using React. In this article, I will try to clarify with examples of how and when to use some of the advanced react hooks. Towards the end, I will also show how to write your own hooks.

Photo by Roozbeh Eslami on Unsplash

Why hooks?

You can read about the motivation behind hooks either here or for the sake of time and brevity, below.

Most of the time, complex class components can be difficult to break down into smaller units because of their stateful logic. As the development progress, they become wordy, complicated, confusing and after a certain point, much difficult to read and manage. Hooks allow us to achieve “separation of concerns” by letting us extract component logic into smaller and simpler independent reusable functions. So each time a hook is called, it initializes a local state and encapsulates it within the currently executing component. So, by using hooks, we have:

  • No complex patterns.
  • No code duplication.
  • Increased code readability.

Also as a bonus, no more dealing with the this keyword.

While we’re at it, here’s a quick note on when to use hooks. Basically:

  • Call hooks only at the top level — never inside loops, conditions or nested function. Why? Read this article by Rudi Yardley on unpacking how hooks works. The same hook can be used multiple times inside of a function but only and only at the top level.
  • Call hooks only from React function component — calling hooks inside a hook is valid.

So many hooks!

React provides the following built-in hooks:

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

The first three are the basic hooks and the next 7 are the advanced hooks. In this article, I will try to explain the advanced hooks with the exception of useReducer. I suspect many of you are or have been using useReducer in conjunction with useContext for the purpose of state management. Besides, there are tons of resources on the same, so check those out.

useRef

useRef 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.

Typically in react, a child component re-renders whenever the parent component passes new props to it. However, at times we may want to reference the HTML element directly to modify it imperatively once it has mounted on the DOM. To access the actual HTML element we need to create a react reference and pass is to the element.

Here’s an example of how we’d use useRef to reference an input element for auto focusing. We’re telling react to set focus on the input element on the DOM when the component mounts. The .current property holds the mutable <input/> object.

import React, { useRef, useEffect } from 'react';
import './App.css';
const App = ( props ) => {
const inputRef = useRef(null);
const buttonRef = useRef(null);
useEffect(
() => {
inputRef.current.focus()
}, []
);
return(
<div className="container">
<input type="text"
placeholder="Enter some text here"
name="name"
ref={inputRef} />
<button ref={buttonRef}> Done! </button>
</div>
);
}
export default App;

Try out the code above. You’ll see that as soon as the DOM loads, the input is focused. Some of the most common use cases for using a ref are managing focus, determining element sizes, triggering animations, integrating with DOM manipulating plugins and so on. For your information you can also use a react ref for storing mutable values such as a number or a string or boolean (like you would in useState) if you want to prevent react from re-rendering the DOM. React refs are not reactive. See below to understand what I mean.

import React, { useRef } from 'react';
import './App.css';
const App = ( props ) => {
const intRef = useRef(0);
const increase = () => {
intRef.current += 1;
console.log(`Current value is ${intRef.current}`);
}
const decrease = () => {
intRef.current -= 1;
console.log(`Current value is ${intRef.current}`);
}
return (
<div className='container'>
<div>
Current value is {intRef.current}
</div>
<div>
<button onClick={increase}> + </button>
<button onClick={decrease}> - </button>
</div>
</div>
)
}
export default App;

Even though the integer value is actually changing, there is no change on the DOM. The value rendered on the DOM is still 0. If we had used useState instead of useRef the value on the DOM would also change. However, please don’t do this unless you really know what you’re doing! Use useState instead. It’s much cleaner, nicer and sane.

This is all good! We know how to reference a DOM element from within our react code. BUT! What if you have built your own custom function component and want to allow other developers to take a ref to it? Here’s where you can use forwarding refs. From react doc:

If you want to allow people to take a ref to your function component, you can use forwardRef (possibly in conjunction with useImperativeHandle), or you can convert the component to a class.

This will have to wait until we reach the useImperativeHandle hook.

useCallback

Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

Quick recap of reference equality. Here’s a very small example.

console.log([1, 2] === [1, 2])
// false
const c = [1, 2]
console.log(c === c)
// true

In the first case even though both objects seem identical, they are actually not because these objects are in different locations in memory. In the second case, c refers to the same object i.e. itself. Referential equality checks to see if two given objects refer to the same memory location. What this means in JavaScript is that two objects that look identical are not the same unless they both point at the same object.

When react components are about to re-render, it compares each object in the components to the newly created version of itself. Since all objects in the new version are created anew, they refer to different memory locations thus allowing react to re-render. Therefore, all functions are recreated as well. The purpose of useCallback is to prevent unnecessary recreation of functions and instead return the same function. Only when one of the dependencies changes a new function will be created. This article by Nikolay Grozev explains nicely how and when to use useCallback which I have reproduced below:

import React, { useState, useCallback } from 'react';
import './App.css';
const totalFunctions = new Set();const App = ( props ) => {const [delta, setDelta] = useState(0);
const [total, setTotal] = useState(0);
const increaseTotal = () => setTotal(total => total+delta);
const decreaseTotal = () => setTotal(total => total-delta);
const increaseDelta = () => setDelta(delta => delta+1);
const decreaseDelta = () => setDelta(delta => delta-1);
totalFunctions.add(increaseTotal);
totalFunctions.add(decreaseTotal);
totalFunctions.add(increaseDelta);
totalFunctions.add(decreaseDelta);
return (
<div className='container'>
<div>
Current total is {total}
</div>
<div>
<button onClick={increaseTotal}> + </button>
<button onClick={decreaseTotal}> - </button>
</div>
<div>
Current delta is {delta}
</div>
<div>
<button onClick={increaseDelta}> + </button>
<button onClick={decreaseDelta}> - </button>
</div>
<div>
Total functions {totalFunctions.size}
</div>
</div>
)
}
export default App;

Check out what happens when you run the code above. Notice the total functions count. Initially it is 4. And each time react re-renders it creates 4 new functions.

Now think about this — do we really need to create new increaseTotal and decreaseTotal functions each time when delta itself does not change? We can optimize this by using useCallback. After modifying the code below with useCallback:

import React, { useState, useCallback } from 'react';
import './App.css';
const totalFunctions = new Set();const App = ( props ) => {const [delta, setDelta] = useState(0);
const [total, setTotal] = useState(0);
const increaseTotal = useCallback(() => setTotal(total => total+delta), [delta]);
const decreaseTotal = useCallback(() => setTotal(total => total-delta), [delta]);
const increaseDelta = useCallback(() => setDelta(delta => delta+1), []);
const decreaseDelta = useCallback(() => setDelta(delta => delta-1), []);
totalFunctions.add(increaseTotal);
totalFunctions.add(decreaseTotal);
totalFunctions.add(increaseDelta);
totalFunctions.add(decreaseDelta);
return (
<div className='container'>
<div>
Current total is {total}
</div>
<div>
<button onClick={increaseTotal}> + </button>
<button onClick={decreaseTotal}> - </button>
</div>
<div>
Current delta is {delta}
</div>
<div>
<button onClick={increaseDelta}> + </button>
<button onClick={decreaseDelta}> - </button>
</div>
<div>
Total functions {totalFunctions.size}
</div>
</div>
)
}
export default App;

Now check out what happens when you run the code above. Again notice the total functions count.

Initially the count of total functions is 4. And each time react re-renders it creates 2 new functions and these are the new versions of increaseTotal and decreaseTotal both of which has delta as the dependency. So only when delta changes, react creates new functions. We have also set increaseDelta and decreaseDelta functions to be created only once at the start since these functions are not dependent on anything.

useCallback has reduced the number of functions recreations significantly, and thus memory is optimized. However, for small use cases like above, it may be an overkill.

useMemo

Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.

useMemo is very similar to useCallback. In fact, react doc itself states

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)

useMemo calls the function when one of the dependencies change and returns an object. To clarify, useCallback returns a cached uncalled function unless one of its dependencies changes and useMemo returns a cached object computed by the function unless one of its dependencies changes. The returned object may very well be a function as in above. So we may say that useMemo is the generalized form of useCallback. This hook is useful when we want to prevent any child component from re-rendering unnecessarily when the parent component re-renders. Imagine you have a parent component that contains a child component which has a large list. Every time the parent is re-rendered, the child will also be re-rendered. If the list is huge it will slow down the app’s performance. See an example below:

import React from 'react';
import './App.css';

const App = () => {
const [todos, setTodos] = React.useState(["Bake cookies", "Buy milk", "Buy eggs", "Buy avocado"]);
const [text, setText] = React.useState('');

const handleAddUser = () =>{
setTodos([text, ...todos]);
};

const handleRemove = (index) => {
todos.splice(index, 1);
setTodos([...todos]);
};
const renderList = () => {
console.log("Render List");
return todos.map((item, index) => (
<li key={index} style={{padding: 5}}>
<button type="button" onClick={() => handleRemove(index)}> - </button>
{item}
</li>
))
}
console.log('Render App');

return (
<div className="container">
<div>
<input type="text" value={text} onChange={evt => setText(evt.target.value)} />
<button type="button" onClick={handleAddUser}> + </button>
</div>
<ul>
{renderList()}
</ul>
</div>
);
};

export default App;

See the console log. Without memoizing, the list is rendered every time when the user input changes. This is an extremely small list here. But imagine if the list contains hundreds of thousands of items! Therefore, we need a way to prevent unnecessary re-renders by memoizing the list and only re-render it when an item is added or removed from the list. And for this react provides the useMemo hook. Below is the same code modified with useMemo.

import React, { useCallback, useMemo } from 'react';
import './App.css';

const App = () => {
const [todos, setTodos] = React.useState(["Bake cookies", "Buy milk", "Buy eggs", "Buy avocado"]);
const [text, setText] = React.useState('');

const handleAddUser = () =>{
setTodos([text, ...todos]);
};
const handleRemove = useCallback((index) => {
todos.splice(index, 1);
setTodos([...todos]);
}, [todos]);
const renderMemoizedList = useMemo(() => {
console.log("Render Memoized List");
return todos.map((item, index) => (
<li key={index} style={{padding: 5}}>
<button type="button" onClick={() => handleRemove(index)}> - </button>
{item}
</li>
))
}, [todos, handleRemove]);
console.log('Render App');

return (
<div className="container">
<div>
<input type="text" value={text} onChange={evt => setText(evt.target.value)} />
<button type="button" onClick={handleAddUser}> + </button>
</div>
<ul>
{renderMemoizedList}
</ul>
</div>
);
};

export default App;

Now check out the result below. Every time we type in the input, the app re-renders and logs to the console. However, the list is re-rendered only when we add or remove an item from the list. Notice that handleRemove is wrapped inside useCallback. Can you figure why?

To recapitulate, useCallback is used to memoize functions, useMemo is used to memoize objects.

useImperativeHandle

useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef.

This does exactly what it says — customizes the instance value that is exposed to parent component when using ref. In 99% of use cases, we wouldn’t need this because there are always better ways of getting things done. We will look into an example of useImperativeHandle and then also see how it could be avoided. Imagine we’re designing a Drawer that shows on button click. We will use useImperativeHandle with React.forwardRef to expose some properties of the drawer to the parent component. A little basic css animation is used to achieve the transformation.

import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react';
import './App.css';
import books from './books';const Drawer = forwardRef((props, ref) => {
const [styleClass, setStyleClass] = useState("drawer");
useImperativeHandle(ref, () => ({
show: () => setStyleClass("drawer show"),
hide: () => setStyleClass("drawer hide"),
}));
return (
<div className={styleClass} onClick={props.onClick}>
{props.children}
</div>
);
});

const App = () => {
const ref = useRef(null);
const hideDrawer = () => ref.current.hide();

return (
<div className="container">
<Drawer ref={ref} onClick={hideDrawer}>
{
books.map((book, index) => <div key={index} className="content"> {book} </div> )
}
</Drawer>
<button onClick={() => ref.current.show()}> Show drawer </button>
</div>
);
};

export default App;

Neat animation, isn’t it? With this example, we can also see how animation could be triggered using reference. But, the important thing to notice here of course is how useImperativeHandle and forwardRef is used here. Using this hook we have exposed show and hide from the component Drawer to its parent App. But as stated before, there are nicer ways of getting this done and that is by using useEffect. Below is the code using useEffect and guess what? We don’t need to use useRef, forwardRef or useImperativeHandle. Sweet, right?

import React, { useState, useEffect } from 'react';
import './App.css';
import books from './books';const Drawer = (props) => {
const [styleClass, setStyleClass] = useState("drawer");
const [isInitialLoad, setIsInitialLoad] = useState(true);
useEffect(() => {
if(isInitialLoad){
setIsInitialLoad(false);
} else if(props.isOpen){
setStyleClass("drawer show");
} else {
setStyleClass("drawer hide");
}
}, [isInitialLoad, props.isOpen]);
return (
<div className={styleClass} onClick={props.onClick}>
{props.children}
</div>
);
};

const App = () => {
const [isOpen, setIsOpen] = useState(false);
const hideDrawer = () => setIsOpen(false);

return (
<div className="container">
<Drawer isOpen={isOpen} onClick={hideDrawer}>
{
books.map((book, index) => <div key={index} className="content"> {book} </div> )
}
</Drawer>
<button onClick={() => setIsOpen(true)}> Show drawer </button>
</div>
);
};

export default App;

This code produces the exact same result as before. We have to use a state isInitialLoad to prevent the animation from running for the first time since useEffect always runs when react renders for the first time. Remember, in 99% of our use cases, we can achieve using useEffect what we intend to achieve using useImperativeHandle. However, in some very rare cases we may need to use it.

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.

The last hooks we will look at is useLayoutEffect. The important line to notice above is:

The signature is identical to useEffect, but it fires synchronously after all DOM mutations.

useEffect(() => {
// do something
}, [dependencies])
useLayoutEffect(() => {
// do something
}, [dependencies])

However, the difference between the two is that useEffect runs asynchronously whereas useLayoutEffect runs synchronously right after the DOM mutations are computed. In other words, useEffect is deferred until after the browser is painted whereas useLayoutEffect does not wait for browser window to finish repainting before executing. This is so that the user do not perceive any visual inconsistency in the layout due to the DOM mutations. However before any new render occurs useEffect is guaranteed to execute. Check out the code and the rendering below.

import React, { useEffect } from 'react';
import './App.css';
const App = () => {
const style = {
width: "40px",
height: "40px",
backgroundColor: "rgb(57, 245, 235)",
borderRadius: "50%"
}
useEffect(() => {
const circle = document.getElementById("circle");
circle.style.transform = "translate(157%, 127%)";
circle.style.left = "259%"
circle.style.top = "377%"
});
return (
<div id="container">
<div id="circle" style={ style } />
</div>
);
}
export default App;

See the circle flicker? This occurs because since useEffect executes after App has been rendered, the new attributes to the circle’s style is added after it’s been painted on the DOM. Therefore, when the circle translates to its new location, we perceive a slight flicker. Change useEffect to useLayoutEffect and we won’t notice this flicker anymore because by the time the browser window is repainted the new position of the circle would already have been calculated.

useDebugValue

useDebugValue can be used to display a label for custom hooks in React DevTools.

That’s it. Seriously. It just displays a label in React DevTools. Now if you look at the react hooks document, you will notice a sub section Defer formatting debug values. It explains that useDebugValue takes a second parameter — a formatting function for when formatting the value you want to display is an expensive function. Okay… Uh? What? Why would I have an expensive formatting operation? That’s just wrong!

Anyhoo! IMO, just use console log. I can safely say console log is the most used method, albeit may not be the favorite one, for debugging by more than 90% developers.

WYODH

Yes. If you want to make your code modular, easily readable and reusable then write your own damn hooks. Hooks are incredible. With that said, you should be able to write your own hooks and here’s an example.

import React, { useState } from 'react';const useNetworkStatus = () => {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
const [type, setType] = useState(connection.effectiveType)
connection.addEventListener('change', () => setType(connection.effectiveType)); return [navigator.onLine, navigator.onLine ? type : "undefined"];
}
const App = () => {
const [isOnline, networkType] = useNetworkStatus();
return (
<div style={{margin: "20px 0px", width: '100%', textAlign: 'center'}}>
<div>Network is {isOnline ? "Online" : "Offline"}</div
<div>Network type is {networkType}</div>
</div>
);
}

In the example above, we have created a hook to check if the connection is online or not and what the type of the connection is. For this I used the Network Information API which you can find here. When you run the code, open the developers tool on chrome, go to the Network tab and change the network throttling profile (available options are Online (default), Fast 3G, Slow 3G and Offline). You’ll see the text on the page changes accordingly.

NOTE: You can test the code above on any modern browsers except IE, Safari and Safari for iOS. But I assume most of you are using Chrome, so this should work fantastic.

So there you have it. Advanced react hooks demystified. In the end, I would throw in a caveat though— using these hooks adds an overhead to the code. First understand where optimize needs to be done and then use these hooks.

Enjoy using hooks and writing your own damn hooks and break a leg!

--

--