Understanding react context api with typescript

Hello, can someone tell me what’s the difference between these two?

export const DataContext = createContext({} as DataContextInterface);
export const DataContext = createContext<DataContextInterface | null>(null);

Here’s their type interface

interface DataContextInterface {
  userData: UserDataInterface | null | undefined;
  setUserData: React.Dispatch<React.SetStateAction<UserDataInterface | null>>;
}

From what I understand, both say that createContext accepts an object with the same structure of DataContextInterface.

But the second one throws this error, when I try to use the context

Thanks.

With the explicit generic form, you’re telling TS to treat the arg for createContext as a value that can always be either DataContextInterface or null (polymorphic), not something that is only one of those (monomorphic). (Edit: actually I’m not positive that’s what’s going on here. I’m more familiar with Haskell’s type system than TS’s, so I may have assumed more than I should).

You shouldn’t even need the as DataContextInterface, as TS should be able to infer it from the argument alone. If it can’t, then your empty object is wrong, and it will crash later on down the road. React has excellent TS support, so you should hardly ever need to reach for an explicit cast.

Hello there,

I would have said this is plain incorrect:

By that, I mean a best-practice is to use as as a way to tell TS the type of something it cannot infer. Currently, the line above is saying {} is the same as DataContextInterface, which does not include {}.

Other than that, @chuckadams covered the reason for the error.

@chuckadams

This is my understanding too. So, I would assume this should be correct. But, typescript keeps throwing an error that the object I am trying to destructure doesn’t exist on type ‘DataContextInterface | null’

export const DataContext = createContext<DataContextInterface | null>(null);

Here’s what I’m trying to do quizzical-curran-sv6eq - CodeSandbox.

The solution I found was to explicitly tell that createContext would accept an argument of object as DataContextInterface. Which is the first one.

Edit: The error shows up in App.tsx

Hello @Sky020 ,

Isn’t it the same with this?

export const DataContext = createContext<DataContextInterface | null>(null);

I believe this one tells that createContext would accept an object the same as DataContextInterface or null

I recreated this problem in codesandbox quizzical-curran-sv6eq - CodeSandbox

The error stops if I use this one:

export const DataContext = createContext({} as DataContextInterface);

Edit: The error shows up in App.tsx

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:

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

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

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

3 Likes

Hello there. I’m happy to see this question. I have to learn React context as well. Could somebody give me some good resources to do that? Thanks.

The React docs are fine: it’s not a complicated thing, it’s a tiny API (you create a context, which gives you a component which you can store some arbitrary values in). If you’ve got specific questions, do open a new thread – this thread is about a very specific issue relating to how Typescript works when running the function that creates a context.

1 Like

Thank you so much @DanCouper. I understand now.

1 Like

This exact issue tripped me up for ages. I’d strongly suggest this article, which gives a nice breakdown. There is a subtlety which I don’t think he covers re handling null in Typescript (I may have just misread it though): I can’t get the inference to work without an explicit check in the hook for null – he simply misses out the argument in createContext, but because it requires an argument, the typechecking fails. But as an overview of best practices, it’s really good:

1 Like

@resetnakAlex As said by Dan , React Docs are okay. But if you want to see examples that use hooks, you may look at these:

1 Like