Image Credit: Carolyn Jao

I had a great experience attending a workshop taught by Kent C. Dodds on the topic of Advanced React Hooks. 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 docs, 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 article 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 docs:

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 post 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