AlbertMunichMar
AlbertMunichMar

Reputation: 1866

How to initialise the set function of useState for TypeScript in a createContext?

I have a Provider that provides a state variable and its corresponding setter through two contexts.

const BookedBatchContext = createContext({})
const SetBookedBatchContext = createContext(null)

const initialState = {
  id: null
}

The Provider looks like this:

export const BookedBatchProvider = ({ children }: { children: any }) => {
  const [bookedBatch, setBookedBatch] = useState(localState ||initialState)

  return (
    <SetBookedBatchContext.Provider value={setBookedBatch}>
      <BookedBatchContext.Provider value={bookedBatch}>
        { children }
      </BookedBatchContext.Provider>
    </SetBookedBatchContext.Provider>
  )
}

Through a custom hook I make the setBookedBatch available to other components:

export const useBookedBatch = () => {
  const bookedBatch = useContext(BookedBatchContext)
  const setBookedBatch = useContext(SetBookedBatchContext)

  return { bookedBatch, setBookedBatch }
}

When trying to use the setBookedBatch function, I get following error in a given component:

setBookedBatch(selectedBatch)

The error:

TS2721: Cannot invoke an object which is possibly 'null'.

Since the setter of the useState function is a function that I didn't create, I don't know how to initialise it when I create the context:

const SetBookedBatchContext = createContext(null)

So that TypeScript does not complain.

  1. How can I know the initial value of the setter function?
  2. How can I avoid that TS complains about the null value, if I am not providing any types?

Upvotes: 5

Views: 4515

Answers (1)

ford04
ford04

Reputation: 74500

The return types of React.createContext and React.useState are determined by inference from the initial values you pass in.

1.) You can create the proper context type by manually specifying the generic type:

const SetBookedBatchContext = createContext<null | React.Dispatch<React.SetStateAction<State>>>(null)

Note: The setter for useState has type React.Dispatch<React.SetStateAction<State>>, where State is whatever localState || initialState is.

2.) Assert in your custom Hook useBookedBatch, that setBookedBatch is not null:

export const useBookedBatch = () => {
  const bookedBatch = useContext(BookedBatchContext)
  const setBookedBatch = useContext(SetBookedBatchContext)
  if (setBookedBatch === null) throw new Error() // this will make setBookedBatch non-null
  return { bookedBatch, setBookedBatch }
  // returns: { bookedBatch: {}; setBookedBatch: React.Dispatch<React.SetStateAction<State>>; }
}

3.) Then setBookedBatch can be invoked without assertion later on:

const App = () => {
  const { setBookedBatch } = useBookedBatch()
  useEffect(() => { setBookedBatch({ id: "foo" }) }, [])
}

Sample on the playground

Upvotes: 8

Related Questions