React Context: Managing Single and Multi-Instance Contexts Using Zustand

Photo by mana5280 on Unsplash

React Context: Managing Single and Multi-Instance Contexts Using Zustand

ยท

4 min read

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?

  1. 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.

  2. 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.

  3. 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.

  1. typeof: This operator returns the type of a variable, function, or expression. In the context of our Zustand store, using typeof would return the type of the function useChatStore, not the return value of the function itself.

  2. ReturnType: This utility type extracts the return type of a function. If you apply ReturnType to useChatStore, 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

  1. Shared Context Instance with typeof: If you want a shared store across all components, use typeof. This ensures that the state remains consistent across your entire application, and changes in one part of the app reflect everywhere.

  2. Separate Context Instances with ReturnType: For more isolated and independent state management, use ReturnType. 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! ๐Ÿš€

ย