An Intro to Advanced React Hooks

David Han
David Han
Jul 8 · 6 min read
Image Credit:

I had a great experience attending a . The workshop allowed me to take a deeper dive into a topic that I’ve been wanting to learn for a while, and also gave me the opportunity to ask questions to an expert. Even when Kent didn’t have time to answer a question in depth, he was always able to provide the right resource for further reading on the subject. I highly recommend this workshop for anyone who is interested in learning more about React Hooks beyond useState and useEffect. This blog post is more of a way for me to write down my takeaways so that I don’t forget them, but I hope others would find it useful as well.

This post will probably not make a lot of sense if you’re not familiar with the basics of React hooks and specifically how useState and useEffect works. You can read more about it or get a refresher from the , before proceeding.

useReducer

I’ve never used a reducer before because I always thought that useState could be used to handle most cases and that it would be confusing to have two different ways to set state — but I learned that useReducer can be a great way to simplify your API and express intention while consolidating more complex state interactions in a reducer. Check out the two different implementations below for the getUsers() function and decide for yourself which is clearer.

useState implementation:

function UsersList() {
const [users, setUsers] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
function getUsers() {
setLoading(true);
setError(null);
setUsers(null);
fetchUsers().then(
users => {
setLoading(false);
setError(null);
setUsers(users);
},
error => {
setLoading(false);
setError(error);
setUsers(null);
}
);
}
}

useReducer implementation:

function usersReducer(state, action) {
switch (action.type) {
case "LOADING": {
return { loading: true, users: null, error: null };
}
case "LOADED": {
return { loading: false, users: action.users, error: null };
}
case "ERROR": {
return { loading: false, users: null, error: action.error };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function UsersList() {
const [state, dispatch] = React.useReducer(usersReducer, {
users: null,
loading: false,
error: null
});
function getUsers() {
dispatch({ type: "LOADING" });
fetchUsers().then(
users => {
dispatch({ type: "LOADED", users });
},
error => {
dispatch({ type: "ERROR", error });
}
);
}
}

As you can imagine, when fetching resources, this type of state interaction happens quite often in our apps and we can extract this to a reusable useAsync custom hook. This was an extra credit question in the workshop which I won’t get into here for the sake of brevity.

Side Note: useReducer is more performant than useState in the example above since we’re replacing multiple useState calls (which can cause multiple re-renders) with one useReducer call. The difference in performance is not significant enough for this to be a concern, but it is worth noting that there is a slight performance benefit to taking the useReducer approach in similar scenarios.

For a more in-depth look into when to utilize useReducer vs useState, you can check out this helpful shared by Kent.

useMemo and useCallback

Both of these hooks are similar in that they are used to memoize. The main difference is that useMemo can be used to memoize any value including functions, while useCallback can only be used to memoize functions.

From the react :

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

Example of useMemo:

const memoizedValue = useMemo(() => someExpensiveComputation(input), [input]);

Example of useCallback:

const memoizedFunction = useCallback(
() => {
fnToBeMemoized(input);
},
[input],
);

In both examples, the second argument of [input] is called the dependencies array, which means that the memoized value will only recompute when the input value changes.

Here is a helpful on when to use each of these hooks. In it, Kent says:

So when should I useMemo and useCallback?

1. Referential equality

2. Computationally expensive calculations

React.memo can also be used to mimic the same behavior as PureComponent by wrapping the component.

const Button = React.memo(function Button({onClick}) {
return <button onClick={onClick}>Button Text</button>
})

However, since React is already very performant when it comes to re-rendering components, we should only use this as a last resort. (The same principle applies to PureComponent.) The bigger problem is when the render function is slow. It is better to fix slow render functions rather than to apply useMemo all over the place which can ultimately make performance worse than it was before.

useContext

useContext is helpful any time you need to pass a prop through many nested children (to solve the problem), or when you need global state. In the example below, even though we didn’t pass any props to the RandomNumber or NumberGenerator components, they can still access the necessary props through RandomNumberContext.

Side note on performance: any time a context value changes, all the consumers of that value will be re-rendered.

You may notice that the value we are sending to the provider is an object that contains the randomNumber and setRandomNumber. As a result, any time the randomNumber value is updated, all consumers of RandomNumberContext will be re-rendered. We can slightly improve the performance of the previous example by creating separate contexts for randomNumber and setRandomNumber as shown in the example below.

Now that randomNumber and setRandomNumber are being passed through separate contexts, when the randomNumber value changes, only the RandomNumber component is re-rendered, as opposed to in the previous example where NumberGenerator was also re-rendered along with RandomNumber.

Again, since React is already performant when it comes to re-renders, you don’t have to bother with this type of solution unless you have many consumers of a context provider and performance becomes an issue.

useRef

useRef is often used to keep a reference of a dom element in order to focus on an input or to perform other similar dom interactions.

Snippet of example usage from React :

function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

useRef also has a more generic use case which is to hold a mutable value that doesn’t cause re-renders when the value changes. Example:

const count = useRef(0);
let currentCount = count.current;

You probably won’t use the following hooks too often, but it would be good to understand the rare use cases and log them in the back of your mind in case you ever need them.

useImperativeHandle

This hook is only necessary when you need to forward a reference from a child to a parent. Some example use cases are when you need to control focusing an input from a parent component, or when you need to control scrolling a child container from the parent component. Even though you won’t need this hook most of the time, it’s nice that React offers an “escape hatch” for rare instances in which you need to pass a child reference to the parent.

If you’re like me and are not sure what imperative means in this context, here is a helpful that was shared during the workshop which explains the difference between imperative and declarative programming.

Example usage can be found in the docs .

useLayoutEffect

A notable change from class components to hooks is that componentDidMount and componenentDidUpdate fires before the UI is painted while useEffect fires after the UI is painted. Why is this the new default? You may have code in useEffect that can take a while, like an expensive call to the server, or a call to a third party service that shouldn’t block the UI from displaying content to the user.

If you want the previous behavior (of firing useEffect before the UI is painted) that's where useLayoutEffect comes in. The only time you need this is when you have code within useEffect that has an observable visual effect — for example, when you are scrolling to the bottom of a window or when you are resizing an element. If you find that the UI is jumping around based on code in useEffect, try replacing useEffect with useLayoutEffect.


Looking for a place to practice your newfound knowledge of hooks? We’re !

Feel free to let me know if you have any questions or comments below!

In the Weeds

A blog by the Greenhouse Engineering team

Thanks to Mark McDonald.

David Han

Written by

David Han

Software Engineer @ Greenhouse Software

In the Weeds

A blog by the Greenhouse Engineering team