Skip to main content

Creating React Context types with generics

11 Mar, 2021

There are several great articles describing how to work with React Context using TypeScript, including one from the folks at React Training: https://reacttraining.com/blog/react-context-with-typescript/

In this quick post, I want to describe how I've used TypeScript generics in combination with React Context to build more flexible but strongly typed shared state. So prior knowledge of React Context and TypeScript is assumed, otherwise select one of the previous links before continuing.

For a refresher on generics:

While using any is certainly generic in that it will cause the function to accept any and all types for the type of arg, we actually are losing the information about what that type was when the function returns. If we passed in a number, the only information we have is that any type could be returned.

Instead, we need a way of capturing the type of the argument in such a way that we can also use it to denote what is being returned. Here, we will use a type variable, a special kind of variable that works on types rather than values.

function identity<Type>(arg: Type): Type {
return arg;
}

https://www.typescriptlang.org/docs/handbook/2/generics.html#hello-world-of-generics

For the feature in question, there is a list of items that can be selected in a table with the selected items stored as a list of IDs. We want to make sure this Context data stores objects that must include an id property. We need to tell TypeScript that the generic type argument has constraints (required fields in this case): https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints

type ItemWithID = { id: string };
type MyContextData<Item extends ItemWithID> = {
selectedItems: string[];
itemList: Item[];
};

This gets a little tricky when creating the Context object since it is declared as a static variable:

const MyContext = React.createContext<MyContextData<ItemWithID>>(null);

The generic type argument for MyContextData will use the constrained ItemWithId type as a placeholder. Then we can create our Provider component to accept an Item generic:

type MyProviderProps<Item extends ItemWithId> = {
itemList: Item[];
}

function MyProvider <Item extends ItemWithId>(
{ children, itemList }: React.PropsWithChildren<MyProviderProps<Item>>
) {
// keep track of `selectedItems` somehow

return <MyContext.Provider value={{ itemList, selectedItems }}>{children}</MyContext.Provider>;
}

To get the correct generic usage for the hook that consumes that Context object, some overrides need to happen:

function useMyContext<Item extends ItemWithID>() {
const context = React.useContext<MyContextData<Item>>(
(MyContext as unknown) as React.Context<MyContextData<Item>>
);
if (!context) {
throw new Error('useMyContext must be used under MyContextProvider');
}
return context;
}

The type of MyContext is asserted to match the expected type with Item generic. Otherwise, TypeScript will raise an error like:

Type 'MyContextData<ItemWithID>' is not assignable to type 'MyContextData<Item>'

Finally, we can use our Provider component and hook with our feature:

// import MyProvider and useMyContext

type Account {
id: string;
name: string;
age: number;
}

const App = () => {
const itemList = // request or create this data

return (
<MyProvider itemList={itemList}>
<AccountTable />
</MyProvider>
);
};

const AccountTable = () => {
const { itemList, selectedItems } = useMyContext<Account>();

// use the itemList and selectedItems data, which should match `Account[]` and `string[]` types respectively
};

That's it! Once you get the hang of the pattern, it opens up a world of flexible, reusable state management in your React + TypeScript application.