React Context in Typescript - reactjs

I am creating context to handle authentication in typescript app. I have written this AuthProvider.ts
/*-- Imports --*/
/*-- AuthContextType interface and defaults -- */
const AuthContext = createContext<AuthContextType>(contextDefaults);
export const useAuth = () => {
return useContext(AuthContext);
};
interface AuthProviderProps {
children: ReactNode;
}
const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null);
const isLoggedIn = !!user;
const login = (data: User) => {
setUser(data);
};
const logout = () => {
setUser(null);
};
return (
<AuthContextType.Provider value={{ user, isLoggedIn, login, logout }}>
{children}
</AuthContextType.Provider>
);
};
export default AuthProvider;
Then I am wrapping App with this AuthProvider;
/* --- imports ---*/
const App = () => {
return (
<AuthProvider>
<div>App</div>
</AuthProvider>
);
};
export default App;
This code is giving errors and when I write same code in javascript app it is working fine.
AuthContext.ts Errors
App.js Error
Any help will be appreciated.
Thanks

Could it be that you've used: AuthContextType.Provider when you mean AuthContext.Provider?
AuthContextType is a Typescript type, not a react context object.
const AuthProvider = ({ children }: AuthProviderProps) => {
....
return (
<AuthContext.Provider value={{ user, isLoggedIn, login, logout }}>
{children}
</AuthContext.Provider>
);
};
Does this work?

Related

Why is the function from the context not recognized?

