26

I am trying to set up a React store using useReducer and useContext hooks. The React.createContext(defaultValue) is creating issues with my TS checker. I have tried a few different things, but essentially, I createContext(null) then in the component useReducer() to set state and dispatch, but when I call the Provider and pass the value as {state, dispatch}, it doesn't tells me "Type '{ state: any; dispatch: React.Dispatch; }' is not assignable to type 'null'.

I don't understand this because by the time it errors, I have assigned state and dispatch and the value should no longer be null.

Here is the Context Provider wrapper that I am trying to create.

import React, { createContext, useReducer, FC, Dispatch } from 'react';
import storeReducer, { initialState } from './reducer';
import { Action, State } from './types';
import { isNull } from 'util';

const StoreContext = createContext(null);

const StoreProvider:FC = ({ children }) => {
  const [state, dispatch] = useReducer(storeReducer, initialState);
  const value = {state, dispatch}
  return (
    <StoreContext.Provider value={value}>
      {children}
    </StoreContext.Provider>
  );
};

export { StoreProvider, StoreContext };

export interface IStoreContext {
  dispatch: (action: Action<any>) => {};
  state: State;
}

if I leave it as simply const StoreContext = createContext();, then it complains that defaultValue is not being defined.

The crazy thing is I have lifted this from an old project and had no issues compiling.

5 Answers 5

37

This may be a case where it is okay to temporarily cast it in order to save yourself a hassle:

interface Store {
  state: MyStateType,
  dispatch: Dispatch,
}

const StoreContext = createContext<Store>({} as Store);

const StoreProvider = ({children}) => {
  const {state, dispatch} = theseAreAlwaysPresent()

  return (
    <StoreContext.Provider value={{state, dispatch}}>
      {children}
    </StoreContext.Provider>
  )
}
... 

Sign up to request clarification or add additional context in comments.

Comments

12

Since I just made an account to answer this, I can't comment above. Alex Wayne is correct, that is acceptable to Typescript. However, the useContext hook won't really work for this because as far as Typescript is concerned, you may have a null value in that Context.

From original answer... here is our Context in Typescript const StoreContext = createContext<{state: MyStateType, dispatch: Dispatch} | null>(null);

So, we need to create a new hook, let's call it useContextAndErrorIfNull. This would look like:

const useContextAndErrorIfNull = <ItemType>(context: Context<ItemType | null>): ItemType => {
  const contextValue = useContext(context);
  if (contextValue === null) {
    throw Error("Context has not been Provided!");
  }
  return contextValue;
}

Use this hook instead of useContext and it should all work well.

1 Comment

Perfect, this works great when you know you'll never use your context when the provider hasn't been initialised
10

When you initialize a context with null, the intended type can't be inferred. You have to give the type of the context explicitly in this case.

Here that would look something like:

const StoreContext = createContext<{
  state: MyStateType,
  dispatch: Dispact,
} | null>(null);

2 Comments

I have tried that. When I declare the type I get it working but then in the consumer (where I call useContext), I get the following error: Argument of type 'Context<MyContextType | null>' is not assignable to parameter of type 'Context<MyContextType>'. I created an interface for "MyContextType" that looks like this: export interface MyContextType { state: State; dispatch: Dispatch<any>; }. And here is the context callback in the consumer: const ctx = useContext<MyContextType>(StoreContext)
It doesn't like a null as a value when I initialize, but it doesn't let me declare <TypeName | null> either.
0
 //So basically in my case you just have to assign an empty object that 
   will solve your problem.


 import React, { createContext, useReducer, FC, Dispatch } from 'react';
 import storeReducer, { initialState } from './reducer';
 import { Action, State } from './types';
 import { isNull } from 'util';

 // replaced null with {}
 // because assigning a value and any type of parameter will not be 
    acceptable in typescript
 const StoreContext = createContext({});

const StoreProvider:FC = ({ children }) => {
const [state, dispatch] = useReducer(storeReducer, initialState);
const value = {state, dispatch}
return (
<StoreContext.Provider value={value}>
  {children}
</StoreContext.Provider>
 )};

export { StoreProvider, StoreContext };

export interface IStoreContext {
dispatch: (action: Action<any>) => {};
state: State;
}

Comments

0

Short answer:

const MyContext = createContext<MyContextValueType | null>(null);

See: Context without default value.

Long answer:

(1.) createContext is defined as:

function createContext<T>( defaultValue: T ): Context<T>

So createContext returns Context<T>, where T is inferred from the given defaultValue.
If defaultValue is null, then the return type of createContext is Context<null>:

const MyContext: Context<null> = createContext(null);

(2.) Context is defined as:

interface Context<T> { Provider: Provider<T>; Consumer: Consumer<T>; }

So, given that Context<null>, the provider and consumer will be
Provider<null> and Consumer<null>.

Therefore, you can not pass any value other than null to MyContext.Provider.

(MyContext.Provider is Provider<null> and expects only null and nothing else)

const MyContext: Context<null> = createContext(null);
// ...

<MyContext.Provider value={ myActualStateValue }>  {* <-- ERROR: Provider expects only `null` *}

General solution:

Define the type of the defaultValue explicitly to accept both null and the actual state,
with createContext<MyContextValueType | null>.

  • This will return Context<MyContextValueType | null>,
  • therefore the provider is Provider<MyContextValueType | null>,
  • therefore MyContext.Provider accepts both, MyContextValueType and null.

Additionally, the consumer should always handle the null value:

const MyConsumingComponent: React.FC = function(){
    const myStateValue: MyContextValueType | null = useContext( MyContext );

    console.debug( myStateValue?.state );  // <-- no error because of the "?" 

    if( myStateValue === null ){
        return null;
    }
    
    console.debug( myStateValue.state );  // <-- no error because of `myStateValue === null` 

    return <p>...</p>
};

If type assertions are preferred:

If you know for sure that the null value will never reach your consumers, and you prefer a solution with type assertion for some reason, this type assertion should probably better be done inside the consumers, not in the provider.

That is because in existing consumers you do know that they are only used when the state is available, but in the provider you do not know for sure if some day later another consumer will be created that will see the null value, and adapting the provider then can easily be forgotten.

A note about the reasoning

MyContext.Provider and MyContext.Consumer can, in principle, used independently. So you may use the consumer before a proper value was passed to the provider (even if you don't actually do that).

Therefore, the context should always have a "default" value, which always needs to be handled in consumers.

The consumer may

  • display a fallback,
  • or cause a component to return null,
  • or just decide that this is an error,
  • or something else,

but in any case, the default value should be handled explicitly in some way, because it can (in principle) happen.

Solution for your example:

import { type Context, createContext, type Dispatch, useReducer } from "react";

type MyStateValueType = string;

interface MyContextValueType {
    state:    MyStateValueType,
    dispatch: Dispatch<MyStateValueType>,
}

const MyContext: Context<MyContextValueType | null> = createContext<MyContextValueType | null>(null);

const MyStoreProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
    const [ state, dispatch ] = useReducer( (state: MyStateValueType)=> state, 'some value' );

    return (
        <MyContext.Provider value={{ state, dispatch }}> {/* <-- works: Provider accepts `null` and MyStateType */}
            { children }
        </MyContext.Provider>
    );
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.