I have been practicing with React Context, but I have a problem with storing a user in my general state, the problem is that I have to click the save button twice to save the state in my context, with the first click I only see that the state is as an empty object and with the second click the state is saved (the name and password), this is the code I have. This would be the context of my application:
import { createContext } from "react";
const UserContext = createContext()
export default UserContext
this would be my types file:
export const LOGIN= 'LOGIN'
this is my UserState file:
import { useReducer } from "react"
import UserReducer from './UserReducer'
import UserContext from "./UserContex"
const UserState = ({ children }) => {
const initialState = {
usernew: {}
}
const [state, dispatch] = useReducer(UserReducer, initialState)
const login = (userLogin) => {
dispatch({
type: 'LOGIN',
payload: userLogin
})
window.localStorage.setItem('user', JSON.stringify(state.usernew))
const data = window.localStorage.getItem('user')
console.log(data)
}
return (
<UserContext.Provider
value={{
usernew: state.usernew,
login,
}}
>
{children}
</UserContext.Provider>
)
}
export default UserState
UserReducer file:
import {LOGIN} from "../types";
export default (state, action) => {
const { payload, type } = action;
switch (type) {
case LOGIN:
return {
...state,
usernew: payload
}
default:
return state
}
}
and this would be my component where I am changing the context:
import { Box, Button } from '#chakra-ui/react'
import { Input } from '#chakra-ui/react'
import { useState } from 'react'
import { useContext } from 'react'
import UserContext from '../context/User/UserContex'
const Login = () => {
const { login, usernew} = useContext(UserContext)
const [fields, setFields] = useState({
name: '',
password: ''
})
const handleChange = (e) => {
const name = e.target.name;
const value = e.target.value;
setFields({
...fields,
[name]: value
})
}
return (
<Box w='80%' p={4}>
<Input
placeholder='Name'
onChange={handleChange}
name="name"
value={fields.name}
/>
<Input
placeholder='Password'
onChange={handleChange}
name="password"
value={fields.password}
/>
<Button bg="teal" color="#fff" onClick={() => login(fields)}>Submit</Button>
{usernew ?
<div>
<h2>Usuario con {usernew.name} - {usernew.password}</h2>
</div>
:
<div>
<h1>Sin usuario</h1>
</div>
}
</Box>
)
}
export default Login
I checked all the code but I can't find the error, that's why I'm showing all the code I have. Thank you for your attention
Related
I have a simple next.js app, that allows a user to login via a login-page. The login is done via a graphql-api. I'm using the react context-API and after the user has logged in succesfully I'm updating the context. Afterwards I would like to redirect the user to a dashboard-page. It actually works as intended, however always (and only) on the second login (= login, logout, login again) I get the following Error in my console:
I understand the error (or warning?), but I don't know why it occurs or what I'm doing wrong. Any suggestion to point me in the right direction is much appreciated.
Here's my code:
auth-context.ts
import { createContext, useEffect, useState } from 'react';
//type-definitions removed for better readability
const AuthContext = createContext<AuthContext>({
isAuthenticated: false,
isAdmin: false,
userId: '',
loginSuccessHandler: () => {},
logoutHandler: () => {},
});
const AuthContextProvider = ({ children }: AuthContextProviderProps) => {
const [authData, setAuthData] = useState<AuthData>({
isAuthenticated: false,
isAdmin: false,
userId: null,
});
useEffect(() => {
const storedIsAuthenticated = localStorage.getItem('isAuthenticated');
const storedUserId = localStorage.getItem('userId');
const storedRole = localStorage.getItem('role');
if (storedIsAuthenticated === '1') {
setAuthData({
isAuthenticated: true,
userId: storedUserId,
isAdmin: storedRole === 'ADMIN',
});
}
}, []);
const loginSuccessHandler = (isAuthenticated: boolean, userId: string, role: string) => {
localStorage.setItem('isAuthenticated', '1');
localStorage.setItem('userId', userId);
localStorage.setItem('role', role);
setAuthData({
isAuthenticated: isAuthenticated,
userId: userId,
isAdmin: role === 'ADMIN',
});
};
const logoutHandler = () => {
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId');
localStorage.removeItem('role');
setAuthData({
isAuthenticated: false,
userId: null,
isAdmin: false,
});
};
return (
<AuthContext.Provider
value={{
isAuthenticated: authData.isAuthenticated,
isAdmin: authData.isAdmin,
userId: authData.userId,
loginSuccessHandler,
logoutHandler,
}}
>
{children}
</AuthContext.Provider>
);
};
export { AuthContext, AuthContextProvider };
pages/login.tsx
import { gql, useLazyQuery } from '#apollo/client';
import { useRouter } from 'next/router';
import { SyntheticEvent, useContext, useEffect, useRef } from 'react';
import TextInput from '../components/Input/TextInput';
import { AuthContext } from '../contexts/auth-context';
import type { NextPage } from 'next';
const Login: NextPage = () => {
const emailInputRef = useRef<HTMLInputElement>(null);
const passwordInputRef = useRef<HTMLInputElement>(null);
const authCtx = useContext(AuthContext);
const router = useRouter();
const LOGIN_QUERY = gql`
query LoginQuery($email: String!, $password: String!) {
login(email: $email, password: $password) {
userId
token
role
}
}
`;
const [login, { loading, error }] = useLazyQuery(LOGIN_QUERY);
const submitButtonHandler = (event: SyntheticEvent) => {
event.preventDefault();
const email = emailInputRef?.current?.value || null;
const password = passwordInputRef?.current?.value || null;
login({
variables: { email, password },
onCompleted(data) {
authCtx.loginSuccessHandler(true, data.login.userId, data.login.role);
},
onError(error) {
authCtx.logoutHandler();
},
});
};
/*
The following Effect leads to the warning/error, when I remove it, the error disappears.
However, that's not what I want, I want it to work like this
*/
useEffect(() => {
authCtx.isAuthenticated
? authCtx.isAdmin
? router.push('/admin')
: router.push('/my-account')
: router.push('/login');
}, [authCtx.isAuthenticated, authCtx.isAdmin]);
return (
<div>
<form>
<h1>Login</h1>
{loading && <p>Loading</p>}
{error && <p>Error: {error.message}</p>}
<TextInput type="email" id="email" ref={emailInputRef} />
<TextInput type="password" id="password" ref={passwordInputRef} />
<button onClick={submitButtonHandler} >
Submit
</button>
</form>
</div>
);
};
export default Login;
In _app.tsx I use my AuthContextProvider like this:
import '../styles/globals.scss';
import Layout from '../components/Layout/Layout';
import type { AppProps } from 'next/app';
import { AuthContextProvider } from '../contexts/auth-context';
import { ApolloProvider } from '#apollo/client';
import apolloClient from '../lib/apollo-client';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={apolloClient}>
<AuthContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AuthContextProvider>
</ApolloProvider>
);
}
export default MyApp;
Thanks to #Anthony Ma's comment, I found that the onCompleted-Handler in login.tsx seemed to be the issue here. It updates the authContext while still being in the rendering-process of the login-component.
I changed my login.tsx file to add the data-response from my graphql-API to a state-object and then use an effect with that state-object as dependency (Find -> changed comments in the code below to see all changes).
Updated login.tsx:
import { gql, useLazyQuery } from '#apollo/client';
import { useRouter } from 'next/router';
/*
-> changed: add useState to the list of imports
*/
import { SyntheticEvent, useContext, useEffect, useRef, useState } from 'react';
import TextInput from '../components/Input/TextInput';
import { AuthContext } from '../contexts/auth-context';
import type { NextPage } from 'next';
//type definition "AuthData" goes here
const Login: NextPage = () => {
const emailInputRef = useRef<HTMLInputElement>(null);
const passwordInputRef = useRef<HTMLInputElement>(null);
const authCtx = useContext(AuthContext);
const router = useRouter();
/*
-> changed: Add authData State
*/
const [authData, setAuthData] = useState<AuthData>({
isAuthenticated: false,
userId: '',
role: '',
token: '',
});
const LOGIN_QUERY = gql`
query LoginQuery($email: String!, $password: String!) {
login(email: $email, password: $password) {
userId
token
role
}
}
`;
const [login, { loading, error }] = useLazyQuery(LOGIN_QUERY);
const submitButtonHandler = (event: SyntheticEvent) => {
event.preventDefault();
const email = emailInputRef?.current?.value || null;
const password = passwordInputRef?.current?.value || null;
login({
variables: { email, password },
/*
-> changed: Important: Set fetchPolicy to 'network-only' to prevent
caching of the response (otherwise the response wont change on
every login request and therefore the state wont change resulting
in the effect (see below) not being triggered.
*/
fetchPolicy: 'network-only',
onCompleted(data) {
/*
-> changed: add response data to the new state object "authData"
*/
setAuthData({
isAuthenticated: true,
userId: data.login.userId,
role: data.login.role,
token: data.login.token,
});
},
onError(error) {
authCtx.logoutHandler();
},
});
};
/*
-> changed: added this new effect with the authData state object as
dependency
*/
useEffect(() => {
authCtx.loginSuccessHandler({
isAuthenticated: authData.isAuthenticated,
userId: authData.userId,
role: authData.role,
token: authData.token,
});
}, [authData]);
useEffect(() => {
authCtx.isAuthenticated
? authCtx.isAdmin
? router.push('/admin')
: router.push('/my-account')
: router.push('/login');
}, [authCtx.isAuthenticated, authCtx.isAdmin]);
return (
<div>
<form>
<h1>Login</h1>
{loading && <p>Loading</p>}
{error && <p>Error: {error.message}</p>}
<TextInput type="email" id="email" ref={emailInputRef} />
<TextInput type="password" id="password" ref={passwordInputRef} />
<button onClick={submitButtonHandler} >
Submit
</button>
</form>
</div>
);
};
export default Login;
I'm working with ReacJs and Typescript.
When I'm authenticating a user, using the methods below, I get the following error message:
Unhandled Rejection (TypeError): auth.authenticate is not a function
onSubmit
src/components/Login/index.tsx:33
30 |
31 | const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
32 | event.preventDefault();
> 33 | await auth.authenticate(
| ^34 | values.username,
35 | values.password
36 | );
My inexperience must be leading me to this mistake. I'm still very confused by Typescript declarations.
I present below the codes of the methods I'm using:
Login.tsx
import React, { useState } from "react";
import { useAuth } from "../../context/AuthProvider/useAuth"
import { useHistory } from "react-router"
import { Link } from "react-router-dom";
import {
Label,
Input,
wrapper,
FormWrapper,
Date,
Submit,
Button,
LoginQuestion
} from "./styles";
const Login = () => {
const auth = useAuth();
const history = useHistory();
const initialState = {
username: "",
password: "",
};
const [values, setValues] = useState(initialState);
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValues({ ...values, [event.target.name]: event.target.value });
};
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
await auth.authenticate( **<--------- Error on this line.**
values.username,
values.password
);
history.push('/');
};
return (
<Wrapper>
<FormWrapper>
<h2>Login</h2>
<form onSubmit={onSubmit} noValidate >
<Date>
<Label htmlFor="username">User</Label>
<Input type='text' name='username' onChange={onChange} />
</Date>
<Date>
<Label htmlFor="password">Password</Label>
<Input type='password' name='password' onChange={onChange} />
</Date>
<Submit>
<Button>Login</Button>
</Submit>
<LoginQuestion>
<Link
to="/signup">Create an account
</Link>
</LoginQuestion>
<LoginQuestion>
<Link
to="/signup">Recover password
</Link>
</LoginQuestion>
</form>
</FormWrapper>
</Wrapper>
);
}
export default Login
useAuth.tsx
import { useContext } from "react"
import { AuthContext } from "."
export const useAuth = () => {
const context = useContext(AuthContext);
return context;
}
AuthProvider.tsx
import React, {createContext, useEffect, useState} from "react";
import { IAuthProvider, IContext, IUser } from "./types";
import { getUserLocalStorage, LoginRequest, setUserLocalStorage } from "./util";
export const AuthContext = createContext<IContext>({} as IContext)
export const AuthProvider = ({children}: IAuthProvider) => {
const [user, setUser] = useState<IUser | null>()
useEffect(() => {
const user = getUserLocalStorage();
if (user) {
setUser(user);
}
}, [])
async function authenticate(
username:string,
password: string
) {
const response = await LoginRequest(username, password);
const payload = {token: response?.token};
setUser(payload);
setUserLocalStorage(payload);
}
function logout () {
setUser(null);
setUserLocalStorage(null);
}
return (
<AuthContext.Provider value={{...user, authenticate, logout}}>
{children}
</AuthContext.Provider>
)
}
In this file I declare the interfaces.
types.ts
export interface IUser {
username?: string;
token?: string;
}
export interface IContext extends IUser {
authenticate: (username: string, password: string) => Promise<void>;
logout: () => void;
}
export interface IAuthProvider {
children: JSX.Element;
}
export interface ILoginRequest {
token: string;
}
What am I doing wrong ?
Thanks!
As noted by Marcus, in the comment above, I was not calling the AuthProvider function, and this caused the error in question. Thanks to Marcus.
AuthProvider must be called before routes, according to the code below:
import React, { FC } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './context/AuthProvider'
import ProtectedLayout from './components/ProtectedLayout';
import Routers from './routers';
import Sidebar from './components/sidebar/Sidebar';
const App: FC = () => {
return (
<AuthProvider>
<BrowserRouter>
<ProtectedLayout>
<Sidebar />
</ProtectedLayout>
<Routers />
</BrowserRouter>
</AuthProvider>
);
};
export default App;
I'm following a tutorial on learning Redux and I'm stuck at this point where state that should have an image url is returned as undefined.
Image is successfully saved in firbase storage and dispatched but when I try to get the url on new route with useSelector it is undefined.
import React, {useEffect} from "react";
import {useSelector} from "react-redux";
import {useHistory} from "react-router-dom";
import "./ChatView.css";
import {selectSelectedImage} from "./features/appSlice";
function ChatView() {
const selectedImage = useSelector(selectSelectedImage);
const history = useHistory();
useEffect(() => {
if(!selectedImage) {
exit();
}
}, [selectedImage])
const exit = () => {
history.replace('/chats');
}
console.log(selectedImage)
return (
<div className="chatView">
<img src={selectedImage} onClick={exit} alt="" />
</div>
)
}
export default ChatView
reducer created for chat (slice):
import { createSlice } from '#reduxjs/toolkit';
export const appSlice = createSlice({
name: 'app',
initialState: {
user:null,
selectedImage:null,
},
reducers: {
login: (state, action) => {
state.user = action.payload;
},
logout: (state) => {
state.user = null;
},
selectImage:(state, action) => {
state.selectedImage = action.payload
},
resetImage:(state) => {
state.selectedImage = null
}
},
});
export const { login, logout, selectImage, resetImage} = appSlice.actions;
export const selectUser = (state) => state.app.user;
export const selectSelectedImage = (state) => state.app.selectImage;
export default appSlice.reducer;
and code for dispatching that imageURL which when i console.log it gives the correct url:
import {Avatar} from "#material-ui/core";
import StopRoundedIcon from "#material-ui/icons/StopRounded"
import "./Chat.css";
import ReactTimeago from "react-timeago";
import {selectImage} from "./features/appSlice";
import {useDispatch} from "react-redux";
import {db} from "./firebase";
import {useHistory} from "react-router-dom";
function Chat({id, username, timestamp, read, imageUrl, profilePic}) {
const dispatch = useDispatch();
const history = useHistory();
const open = () => {
if(!read) {
dispatch(selectImage(imageUrl));
db.collection('posts').doc(id).set({read:true,}, {merge:true});
history.push('/chats/view');
}
};
return (
<div onClick={open} className="chat">
<Avatar className="chat__avatar" src={profilePic} />
<div className="chat__info">
<h4>{username}</h4>
<p>Tap to view - <ReactTimeago date={new Date(timestamp?.toDate()).toUTCString()} /></p>
</div>
{!read && <StopRoundedIcon className="chat__readIcon" />}
</div>
)
}
export default Chat
Your selector is trying to access the wrong field.
export const selectSelectedImage = (state) => state.app.selectImage;
Should actually be:
export const selectSelectedImage = (state) => state.app.selectedImage;
as your state has selectedImage field and not selectImage.
I am trying to implement Typescript and Context API together in an application. In that case I am trying to make the Context API for the login.
This is the error what I get:
Error: Rendered more hooks than during the previous render.
I am not sure what I did wrong, here is my code:
StateProvider.tsx:
import React, {
Reducer,
Dispatch,
createContext,
useContext,
useReducer,
} from "react";
import { initialState, LoginState } from "./reducer";
export type StateContextType={
state: unknown;
dispatch({}):void;
}
export const StateContext = createContext<StateContextType>({state: {}, dispatch: ()=>{}});
interface IProvider{
reducer:any;
initState:typeof initialState;
children:any;
}
export const StateProvider:React.FC<IProvider> = ({
reducer,
initState,
children,
}) => {
const [state, dispatch] = useReducer(reducer, initState);
const value = { state, dispatch };
return (
<StateContext.Provider value={value}>{children}</StateContext.Provider>
);
};
export const useStateValue = () => useContext(StateContext);
reducer.tsx:
import React, { useState } from "react";
import axios from "./axios";
import { Button, TextField } from "#material-ui/core";
import { Link, useHistory } from "react-router-dom";
import { useStateValue } from './StateProvider'
export const initialState: LoginState = {
user: null,
};
export interface LoginState {
user: string | object | null;
}
type LoginAction = { type: "SET_USER"; payload: string };
function reducer(state: LoginState, action: LoginAction) {
switch (action.type) {
case "SET_USER":
return {
...state,
user: action.payload,
};
default:
return state;
}
}
export default function LoginUseReducer() {
const {state, dispatch} = useStateValue();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const history = useHistory();
const loginHandler = (e: React.FormEvent) => {
e.preventDefault();
const data = {
Email: email,
Password: password,
};
axios
.put("/auth", data)
.then((response) => {
console.log(response);
dispatch({ type: "SET_USER", payload: response });
})
.catch((error) => {
console.log(error.message);
});
};
const handleEmailChange: React.ChangeEventHandler<HTMLInputElement> = (
event
) => {
setEmail(event.target.value);
};
const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = (
event
) => {
setPassword(event.target.value);
};
return (
<div>
<TextField
label="Email"
variant="standard"
helperText="Use your student email (JhonDoe#stud.uni-obuda.hu)"
value={email}
onChange={handleEmailChange}
style={{ width: "80%", marginBottom: 30 }}
></TextField>
<TextField
label="Password"
variant="standard"
type="password"
value={password}
onChange={handlePasswordChange}
style={{ width: "80%" }}
></TextField>
<div className="form_buttons" style={{ marginTop: 30 }}>
<Button
onClick={loginHandler}
style={{ fontSize: "large", padding: 15, width: 100 }}
>
Send
</Button>
</div>
</div>
);
}
Login.tsx:
import React from 'react';
import './Login.scss';
import LoginUseReducer, { initialState } from "../../reducer";
function Login() {
return (
<div>
{LoginUseReducer()}
</div>
)
}
export default Login;
If you guys have any idea, please let me know. Thanks for your time!
I created a codesandbox which works as expected, when LoginUseReducer is used as a component instead of function as the other user has pointed out. I also made few minor modifications to help catch issues like this.
LoginUseReducer needs to be rendered like a component and not called like a function
function Login() {
return (
<div>
<LoginUseReducer />
</div>
)
}
export default Login;
I am working on an authentication system using react at front. I am storing token which comes from my backend server to localStorage and i want user to redirect to dashboard page when there is a token present in localStorage. Every time i login using correct credentials i get token but not redirecting to dashboard page. But when i change route in url it works. I am using react context api.
AuthContext.js
import { createContext } from "react";
const AuthContext = createContext();
export default AuthContext;
AuthState.js
import React, { useReducer, useState } from "react";
import AuthContext from "./AuthContext";
import { SUCCESS_LOGIN } from "../types";
import AuthReducers from "./AuthReducers";
import Axios from "axios";
const AuthState = ({ children }) => {
//setting up initial state for authcontext
const initialState = {
userAuth: null,
userLoading: false,
token: localStorage.getItem("token"),
errors: null,
};
const [state, dispatch] = useReducer(AuthReducers, initialState);
//logging user in
const loginUser = async (userData) => {
const config = {
headers: {
"Content-Type": "application/json",
},
};
try {
//posting to api
const res = await Axios.post("/api/user/login", userData, config);
console.log(res.data);
dispatch({
type: SUCCESS_LOGIN,
payload: res.data,
});
} catch (error) {
console.log(error.response);
}
};
return (
<AuthContext.Provider
value={{
userAuth: state.userAuth,
errors: state.errors,
token: state.token,
loginUser,
}}
>
{children}
</AuthContext.Provider>
);
};
export default AuthState;
AuthReducers.js
import { SUCCESS_LOGIN } from "../types";
export default (state, action) => {
switch (action.type) {
case SUCCESS_LOGIN:
const token = action.payload.token;
localStorage.setItem("token", token);
return {
...state,
userAuth: true,
userLoading: true,
errors: null,
token: localStorage.getItem("token"),
};
default:
return state;
}
};
Login.js
import React, { useState, useContext } from "react";
import { useHistory } from "react-router-dom";
import { Button, Form, FormGroup, Label, Input, FormText } from "reactstrap";
import styles from "./login.module.css";
import AuthContext from "../../context/AuthContext/AuthContext";
const Login = (props) => {
//grabbing states from authContext
const { loginUser, userAuth } = useContext(AuthContext);
let history = useHistory();
const [credentials, setCredentials] = useState({
email: "",
password: "",
});
//pulling email and password from state
const { email, password } = credentials;
//method to handle changes on input fields
const handleChange = (e) => {
const { name, value } = e.target;
setCredentials({
...credentials,
[name]: value,
});
};
//method to handle login when user submits the form
const handleLogin = (e) => {
e.preventDefault();
loginUser({ email, password });
console.log(userAuth);
if (userAuth) {
history.push("/dashboard");
}
};
return (
<Form onSubmit={handleLogin}>
<FormGroup>
<Label for="email">Email</Label>
<Input
type="email"
name="email"
value={email}
placeholder="Enter your email"
onChange={handleChange}
/>
</FormGroup>
<FormGroup>
<Label for="password">Password</Label>
<Input
type="password"
name="password"
value={password}
placeholder="Enter password"
onChange={handleChange}
/>
</FormGroup>
<Button className={styles.loginBtn}>Submit</Button>
</Form>
);
};
export default Login;
PrivateRoute.js
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import AuthContext from "../../context/AuthContext/AuthContext";
const PrivateRoute = ({ component: Component, ...rest }) => {
const { token, userAuth } = useContext(AuthContext);
return (
<div>
<Route
{...rest}
render={(props) =>
token ? <Component {...props} /> : <Redirect to="/" />
}
/>
</div>
);
};
export default PrivateRoute;
You need to do this in Login.js.
useEffect(() => {
if (userAuth) {
history.push("/dashboard");
}
},[userAuth,history])
Its happening because when you do handleLogin click functionality you dont have userAuth at that time as true(its taking previous value). Because context update change is not available in handleLogin function . Instead track userAuth in useEffect
If you are trying to redirect the user after successful login via your handleLogin() function, it won't work because of this:
if (userAuth) {
history.push("/dashboard");
}
The above will not run, because userAuth won't change until the component re-renders, after which, the function will have finished executing. You should either return something from your loginUser() action, and redirect based on its return of a successful "login", or implement conditional rendering inside of the Login component, like so:
return userAuth
? <Redirect to="/dashboard" /> // redirect if userAuth == true
: (
// your Login JSX // if userAuth == false, render Login form
)