Custom React Hooks: Why Do We Need a Context

Post Editor

Sometimes a hook is just a hook, but often you'll need more context. This short blog post sums it up.

3 min read
2 comments
post

Custom React Hooks: Why Do We Need a Context

Sometimes a hook is just a hook, but often you'll need more context. This short blog post sums it up.

post
post
3 min read
2 comments
2 comments

TL;DR

Custom React Hooks is a very convenient way to encapsulate logic and pass the data down the rendering tree.
The rules for custom React Hooks are quite simple:

A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.

Pure Hooks
Link to this section

Consider this very naive implementation of a custom hook that encapsulates permissions mapping for some shared document service:

<>Copy
import React from 'react'; import { Permissions, usePermissions } from '@hooks/permissions'; const useIsPermitted = () => { // usePermissions is another custom hook that returns an array of permissions const permissions = usePermissions(); return { isEditPermitted: permissions.includes(Permissions.EDIT_SITE_PERMISSION), } }

As you can see this hook is pretty simple - it makes use of another custom hook that returns an array of permissions and maps it into a simple permitted/non-permitted dictionary. This will allow for better code reuse across the application and help us avoid code duplication in every place where we need this check:

<>Copy
import React from 'react'; export const EditButton = () => { const { isEditPermitted } = useIsPermitted(); return <button disabled={!isEditPermitted}>Edit</button> }

The only problem is that this logic will run on every re-render. In case of a small array it's negligible but if it's a large array then we're in trouble. A simple addition to the custom hook can solve this issue:

<>Copy
import React, { useMemo } from 'react'; import { Permissions, usePermissions } from '@hooks/permissions'; const useIsPermitted = () => { // usePermissions is another custom hook that returns an array of permissions const permissions = usePermissions(); return useMemo(() => ({ isEditPermitted: permissions.includes(Permissions.EDIT_SITE_PERMISSION), }), [permissions]); }

This way the permissions will be rematched only when the permissions array changes.  

Case solved! Or is it?

Official React documentation can give us a hint what might be wrong with this approach:

Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
How does a custom Hook get isolated state? Each call to a Hook gets isolated state. Because we call useFriendStatus directly, from React’s point of view our component just calls useState and useEffect. And as we learned earlier, we can call useState and useEffect many times in one component, and they will be completely independent.
Effectively this means that if we call useIsPermitted from two different components (or even twice from the same component), the logic will be executed for every instance of the useIsPermitted invocation, even thought we use useMemo inside.

Custom Hooks with Context
Link to this section

A solution for this would be combining a custom hook with a context.  

Let's revise our useIsPermitted hook implementation:

<>Copy
import React, { createContext, useMemo } from 'react'; import { Permissions, usePermissions } from '@hooks/permissions'; export const PermissionsContext = createContext({}); export const IsPermittedProvider: React.FC = ({ children }) => { const permissions = usePermissions(); const permissionsDictionary = useMemo(() => ({ isEditPermitted: permissions.includes(Permissions.EDIT_SITE_PERMISSION), }), [permissions]); return ( <PermissionsContext.Provider value={permissionsDictionary} > {children} </PermissionsContext.Provider> ); };
<>Copy
import React, { useContext } from 'react'; import { PermissionsContext } from '@contexts/permissions'; export const useIsPermitted = () => useContext(PermissionsContext);

Now the logic is scoped to a specific context provider. That means that if we use this provider only once at the root of the application, the logic will be executed only once:

<>Copy
import React from 'react'; import { App } from './app'; <PermissionsProvider> <App /> </PermissionsProvider>

Of course we can decide to put the provider lower in the rendering tree so that the logic will be executed only when a relevant part is rendered, but bottom line is - we have more control over the granularity now.

When to Use What
Link to this section

While a hook with context seems to be a more robust solution in terms of performance and memory consumption, it doesn't mean that you should always go with this approach.
There is place for both approaches, but it's important to understand the implications of each one of them.  

Here is a short checklist that will help you decide on the right approach:

Use Pure Hook when:

  • Custom hook state must be isolated (different per instance) OR
  • No heavy calculations are performed in the custom hook OR
  • You only use the hook once in the application (as a way to pass data down the rendering tree)

Use Hook with Context when:

  • The whole subtree must share the hook's state OR
  • There is a heavy calculation performed inside the hook and you want it to run as seldom as possible

Follow me if you liked the article, comment here or DM on Twitter if you have any questions.

Comments (2)

authormalthoff
19 August 2021

The code below "Let's revise our useIsPermitted hook implementation:" is missing. Thanks for your article.

authorjust-jeb
19 August 2021

Thanks for letting know, should be OK now.

authoror4
21 August 2021

thanks for article, you can use constate for create hook with context and this lib has memo on selectors

authorjust-jeb
22 August 2021

Thanks for mentioning that, nice lib! Could you elaborate on what's the advantage of using this lib VS plain state and context? As I see it the only advantage is syntactic sugar and as for disadvantages there are two:

  • Tight coupling of state to the hosting component (without this lib you can encapsulate the state inside the provider)
  • Another 0.5k to the bundle size (negligible but feels unnecessary)

Share

About the author

author_image

Fullstack engineer and guild master @ Wix.com, author of @angular-builders and jest-marbles, drummer and kitesurfer.

author_image

About the author

JeB

Fullstack engineer and guild master @ Wix.com, author of @angular-builders and jest-marbles, drummer and kitesurfer.

About the author

author_image

Fullstack engineer and guild master @ Wix.com, author of @angular-builders and jest-marbles, drummer and kitesurfer.

Looking for a JS job?
Job logo
Sr. React Developer

FMK Corporation

United States
Remote
$100k - $140k
Job logo
Full-Stack Developer (React-Rails)

Brightside Health

United States
Remote
$150k - $175k
Job logo
React Developer (Front-end)

Busy Rebel Development

Ukraine
Remote
$42k - $72k
Job logo
Middle React Developer

E-Konzul

Czech Republic
Remote
$64.8k - $75.6k
More jobs
NxReactCli
NxReactCli
NxReactCli

Featured articles