TLDR:
If you want a single instance of a context so that you can use hooks from a component anywhere in the tree, and don't want the advanced functionality of context, then use Zustand's create
instead of createStore
. (See the article Zustand - create vs createStore for a breakdown of the two.)
If you want to create multiple context instances for isolated state management within different components (e.g. multiple chatboxes and each chatbox has its own context), then use Zustand's createStore
. When you declare the context, you must use ReturnType
like so:
const ChatContext = createContext<ReturnType<typeof useChatStore> | null>(null);
instead of
const ChatContext = createContext<typeof useChatStore | null>(null);
If you do not use ReturnType
in the declaration, you will have a single instance of context (which may be what you want, but the same thing can more easily be achieved with create
rather than createStore
, unless you also need the added functionality of createStore
.)
Introduction
In this guide, we'll explore how to create contexts that either allow all components to use the same instance or have separate instances for each component. We will particularly consider how they relate to using the Zustand library for state management. By understanding typeof
and ReturnType
in TypeScript, you'll be able to understand how to make context behave differently based on your requirements.
We will begin by briefly introducing context in React. You can skip this section if you're already familiar (a recap never hurts though!).
Understanding Context in React
What is Context?
Context provides a way to pass data through the component tree without having to pass props down manually at every level. It's like a shared container where components can read and write data, regardless of their position in the hierarchy.
Why Use Context?
Avoid Prop-Drilling: Passing props from one component to another can become cumbersome and lead to messy code. Context helps you avoid this by providing a centralized place for shared data.
Dynamic Data Sharing: Components can both read and write to the context, making it flexible for various use cases like theme switching, localization, or user authentication.
Integration with Libraries: Context is not only a native React feature but also integrates seamlessly with state management libraries like Zustand. This integration allows for more scalable and maintainable code.
Single context instance vs multiple context instances
The creation of context in React can be approached in different ways. How you define the type of your context can lead to different behaviors, and this is particularly relevant when using libraries like Zustand for state management.
Understanding typeof
and ReturnType
In TypeScript, two utility types are typeof
and ReturnType
.
typeof
: This operator returns the type of a variable, function, or expression. In the context of our Zustand store, usingtypeof
would return the type of the functionuseChatStore
, not the return value of the function itself.ReturnType
: This utility type extracts the return type of a function. If you applyReturnType
touseChatStore
, it would give you the actual return type of the function, leading to different behavior in the context.
Let us look at some examples to better illustrate the difference. First, let's define a simplified Zustand store for our chat application:
const useChatStore = () =>
createStore<ChatStoreSettings>((set) => ({
currentMessage: '',
setCurrentMessage: (message: string) => set(() => ({ currentMessage: message })),
}));
Here, our store only contains a currentMessage
and a method to update that message.
Understanding typeof
The typeof
operator in TypeScript retrieves the type of a variable, function, or expression. Here's an example of how you can use typeof
:
function getUser(id: number) {
return { name: 'Alice', age: 25 };
}
type GetUserFunction = typeof getUser;
// Equivalent to: type GetUserFunction = (id: number) => { name: string; age: number }
In the context of our Zustand store, if you create context using typeof
like this:
const ChatContext = createContext<typeof useChatStore | null>(null);
It will create a context with the type of the useChatStore
function, which means all components will share the same context instance.
Understanding ReturnType
The ReturnType
utility type extracts the return type of a function. Here's how you can use it:
type User = ReturnType<typeof getUser>;
// Equivalent to: type User = { name: string; age: number }
In our Zustand store example, if you create context using ReturnType
:
const ChatContext = createContext<ReturnType<typeof useChatStore> | null>(null);
In this case, each component has its own context instance. This gives you the flexibility to have separate state instances for different parts of your application.
Summary
Shared Context Instance with
typeof
: If you want a shared store across all components, usetypeof
. This ensures that the state remains consistent across your entire application, and changes in one part of the app reflect everywhere.Separate Context Instances with
ReturnType
: For more isolated and independent state management, useReturnType
. This allows different parts of your app to have their own state, unaffected by changes in other areas.
I hope you've found this guide helpful - feel free to follow me on Twitter! ๐