3

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)

SrAlch
  • 33
  • 2
  • 5
  • 2
    The context does not have `state` or `setState` in its type. Did you mean to use those names instead of `sessions` and `saveSession`? Also, since you're passing in an object as the context value, the return value of `useContext` will also be an object (with those properties), not an array (so don't use `[]` brackets) – CertainPerformance Mar 11 '22 at 20:03
  • @CertainPerformance I edited the code based on what I think you are suggesting. My guess was that there wasn't a direct relationship between the name of the types and the useState variables created. About the brackets [] not sure if you mean only on the useState or overall. – SrAlch Mar 11 '22 at 20:22
  • are the errors still existing, if yes please update your question? – Ahmed Tarek Mar 11 '22 at 20:32
  • @AhmedTarek yes they are still existing and I updated the errors themselves. – SrAlch Mar 11 '22 at 20:34
  • 1
    I see that the is different in `TS code - Context/Provider` and `Errors` code snippets. I suggest you using what is in `TS code - Context/Provider` – Ahmed Tarek Mar 11 '22 at 20:45
  • 1
    You're now passing an array as the context value instead of an object here: `value={[state, setState]}` Decide on either an array or an object, then use that schema everywhere – CertainPerformance Mar 11 '22 at 20:47
  • So for what I'm understanding both @AhmedTarek and @CertainPerformance are referring to `value={{state, setState}}` as the way to do it as an object. When I do it that way `value={{state` still gives me an error that `Type 'IUser | undefined' is not assignable to type 'IUser'.`. I have also removed the array from the type `UserContextType` as was giving me an error with `setState`.I updated everything on the post – SrAlch Mar 11 '22 at 21:39

2 Answers2

3

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

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 2
    I know is not the expected comment of SO but I want to deeply thank you for the effort you have put into showing me this. Not only now works but now I have a much better understanding of `createContext` – SrAlch Mar 11 '22 at 22:35
  • I'll allow it :) You're welcome. – Alex Wayne Mar 12 '22 at 00:41
-2

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;
}
Gustavo Farias
  • 393
  • 5
  • 15
  • Not gonna lie, this confuses me a bit more, and rises some questions. How do I type check the input of the user as string? as I'm understanding it is an object or a null. Also How do I access from that object the password or the username? – SrAlch Mar 11 '22 at 21:46
  • That is an example of context, sure. But it doesn't explain why the OP is having problems. – Alex Wayne Mar 11 '22 at 22:39