Global State

James Drury
James Drury / Aug 13, 2024
4 min read ・ 196 views

Global State

In React, an application is made up of separate components all connected in what is called a `component tree`. This consists of a parent component at the top level, and its child components underneath:

<Parent> <Children /> <Children /> <Children /> </Parent>

Data is usually shared across these components, by storing it in the parent and passing it down to its children.

Prop Drilling

One way to achieve this is by `prop drilling`, which is the method of passing data as props to the components which need them:

export const Parent = ({data}) => ( <Children data={data} /> )

This approach becomes cumbersome however, when components that need props are nested several layers within the component tree:

export const Parent = ({data}) => ( <Children data={data} /> ) export const Children = ({data)} => ( <NestedChildren data={data} /> )

Context API

To solve this, React created the `Context API`, which provides a hook that allows you to access data from any child component without the need for props:

<ParentContext data={data}> <Children /> <Children /> <Children /> </ParentContext> export const Children = () => { const { data } = useContext() //... }

The downside to this approach however, is every child in the component tree updates when data inside the parent component changes, causing children that don’t call for data to re-render needlessly, slowing the overall performance of the app.

Third Party Libraries

Over the years, third-party packages have been created as a work-around to this issue, by storing and managing data outside of the component tree. Such libraries include:

useGlobalState

Any of the above packages provide sufficient solutions to sharing data between components. Yet, if our app has only minor requirements, installing one of these packages could be excessive.

In such cases, we can create a `global state hook` that performs similar functionality. At first, it may seem that a plain custom hook would do. Simply perform some logic, update some state using that logic, and call that hook in all the components that need to share that state.

Global State Problem

The problem is only the component interacting with the hook would update. This is because state hooks, like `useState`, only operate within the scope of a single component.

const ComponentOne = () => { //updated const [state, setState] = useHook() //this was clicked return <button onClick={() => setState(true)}></button> } const ComponentTwo = () => { //not updated const [state, setState] = useHook() //.... }

Though the same hook may be called in multiple places, each component’s usage of that hook has its own state instance and cannot be shared outside the scope of that component.

To overcome this issue, we need to write a hook that accomplishes two things:

Updates state within a component

Notifies the other components when that state updates

Let’s see how this works:

import React from "react"; const listeners: Array<React.Dispatch<React.SetStateAction<boolean>>> = []; export default function useGlobalState() { const [state, _setState] = React.useState(false); const setState = (newState: boolean) => listeners.forEach((l) => l(newState)); React.useEffect(() => { listeners.push(_setState); return () => { listeners.filter((l) => l !== _setState); }; }, []); return [state, setState] as const; }

In the code above, we are using `useEffect`, so that when each component that calls this custom hook first mounts, its corresponding `_setState` is added to a global array called listeners. Each _setState in the `listeners` array will be unique based on the component it references.

You can then call the `setState` function from any component by passing `newState`, which will then fire each listening component’s _setState with newState, so that the state of every component is updated.

Refactor useGlobalState

When creating a global state hook, it is important to note that our initial state and listeners array sit outside the React component tree. Thus, when interacting with external data, it is more optimal to use the newer `useSyncExternalStore` than useEffect and useState.

let globalState = false as boolean; type SetState = React.Dispatch<React.SetStateAction<typeof globalState>>; const listeners: Array<SetState> = []; export default function useGlobalState() { const setState = (newState: typeof globalState) => { globalState = newState; for (const listener of listeners) listener(globalState); }; const state = React.useSyncExternalStore( (listener: SetState) => { listeners.push(listener); return () => { listeners.filter((l) => l !== listener); }; }, () => globalState ); return [state, setState] as const; }

Included in the articles below, are explanations of the benefits of useSyncExternalStore.

Resources

Subscribe to my newsletter

Get updates on my new notes