I'm trying to make an auth Context:
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
and a custom hook to get it:
import { useContext } from "react";
import AuthContext from "./AuthProvider";
const useAuth = () => {
return useContext(AuthContext);
}
export default useAuth;
And now when I try to use the setAuth function:
const Login = () =>{
const {setAuth} = useAuth();
...
setAuth({authResponce});
I get an error : Uncaught TypeError: setAuth is not a function .
I'm new to react and I don't understand the reason yet, so I'll be glad to help.
Context only works if the provider is farther up the component tree than the consumer. If there is no provider, then Login will receive the default value, and you set the default to an empty object when you wrote createContext({});. Since that object has no setAuth function, you get the error.
To fix this, make sure the auth provider is near the top of the tree, and login is inside it. Login doesn't need to be an immediate child of the provider, but it does need to be a descendant.
// Good
const App = () => {
return (
<AuthProvider>
<Login />
</AuthProvider>
)
}
// Also good
const App = () => {
return (
<AuthProvider>
<SomeOtherComponent />
</AuthProvider>
)
}
const SomeOtherComponent = () => {
return <Login />
}
// Bad
const App = () => {
return (
<div>
<AuthProvider/>
<Login/>
</div>
)
}

TypeScript and createContext - Property is missing on type

Fairly new to TypeScript and trying to get the hang of data values and types.
import React, { useState, createContext } from 'react'
const defaultValues = {
id: undefined
}
type AuthenticatedUser = typeof defaultValues
export const UserContext = createContext<AuthenticatedUser>(defaultValues)
export const UserProvider = (props: { children?: React.ReactChild }) => {
const [authenticatedUser, setAuthenticatedUser] = useState<AuthenticatedUser>(defaultValues)
const { children } = props
return (
<UserContext.Provider value={[authenticatedUser, setAuthenticatedUser]}>
{children}
</UserContext.Provider>
)
}
This gives me the following error
Property 'id' is missing in type '({ id: undefined; } | Dispatch<SetStateAction<{ id: undefined; }>>)[]' but required in type '{ id: undefined; }'.ts(2741)
I have tried all kinds of combinations
In the UserContext.Provider, the value is expecting a type of { id: undefined } but you are currently assigning the value to the provider of type ({ id: undefined; } | Dispatch<SetStateAction<{ id: undefined; }>>)[].
Here, we are supplying the provider with an array of type ({ id: undefined; } | Dispatch<SetStateAction<{ id: undefined; }>>)[] instead of the expected type of { id: undefined }
<UserContext.Provider value={[authenticatedUser, setAuthenticatedUser]}>
{children}
</UserContext.Provider>;
If we supply the Provider with just value={authenticatedUser}, we can see that the error goes away.
<UserContext.Provider value={authenticatedUser}>
{children}
</UserContext.Provider>;
From what I can see, I think you want to be able to change the User from anywhere in your app. You'll need to setup a function on your UserProviderthat allows you to login, logout, etc.
export const UserProvider = (props: { children?: React.ReactChild }) => {
const [authenticatedUser, setAuthenticatedUser] = useState(defaultValues);
const { children } = props;
const login = () => {};
const logout = () => {};
const register = () => {};
return (
<UserContext.Provider value={authenticatedUser}>
{children}
</UserContext.Provider>
);
};
07/07/2021 Update
import React, { useState, createContext } from "react";
type User = {
id: number | undefined;
};
type UserContextType = {
user?: User;
login: () => void;
logout: () => void;
};
export const UserContext = createContext<UserContextType>(
{} as UserContextType
);
export const UserProvider = (props: { children?: React.ReactChild }) => {
const [user, setUser] = useState<User>();
const { children } = props;
const login = () => {};
const logout = () => {};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
};
Problem explanation
You created UserContext with type AuthenticatedUser, but you are passing in an array (value={[authenticatedUser, setAuthenticatedUser]}) in your jsx.
Fix
Change this:
<UserContext.Provider value={[authenticatedUser, setAuthenticatedUser]}>
to this:
<UserContext.Provider value={authenticatedUser}>
If you want access to the setter via your context, you will have to change the type of UserContext to something like this:
// Note: defaultValues will be invalid here.
export const UserContext = createContext<[AuthenticatedUser, React.Dispatch<React.SetStateAction<AuthenticatedUser>>]>(defaultValues);
Playground.

Test component with context and react hook

I have a simple Dashboard component that relies on React context to manage auth. It contains a custom hook useAuth to extract the current user as well as the auth related functions: login, logout, etc.
This is the Context file: AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from "react";
import { auth } from "../config/firebase";
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
function logout() {
return auth.signOut();
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
This is the Dashboard.js component:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
export default function Dashboard() {
const { currentUser, logout } = useAuth();
const [error, setError] = useState("");
const history = useHistory();
const handleLogout = async () => {
setError("");
try {
await logout();
history.push("/login");
} catch (e) {
setError(e.message);
}
};
return (
<div>
{error && <p>{error}</p>}
<h1>This is the Dashboard</h1>
<h5>Email: {currentUser.email}</h5>
<button onClick={handleLogout} type="button">
Logout
</button>
</div>
);
}
As recommened by React Testing Library I have created a test-utils.js file:
import React, { createContext } from "react";
import { render } from "#testing-library/react";
import { BrowserRouter as Router } from "react-router-dom";
const AuthContext = createContext();
const currentUser = {
email: "abc#abc.com",
};
const signup = jest.fn();
const login = jest.fn();
const logout = jest.fn();
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};
const customRender = (ui, options) => {
render(ui, { wrapper: AllTheProviders, ...options });
};
export * from "#testing-library/react";
export { customRender as render };
However, when running Dashboard.test.js I get error
TypeError: Cannot destructure property 'currentUser' of '((cov_5mwatn2cf(...).s[0]++) , (0 , _AuthContext.useAuth)(...))' as it is undefined.
4 |
5 | export default function Dashboard() {
> 6 | const { currentUser, logout } = useAuth();
| ^
7 | const [error, setError] = useState("");
8 | const history = useHistory();
import React from "react";
import Dashboard from "./Dashboard";
import { act, render, screen } from "../config/test-utils-dva";
beforeEach(async () => {
await act(async () => {
render(<Dashboard />);
});
});
test("displays dashboard", () => {
expect(screen.getByText(/dashboard/i)).toBeInTheDocument();
});
I think it is because Dashboard component is trying to use useAuth from AuthContext.js, how can I force the rendered Dashboard component to use the mocked data that I am sending in the test-utils.jsfile?
Instead of creating a new context, use the AuthContext from context/AuthContext for <AuthContext.Provider>, as that's the context that the hook uses.
So, in AuthContext.js, export the context instance:
export const AuthContext = createContext();
Then, in your test-util.js file, instead of again calling createContext (which will create a completely separate context instance - the contexts are not the same even if they are stored in a variable with the same name!), just import the previously exported instance:
import { AuthContext } from "../context/AuthContext";
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};

How to get the value of redux store in other components on typescript react hooks project

a gopher new come to react. Now i write a easy example with golang and React.ts. the server will return a tokenstring, the Login components will receive the token string and decode it get a UserState interface type variable,
export interface UserState {
name: string;
role: string;
}
then send a dispatch with this user. Now I want print the current user name on Home Component.I try the store.getState() function, but i don't know how to defines the type of Home component params..So what is the best way to get the user name on store in Home Component? Thanks!
This is Login Compoent file the ... means return block that just a form with two inputs and a submit button.Now I want get the user name
const Login: React.FC<RouteComponentProps> = (props: RouteComponentProps) => {
const [loading, setLoading] = useState(false);
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
const dispatch = useDispatch();
const handleSubmit = async (values: LoginFormData) => {
try {
setLoading(true);
const result = (await request(
'/api/login',
values,
'POST'
)) as LoginResponseData;
if (result.code === '1') {
storageUtils.saveToken(result.token);
const user = storageUtils.getUser(result.token);
dispatch(receiveUser(user));
message.success(result.msg);
setLoading(false);
props.history.push('/home');
} else {
message.error(result.msg);
setLoading(false);
}
} catch (err) {
message.error(err.message);
}
};
....
export default Login;
These is my redux define.actions.ts
import storageUtils from '../utils/storage';
import { UserState } from '../types/global';
export const RECEIVE_USER = 'receive_user';
export const RESET_USER = 'reset_user';
interface ReceiveUser {
type: typeof RECEIVE_USER;
payload: UserState;
}
interface LogOut {
type: typeof RESET_USER;
}
export type UserAction = ReceiveUser | LogOut;
export const receiveUser = (user: UserState) => ({
type: RECEIVE_USER,
payload: user,
});
export const logout = () => {
storageUtils.removeToken();
return { type: RESET_USER };
};
The reducer
import { combineReducers } from 'redux';
import { RECEIVE_USER, RESET_USER, UserAction } from './action';
import { UserState } from '../types/global';
const initialUser: UserState = { name: '', role: '' };
function user(state = initialUser, action: UserAction) {
switch (action.type) {
case RECEIVE_USER:
return { ...state, ...action.payload };
case RESET_USER:
return {};
default:
return state;
}
}
export default combineReducers({
user,
});
My store.
import { createStore } from 'redux';
import rootReducer from './reducer';
const initialState = {};
const store = createStore(rootReducer, initialState);
export default store;
I use the <Provider store={store}> <Router/> </Provider> tag nested the Router compoent.
my Router compoent
const Login = React.lazy(() => import('./../views/login'));
const Routers: React.FC = () => (
<HashRouter>
<Suspense fallback={<Loading />}>
<Switch>
<Route exact path="/" component={Login} />
<Route path="/home" component={Home} />
</Switch>
</Suspense>
</HashRouter>
);
export default Routers;
A easy Home compoent.
const Home: React.FC = () => {
return <div>The current user name is {user.name}</div>;
};
You can get the username from a function component by using useSelector
ref: https://react-redux.js.org/api/hooks#useselector
Something like this:
const username = useSelector(state => state.user.username)
Wherever possible, try to use this useSelector method, because it highly optimizes the state selector, and you can even create custom functions that return the same specific field for reusability.

Use Context API and functional component as service in React

I have a context API:
import React, { createContext, useState } from "react";
const UserContext = createContext();
const UserContextProvider = (props) => {
const [userInfo, setUserInfo] = useState({});
return (
<UserContext.Provider value={{ userInfo, setUserInfo }}>
{props.children}
</UserContext.Provider>
)
};
export { UserContextProvider, UserContext };
and use it in App.js:
<UserContextProvider>
// Router,...
</UserContextProvider>
Well, I gonna use context API in component like a service:
import { UserContext } from "...";
function UserService() {
const { userInfo, setUserInfo } = useContext(UserContext);
const updateUserInfo = (newUserInfo) => {
setUserInfo(newUserInfo); // for example: {name:'x'}
}
return null;
}
Now I wanna use UserService inside a component without add <UserService /> ? How can I call UserService.updateUserInfo()?
Your don't need to userService.You can use UserContext directly inside App.js and access to its function but you must wrap App.js inside UserContextProvider like:
<UserContextProvider>
<App />
</UserContextProvider>
Or you can use HOC(higher order component) like:
const withUsersContext = (Comp) => {
return (props) => (
<UserContextProvider>
<Comp {...props} />
</UserContextProvider>
);
};
// then inside App.js:
...
export default withUsersContext(App)
Now inside App.js:
const { userInfo, setUserInfo } = useContext(UserContext);
Note: if you wanna use UserContext inside something like UserService you must write a custom hook for UserService(rules of react-hooks).
Custom hooks for UserService:
function useUserService() {
const { userInfo, setUserInfo } = useContext(UserContext);
const updateUserInfo = (newUserInfo) => {
setUserInfo(newUserInfo);
}
return { updateUserInfo };
}
How to use inside a component:
...
const { updateUserInfo }= useUserService();
...

Resources