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
Senior Full Stack Developer (React/Node)

Cosuno

Germany
Remote
$58k - $93k
Job logo
Full Stack Developer (Django + React)

Self Decode

Worldwide
Remote
$40k - $90k
Default logo
Sr. Java Developer w/React (Full Time) - Remote

Elevate Digital

Worldwide
Remote
$125k - $130k
Job logo
IOS Developer with React Native

iconvergence

Worldwide
Remote
$61k - $146k
More jobs
NxReactCli
NxReactCli
NxReactCli

Featured articles

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
AngularpostTracking user interaction area

13 September 2021

8 min read

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
Angularpost
7 September 202122 min read
Designing Angular architecture - Container-Presentation pattern

Designing architecture could be tricky, especially in the agile world, where requirement changes are frequent. So your design has to support that and provides extendibility without the need for serious modification. In such cases, you will find the Container-Presentation pattern instrumental.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more
micro frontendspostTaking micro-frontends to the next level

6 September 2021

25 min read

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more