The one using the union type of Type | null
on the generic is what you probably should be using, because it’ll give you full type inference.
So say you want to store an object with an interface like:
interface ExampleState {
foo: number;
bar: string;
}
So the createContext
function takes one and only one argument. The issue is that you don’t really care about it at the point the function is called, but that the typechecking and inference depend on it being defined correctly.
There are three options:
- You provide the actual interface it expects with some default values:
const ExampleContext = React.createContext<ExampleState>({ foo: 1, bar: "hi" });
This is perfectly fine but generally impractical because most often you don’t want or need those defaults. It means you’re having to write boilerplate, and although that’s fine if you only have one or two primitive values it’s a massive PITA once you have functions or other objects. It gets even more painful when you can’t define the exact interface up front (ie the value of the provider has some setup logic that actually builds it, possibly async).
-
{} as Interface
, so you’re effectively throwing away typechecking.
const ExampleContext = React.createContext({} as ExampleState);
It works because you’re forcing it through, it’s almost the same as using any
. But now you have the issue that the starting value isn’t actually known, you are creating the context with something that’s pretending to be the correct interface. So everything that depends upon the context is now based on you saying it’s fine, which kinda defeats the point of using TS in the first place. You’re likely to start getting inference issues further down the chain.
- Specify the generic type as
Type | null
. This is preferable by far as you get full typechecking and inference but there’s another step you need. So
const ExampleContext = React.createContext<null | ExampleState>(null);
Then you can do:
export const ExampleProvider = ({ children }: { children: React.ReactNode): JSX.Element => (
<ExampleContext.Provider value={{ foo: 1, bar: "hi" }}>
{ children }
</ExampleContext.Provider>
);
Now the issue is everything will break re type inference because every time you use the value in the context, the TS compiler will say "but this could also be null, and I’m not going to just let you try to access null.foo
or null.bar
".
This is the error you are seeing: you are trying to destructure something that could be null
. You need to put a check in to make sure the value is not null. You can fix this by explicitly checking for null every time you try to access the context value, for example:
const { foo, bar } = React.useContext(ExampleContext) ?? { foo: 1, bar: "default value" };
But this is just boilerplate (you don’t care what the default is, you’re just pleasing the compiler), and prone to error (you’ll miss some or get it wrong at some point).
So, as well as the provider, you should also always define a hook (I’ve made the error handling a bit verbose you can hopefully see what the causes of errors would be):
function useExample(): ExampleState {
const state = React.useContext(Example context);
if (state === null) {
throw new Error("Example state has not been configured, value is null");
} else if (state === undefined) {
throw new Error("You're attempting to access example state outside of the Example context provider");
}
return state;
}
Now you only access the context through the hook, and you always get type inference: you set the context up with null
(avoiding the need for boilerplate or tricking the compiler), but when you access the context the value is always what you expect.