useState with useContext in Typescript
Asked Answered
I

3

11

Lets say I have a basic setup for checking whether a user is logged in or not:

import { createContext } from "react";
const UserContext = createContext<string | null>(null)
export default UserContext

In my App.tsx I want to create a useState hook so that I can manage the state of my context throughout my application:

//Context
const [context, setContext] = useState<string | null>(null)

  <UserContext.Provider value={{context, setContext}}>

    <Routes>

      <Route path="/" element={<Home/>}/> 
      <Route path="/login" element={<Login/>}/> 
      <Route path="/register" element={<Register/>}/> 
      <Route path="/admin" element={<Admin/>}></Route>

    </Routes>  

  </UserContext.Provider>

So as far as I can see I'll only ever need either the name of the logged in user, or set the state of the context to null if I want to deny access to certain pages. Now the issue is that typescript is yelling at me here, specifically in the value of the Provider:

Type '{ context: string | null; setContext: 
React.Dispatch<React.SetStateAction<string | null>>; }' is not assignable to type 'string'.ts(2322)

I can force cast the value as follows:

value={{context, setContext} as any}

But that doesn't seem like a particularly elegant 'typescripty' solution.

Any help is appreciated!

Intent answered 29/5, 2022 at 1:7 Comment(1)
When you write const UserContext = createContext<string | null>(null) you are saying the value of your context is string or null but when you pass the value prop to the context provider you are passing an object with a context property that's string or null as well as a setter. You need to make the types match. Either just pass the context value from useState to the provider or change the type of your call to useContext to be an object with a string or null context property and a setter function.Coursing
K
5

You've typed your context as:

string | null

But then you provide a value for that context of (approxiamately) type:

{ context: string | null, setContext: (newString) => void }

So if you want the state setter in your context, then it needs to be part of the context's type:

const UserContext = createContext<{
  context: string | null,
  setContext: (newValue) => void
}>({
  context: null,
  setContext: () => undefined
})
Kermit answered 29/5, 2022 at 1:20 Comment(1)
Thanks. The code you provided still gave me some more errors but I altered it a bit, and I posted it below.Intent
I
15

Based on Alex's answer I came up with a tweak that works for me:

type UserContextType = {
     context: string | null,
     setContext: React.Dispatch<React.SetStateAction<string | null>>
 }

const iUserContextState = {
    context: null,
    setContext: () => {}
}

const UserContext = createContext<UserContextType>(iUserContextState)

export default UserContext

And then in App.tsx:

//Context
const [context, setContext] = useState<string | null>(null)

  <UserContext.Provider value={{context, setContext}}>

    <Routes>

      <Route path="/" element={<Home/>}/> 
      <Route path="/login" element={<Login/>}/> 
      <Route path="/register" element={<Register/>}/> 
      <Route path="/admin" element={<Admin/>}></Route>

    </Routes>  

  </UserContext.Provider>
Intent answered 29/5, 2022 at 2:7 Comment(0)
K
5

You've typed your context as:

string | null

But then you provide a value for that context of (approxiamately) type:

{ context: string | null, setContext: (newString) => void }

So if you want the state setter in your context, then it needs to be part of the context's type:

const UserContext = createContext<{
  context: string | null,
  setContext: (newValue) => void
}>({
  context: null,
  setContext: () => undefined
})
Kermit answered 29/5, 2022 at 1:20 Comment(1)
Thanks. The code you provided still gave me some more errors but I altered it a bit, and I posted it below.Intent
T
0

If you want to keep the useState's array as a return value, this is a correctly TypeScript solution:

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

// Change shape of this object to your/app needs
const initialAppState = {
  userName: "",
};

export type AppState = typeof initialAppState;

type ContextType = [AppState, React.Dispatch<React.SetStateAction<AppState>>];
const AppStateContext = createContext<ContextType>([] as unknown as ContextType);

type Props = {
  appState?: AppState;
};

/**
 * Provider to pass state to the React application
 */
export const AppStateProvider: FC<React.PropsWithChildren<Props>> = (props) => {
  const { appState = null } = props;
  const useStateRetValue = useState<AppState>(appState == null ? initialAppState : appState);

  return <AppStateContext.Provider value={useStateRetValue}>{props.children}</AppStateContext.Provider>;
};
AppStateProvider.defaultProps = {
  appState: undefined,
};

/**
 * Hook to access application state
 */
export const useAppStateContext = (): ContextType => useContext<ContextType>(AppStateContext);

Provider usage:

  <AppStateProvider>

    <Routes>

      <Route path="/" element={<Home/>}/> 
      <Route path="/login" element={<Login/>}/> 
      <Route path="/register" element={<Register/>}/> 
      <Route path="/admin" element={<Admin/>}></Route>

    </Routes>  

  </AppStateProvider>

Hook usage:


const [appState, setAppState] = useAppStateContext();

console.log(appState.userName);

useEffect(() => {
  setAppState({ userName: "John Doe" });
}, [setAppState]);

// ...or...

const [, setAppState] = useAppStateContext();

// ...or...

const [appState] = useAppStateContext();

Topple answered 9/1 at 11:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.