React Typed Context

Viewed 775

I'm trying to use Context API with Typescript and nothing is working properly, I'm very confused, I already checked a lot of articles on the internet and it still can't give me an understanding of this matter.

import { createContext, useState } from 'react';

export const SelectedContext = createContext<number>();

export const SelectedContextProvider = (props) => {
    const [selectedId, setId] = useState(null);
    const changeValue = (value:number | null) => {
        setId(value);
    }
    return (
        <SelectedContext.Provider value={{selectedId, changeValue}}>
            {props.children}
        </SelectedContext.Provider>
    )
}

This is how I'm making my Context. I want it to have id: null as a default value but than when the user choose an option in Select I want to set is as number and make an API call. But right now I must specify default value for context and if I do than typescript shows me an error than type null can't be assigned to a number I want it to be in handleChange fuction.

And my second question is, how am I suppose to say what type my props will be right here if they're react children so it can be an empty array of array of React Nodes, I don't like to specify it as any because there's no use in using typescript in this situation.

        <SearchContextProvider>
            <CitySearchBar/>
            <CitySelect/>
        </SearchContextProvider>

Will someone be that kind and explain everything for me? I'm new to typescript and I don't know how to fix anmything :(

So I did like vuongvu suggested and now I'm getting

Type '{ selectedId: number | null; changeValue: (value: number | null) => 
void; }' is not assignable to type 'null'.ts(2322)
index.d.ts(335, 9): The expected type comes from property 'value' which is 
declared here on type 'IntrinsicAttributes & ProviderProps<null>'

At my value attribute for context provider

So after major changes I'm getting only one error but it still shows up. It seems that now my provider expects only setter as a value, not both data and setter and I'm getting error like this:

Type '[MySelectedContextType, 
Dispatch<SetStateAction<MySelectedContextType>>]' is not assignable to type 
'Dispatch<SetStateAction<MySelectedContextType>>'.
 Type '[MySelectedContextType, 
 Dispatch<SetStateAction<MySelectedContextType>>]' provides no match for the 
 signature '(value: SetStateAction<MySelectedContextType>): void'.ts(2322)

So the another problem I'm getting is when I try to consume this context I get this error:

TypeError: Object is not iterable (cannot read property 
Symbol(Symbol.iterator))

And I'm getting it like u said:

const [selectedId, setSelectedId] = useSelectedContext();
3 Answers

The major problem that you have is that your context consists not just of the value, but also of the setter. So your context is not just a number, but it's a number (or null) and a setter, like this:

type SelectedContextType = {
  selectedId: number | null,
  changeValue: (arg: number | null) => void
}

This is how I typically do context in TypeScript:

import React, { createContext, useState, useContext } from 'react';

// I like the verbosity of creating a context type separate
type SelectedContextType = {
  selectedId: number | null,
  changeValue: (arg: number | null) => void
}

// This is internal and should be set to SelectedContextType
const SelectedContext = createContext<SelectedContextType | undefined>(undefined);

// This is exported for other things to use and can be cast
export const useSelectedContext = () => useContext(SelectedContext) as SelectedContextType


export const SelectedContextProvider = (props) => {
    const [selectedId, setId] = useState<SelectedContextType['selectedId']>(null);
    const changeValue: SelectedContextType['changeValue'] = (value) => {
        setId(value);
    }
    return (
        <SelectedContext.Provider value={{selectedId, changeValue}}>
            {props.children}
        </SelectedContext.Provider>
    )
}

You can also shorten it a little bit by just passing the setter directly to the context value:

export const SelectedContextProvider = (props) => {
    const [selectedId, setId] = useState<SelectedContextType['selectedId']>(null);
    return (
        <SelectedContext.Provider value={{selectedId, changeValue:setId}}>
            {props.children}
        </SelectedContext.Provider>
    )
}

Essentially, you're just doing a "higher up in the tree" useState, so I'd suggest getting rid of all the boilerplate and just doing this:

import React, { createContext, useState, useContext } from 'react';

// this type is the actual type you are holding in state
type MySelectedContextType = number | null;

// this is what react returns when it calls useState with the type your are holding in state
type SelectedContextType = [
  MySelectedContextType,
  React.Dispatch<React.SetStateAction<MySelectedContextType>>
];

const SelectedContext = createContext<SelectedContextType | undefined>(undefined);

export const useSelectedContext = () => useContext(SelectedContext) as SelectedContextType;

// look how tiny this component is now? I love small components 
export const SelectedContextProvider: React.FC = ({children}) => (
    <SelectedContext.Provider value={useState<MySelectedContextType>(null)}>
        {children}
    </SelectedContext.Provider>
);

And then, where you need to use it

export const SomeOtherComponent: React.FC = () => {
  // destructure it just like you would setState
  const [selectedId,setSelectedId] = useSelectedContext();
  return ( ... )
}

Working stackblitz

You can specify the state value type by the <> symbol, example:

const [selectedId, setId] = useState<number | null>(null);

Reason for your error, if you do not specify the type when initial a value, typescript will automatically assign the type based on the first value you put in, so in this case:

    const [selectedId, setId] = useState(null);

selectedId will have Null type the whole time, so you cannot assign any other types to it later on (like number in your case).

type Context = {
  selectedId: number | null;
  changeValue = (value:number | null) => void;
}
import { createContext, useState } from 'react';

export const SelectedContext = createContext<Context>({} as Context);

export const SelectedContextProvider:React.Fc = ({children}: {children: React.ReactNode}) => {
    const [selectedId, setId] = useState(null);
    const changeValue = (value:number | null) => {
        setId(value);
    }
    return (
        <SelectedContext.Provider value={{selectedId, changeValue}}>
            {children}
        </SelectedContext.Provider>
    )
}
export const useStore = () => React.useContex(SelectedContext);

in order to use it across the whole app:

import {useStore} from './context';
const {selectedId, changeValue} = useStore();
Related