Context and Providers
A provider exposes a value to its descendant components, without the need to pass props:
<FooContext.Provider value={3}>
<X>
<X1 />
</X>
<Y />
<Z />
</FooContext.Provider>
Descendants read the value by referring to the context (through the use of useContext):
// X1.tsx
const value = useContext(FooContext) // 3
We define the context object beforehand. Note: the initial value is overridden by the provider.
const FooContext = createContext<number>(0)
optional pattern: helper hook
We make a helper function that reads the appropriate context. The consumer doesn't have to import the Context object anymore:
// FooContext.tsx
export function useFoo() {
return useContext(FooContext)
}
// X1.tsx
const value = useFoo()
optional pattern: custom provider
We make a custom component that initializes the .Provider component with the correct value, encapsulating away the management of the value prop. The component also hosts the state if applicable. It can then expose the state and the dispatch function, if applicable.
In this example, we expose the state and the dispatch function. We start the context with undefined. In the custom hook, we check against it.
Note: Instead of inlining value={{ foo, setFoo }} in the value prop, which is an object that is created on every renders, and thus signaling the value has changed, we instead memoize an object and use it instead. This is only useful when the value is an object that is created inside the component. It isn't needed when we use a stable reference to the state, such as when doing value={ foo }. It is also theoretically not needed when the provider itself doesn't re-render, which occurs when it's stable at the root of the tree. Having parent providers that change doesn't mean that the nested child provider re-render. We can still use it for extra-safety.
// FooContext.tsx
type T = {
foo: number
setFoo: Dispatch<SetStateAction<number>>
}
// 1. context
const FooContext = createContext<T | undefined>(undefined)
// 2. custom provider
function FooProvider({ children }: { children: ReactNode }) {
const [foo, setFoo] = useState<number>(0)
const value = useMemo(() => ({ foo, setFoo }), [foo])
return <FooContext.Provider value={value}>{children}</FooContext.Provider>
}
// 3. custom reader (helper hook)
export function useFoo() {
const value = useContext(FooContext)
if (!value) throw new Error("useFoo must be used within the Provider")
return value
}
We can then use the component instead of the .Provider. The consumer doesn't have to import the Context object anymore:
// App.tsx
<FooProvider>
<X />
<Y />
</FooProvider>
// X.tsx
const { foo, setFoo } = useFoo()