Home > OS >  What are the right types for a useContext with TypeScript/ReactJS?
What are the right types for a useContext with TypeScript/ReactJS?

Time:03-12

I am refactoring to TypeScript a tutorial in ReactJS that I am following. For the most part, the tutorial teaches you how to refactor most of the code as bonus material. However the log-in part is done in ReactJS/JavaScript only and I tried to refactor my code as a way to challenge myself. The part I am stuck on is createContext as I am not able to understand the types needed.

Original JS code

JS code - Context/Provider

import React, { useState, createContext } from "react";


export const Context = createContext();

const UserProvider = ({ children }) => {
    const [state, setState] = useState(undefined);

    return (
        <Context.Provider value={[state, setState]}>{children}</Context.Provider>
    )
};

export default UserProvider;

On the Login component it gets called with const [_user, setUser] = useContext(Context);

My attempt

What I've tried

I tried to apply the following solutions and I'm not really grasping the concepts well enough to apply them successfully:

TS code - Context/Provider

import React, { useState, createContext } from "react";
import { IUser, UserContextType } from "./@types/context";


export const Context = createContext<UserContextType | undefined>(undefined);

const UserProvider: React.FC<React.ReactNode> = ({ children }) => {
    const [state, setState] = useState<IUser>();

    return (
        <Context.Provider value={{state, setState}}>{children}</Context.Provider>
    )
};

export default UserProvider;

TS code - types

export interface IUser {
    user: string;
    password: string;
};

export type UserContextType = {
    state: IUser;
    setState: (newSession: IUser) => void;
};

Errors

<Context.Provider value={{state, setState}}>{children}</Context.Provider>

Type 'IUser | undefined' is not assignable to type 'IUser'. Type 'undefined' is not assignable to type 'IUser'.ts(2322)

const [_user, setUser] = useContext(Context);

Type 'UserContextType | undefined' is not an array type.ts(2461)

CodePudding user response:

First of all, you declare the context type like so:

export const Context = createContext<UserContextType | undefined>(undefined);

This means that the value of the context is either an object with all properties present of UserContextType or it is undefined.

Then here:

const [state, setState] = useState<IUser>();

You create state that has a type of IUser. But because you do not provide a default value, the default value is undefined. This is added to the state's type for you. So state is of type IUser | undefined.

Then we get here:

value={{state, setState}}

state is IUser | undefined, but the context type expects an IUser[] for that property. So neither IUser or undefined are valid types there.


First, you need to make sure that if you are passing in an object, then the object is correctly typed. That means that it has all required properties present. Else, pass in undefined. You can do this with a ternary expression:

value={state ? {state, setState} : undefined}

Now if state has a value, then the context value is created and passed it. Otherwise the context value is undefined.


But now you get this error:

    Types of property 'state' are incompatible.
      Type 'IUser' is missing the following properties from type 'IUser[]': length, pop, push, concat, and 26 more.(2322)

This is because you trying to assign a single user IUser to an array of users IUser[].

It looks like you mean for the content to only hold a single user, so you probably want to change the contexts type to:

export type UserContextType = {
    state: IUser;
    setState: (newSession: IUser) => void;
};

Which works, see playground

Or you need to pass in an array of users:

const UserProvider: React.FC<React.ReactNode> = ({ children }) => {
    const [state, setState] = useState<IUser[]>([]);

    return (
        <Context.Provider value={{state, setState}}>{children}</Context.Provider>
    )
};

Which also works, see playground


You could also change the context type to:

export type UserContextType = {
    state?: IUser;
    setState: (newSession: IUser) => void;
};

Which makes state optional, and then:

const UserProvider: React.FC<React.ReactNode> = ({ children }) => {
    const [state, setState] = useState<IUser>();

    return (
        <Context.Provider value={{state, setState}}>{children}</Context.Provider>
    )
};

Should work fine, see playground

CodePudding user response:

This is an example of a Context on Typescript

AuthContext.tsx:

import { createContext, ReactNode, useContext, useState } from "react";
import { AuthContextData } from "../models/AuthContextData.model";

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [user, setUser] = useState<object | null>(null);
  const [loading, setLoading] = useState<boolean>(true);

  async function signIn(): Promise<void> {
   console.log('sign in')
  }

  async function signOut(): Promise<void> {
    console.log('sign out')
  }

  return (
    <AuthContext.Provider
      value={{ signed: !!user, user, signIn, signOut, loading }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export function useAuth() {
  const context = useContext(AuthContext);
  return context;
}

AuthContextData.ts:

export interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
  signOut(): Promise<void>;
  loading: boolean;
}
  • Related