preferred way of using a react context
By Per Fröjd
- react
- snippets
- typescript
Using context
There’s an infinite amount of other posts explaining the usefulness of context, often in the context (hah!) of building a clone of redux or maintaining a global state for your application. This is great and all, but to me, global state should be concise, small and contextual (hah, did it again!).
So the basics with contexts is that you need to create a Provider
, which essentially is a component higher up in your component hierarchy, responsible for feeding data, functions or whatever you want to provide via your context. So lets start with that.
import { createContext, useContext } from 'react'
const secretInitialState = {
foo: 'bar',
}
const SecretContext = createContext()
export function SecretProvider({ children }) {
return (
<SecretContext.Provider value={secretInitialState}>
{children}
</SecretContext.Provider>
)
}
export function useSecretContext() {
const context = useContext(SecretContext)
return context
}
In this example, we’re just creating a simple global read-only state for the context to consume. This data could be simple static data hardcoded in the application, but that’s not always that useful, so lets show some other variants.
import { createContext, useState, useMemo, useContext } from 'react'
const secretInitialState = {
foo: 'bar',
}
const SecretContext = createContext()
const useSecretValue = (initialState) => {
const [state, setState] = useState(initialState)
return [state, setState]
}
export function SecretProvider({ children }) {
const [secrets, setSecrets] = useSecretValue(secretInitialState)
const memoizedValue = useMemo(
() => ({
secrets,
setSecrets,
}),
[secrets, setSecrets]
)
return (
<SecretContext.Provider value={memoizedValue}>
{children}
</SecretContext.Provider>
)
}
export function useSecretContext() {
const context = useContext(SecretContext)
return context
}
At this point you can pretty much put anything inside that provider, including custom hooks, real hooks, api calls via useEffect etc. One key is making sure you memoize the values appropriately (should know this by now) so that the children of the provider (likely your entire application) won’t re-render unnecessarily.
If you expose methods, it might also be wise to wrap these in useCallback
before being wrapped in useMemo
within the provider.
So the missing piece from here on, is how you consume these.
function App() {
const [secrets, setSecrets] = useSecretContext()
return <div>{JSON.stringify(secrets, null, 2)}</div>
}
This example here doesn’t show what we do with setSecrets, but since it’s just an example.
There’s some quality of life things we can add here to provide a better experience, like making sure there’s a provider higher up in the component tree.
export function useSecretContext() {
const context = useContext(SecretContext)
if (!context) {
throw new Error(
"You're missing the SecretContextProvider higher up in the component tree"
)
}
return context
}
When using typescript, I often come across the annoyance of dealing with context default values, here’s an “easy” fix for that particular issue.
import { createContext } from 'react'
type SecretCtx = {
setSecret: () => void
secrets: {
[key: string]: string
}
}
const SecretContext = createContext<SecretCtx | undefined>(undefined)
Since we likely already have types set up, we just need to provide a default value as undefined, and type it up.
In the end, a lof of this is premature optimization, but in heavier application it can definitely hurt the user experience with too many re-renders, especially if there are async calls involved in the flow.
I have to admit I’m getting a bit too used to memoizing, and that sometimes hurts, but I guess there’s a lesson in that somewhere. In short, here are few points I like to think about when implementing contexts.
- Decide if the context is read-only, has a lot of writes, or if it’s based on async values (such as state fetched from an API)
- Depending on the amount of consumers (how many different components that consume the context), figure out if re-renders are a problem.
- If there’s a lot of writes, and it’s spread wide, using memoization is definitely good.