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:
- Cannot declare React context in typescript? [duplicate]
- Cannot find namespace 'ctx' error when creating Context with react - typescript
- How to use React Context with TypeScript
- React Context TypeScript — The Easy Way
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;
}