Okay...what is happening here cause I don't undrestand? I have an react context api where I store the current user data. In the login function I console log the currentUser and everything looks fine, but then when I write the currentUser in chrome dev tools, it appears undefined. In localStorage I also have the data. What is happening? What am I doing wrong? Can someone, please help me.
Here is my code:
authContext.js
import { createContext, useState, useEffect } from "react";
import axios from "axios";
export const AuthContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(
JSON.parse(localStorage.getItem("user")) || null
);
const login = async (inputs) => {
try {
const res = await axios.post("/login", inputs);
setCurrentUser(res.data);
console.log("res.data: ", res.data); //returns data
console.log("currentUser ", currentUser); //returns data
} catch (error) {
console.log(error);
}
};
const logout = async () => {
localStorage.clear();
setCurrentUser(null);
};
useEffect(() => {
localStorage.setItem("user", JSON.stringify(currentUser));
}, [currentUser]);
return (
<AuthContext.Provider value={{ currentUser, login, logout }}>
{children}
</AuthContext.Provider>
);
};
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { AuthContextProvider } from "./ccontext/authContext";
import App from "./App";
import "./index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</React.StrictMode>
);
Login.jsx
/*...*/
const LoginForm = () => {
const [error, setError] = useState(null);
const navigate = useNavigate();
const { login } = useContext(AuthContext);
const handleFormSubmit = (values, actions) => {
try {
login(values);
actions.resetForm();
navigate("/");
console.log("logged in");
} catch (err) {
setError(err.response.data);
}
};
/*...*/
Updating state is best seen like an asynchronous operation. You cannot set state in a function / effect and expect it to immediately be updated, on the spot. Well, it technically is, but you won't see the updates in your "already-running" function.
I am pretty sure that if you extract your log in the component root it will display the appropriate value after the login function finishes executing and the component properly sets the new state.
If you do need the value in the function you should directly use res.data.
A deeper dive:
Whenever your login function runs it forms a closure around your current values (including currentUser which is undefined at the moment).
When you update the currentUser in the login function you basically inform react that you need to update that value. It will handle this in the background, preparing the state for the next render, but your login function will keep running with whatever values it started with. Your "new" state values will not be available until you run the function again. This is because the already-running function "closed over" old values, so it can only reference those.
As a side note, if you use a ref for instance you would not have this problem. Why? Because refs do not participate in the react lifecycle. When you modify a ref it changes on the spot. You will have the updated value precisely on the next line. State does not work like that, it is coupled to the component lifecycle, so it will update on the next render.
Related
I'm using GoTrue-JS to authenticate users on a Gatsby site I'm working on and I want the homepage to route users to either their user homepage or back to the login page.
I check the existence of a logged-in user in a Context layer then define a state (user) that is evaluated on the homepage with a useEffect hook with the state as the dependency.
The expected behavior is that the useEffect hook will trigger the check for a user once the function is completed and route the user. But what happens is that the hook seems to check without the user state getting changed which routes the user to the login page.
Here's an abridged version of the code:
context.js
import React, {
useEffect,
createContext,
useState,
useCallback,
useMemo,
} from "react";
import GoTrue from 'gotrue-js';
export const IdentityContext = createContext();
const IdentityContextProvider = (props) => {
//create the user state
const [user, setUser] = useState(null);
//init GoTrue-JS
const auth = useMemo(() => {
return new GoTrue({
APIUrl: "https://XXXXXX.netlify.app/.netlify/identity",
audience: "",
setCookie: true,
});
},[]);
//get the user if they are signed in
useEffect(() => {
setUser(auth.currentUser());
},[auth]);
return (
<IdentityContext.Provider value={{ auth,user }}>
{props.children}
</IdentityContext.Provider>
)
}
export default IdentityContextProvider;
index.js
import { navigate } from 'gatsby-link';
import { useContext, useEffect } from 'react'
import { IdentityContext } from '../contexts/IdentityContext';
export default function HomePage() {
const { user } = useContext(IdentityContext);
useEffect(() => {
if (user) {
navigate("/user/home");
console.log("there's a user");
} else {
navigate("/login");
console.log("no user");
}
}, [user]);
return null
}
When I remove the navigate functions I see no user, then there's a user in the log when I load the homepage. I thought the useEffect hook would only fire if the state I listed in the dependency array (user) was changed. If there's no user then auth.currentUser() will return null and if there is one, then I will get all the user data.
Why are you using the user as a dependency? Just use the useEffect with an empty dependency. Also if you want to block the view while the processing, make a state as isLoading ( bool) and conditional render with it
!isLoading ?
<></>
:
<h1>Loading..</h1>
Here's the solution: Netlify's gotrue-js will return null for currentUser() if there is no user signed in so I need to first declare my user state as something other than null then set my conditional to detect null specifically so my app knows the check for a signed in user occurred.
context.js
import React, {
useEffect,
createContext,
useState,
useCallback,
useMemo,
} from "react";
import GoTrue from 'gotrue-js';
export const IdentityContext = createContext();
const IdentityContextProvider = (props) => {
//create the user state
//set either to empty string or undefined
const [user, setUser] = useState("");
//init GoTrue-JS
const auth = useMemo(() => {
return new GoTrue({
APIUrl: "https://XXXXXX.netlify.app/.netlify/identity",
audience: "",
setCookie: true,
});
},[]);
//get the user if they are signed in
useEffect(() => {
setUser(auth.currentUser());
},[auth]);
return (
<IdentityContext.Provider value={{ auth,user }}>
{props.children}
</IdentityContext.Provider>
)
}
export default IdentityContextProvider;
index.js
import { navigate } from 'gatsby-link';
import { useContext, useEffect } from 'react'
import { IdentityContext } from '../contexts/IdentityContext';
export default function HomePage() {
const { user } = useContext(IdentityContext);
useEffect(() => {
if (user) {
navigate("/user/home");
console.log("there's a user");
} else if (user == null) {
navigate("/login");
console.log("no user");
}
}, [user]);
return null
}
There was a similar question regarding Firebase where they were also getting no user on load even when one was signed in because of the state. The accepted answer is doesn't provide a snippet so it's gone to the wisdom of the ancients, but I was able to work with another engineer to get this solution.
I'm learning firebase authentication in react. I believed onAuthStateChanged only triggers when the user sign-in state changes. But even when I go to a different route or refresh the page, it would still execute.
Here is my AuthContext.js
import React, {useContext,useEffect,useState} from 'react';
import {auth} from './firebase';
import { createUserWithEmailAndPassword, onAuthStateChanged, signInWithEmailAndPassword,
signOut } from "firebase/auth";
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({children}) {
const [currentUser,setCurrentUser] = useState();
const [loading,setLoading] = useState(true);
useEffect(()=>{
const unsub = onAuthStateChanged(auth,user=>{
setLoading(false);
setCurrentUser(user);
console.log("Auth state changed");
})
return unsub;
},[])
function signUp(email,password){
return createUserWithEmailAndPassword(auth,email,password)
}
function login(email,password){
return signInWithEmailAndPassword(auth,email,password);
}
function logout(){
return signOut(auth);
}
const values = {
currentUser,
signUp,
login,
logout
}
return <AuthContext.Provider value={values}>
{!loading && children}
</AuthContext.Provider>;
}
I put onAuthStateChanged in useEffect(), so every time the component renders the code inside will run. But why would onAuthStateChanged() still run when user sign-in state does not change? I'm asking this question because it created problems.
onAuthStateChanged would first return a user of null. If user is already authenticated, it would return the user a second time. Because of the first "null" user, my other page would not work properly. For example, I have this private router that would always redirect to the login page even when the user is authenticated.
My Private Route
import React from 'react';
import {Route, Navigate} from "react-router-dom";
import { useAuth } from './AuthContext';
export default function Private() {
const {currentUser} = useAuth();
return <div>
{currentUser ? <>something</>:<Navigate to="/login"/>}
</div>;
}
if onAuthStateChanged doesn't trigger when I don't sign in or log out, I wouldn't have the problems mentioned above
I don't think the issue is with onAuthStateChange per se, but rather the fact that you're setting loading to false first, and only setting the current user afterwards. While react attempts to batch multiple set states together, asyncronous callbacks which react is unaware of won't be automatically batched (not until react 18 anyway).
So you set loading to false, and the component rerenders with loading === false, and currentUser is still on its initial value of undefined. This then renders a <Navigate>, redirecting you. A moment later, you set the currentUser, but the redirect has already happened.
Try using unstable_batchedUpdates to tell react to combine the state changes into one update:
import React, { useContext, useEffect, useState } from "react";
import { unstable_batchedUpdates } from 'react-dom';
// ...
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
unstable_batchedUpdates(() => {
setLoading(false);
setCurrentUser(user);
});
console.log("Auth state changed");
});
return unsub;
}, []);
I have a provider wrapper around some routes
<Provider>
<Route path={ROUTES.SIGNING}><SignIn /></Route>
<PrivateRoute path={ROUTES.PRIVATE}><Private /></PrivateRoute>
</Provider>
The Provider is simply a wrapper for a userContext
import React, { useState } from 'react';
import UserContext from '../../user.context';
let defaultUser = '';
try {
defaultUser = JSON.parse(localStorage.getItem('profile'));
} catch {
defaultUser = '';
}
function Provider(props) {
const [user, setUser] = useState(defaultUser);
return <UserContext.Provider value={{ user, setUser }}>{props.children}</UserContext.Provider>
}
export default Provider;
My <SignIn /> Component waits for a response from a data service then 1. attempts to update the setter from userContext and then trys to update it's own useState function. It never seems to execute the internal useState function.
function SignIn() {
const { user, setUser } = useContext(UserContext);
const [formStatus, setFormStatus] = useState();
async function handleSubmit(e) {
e.preventDefault();
const result = await signin(credentials);
setUser({ isInternal: result.isInternal, clientId: result.clientId });
setFormStatus((curStatus) => ({ ...curStatus, state: FORMSTATUS.COMPLETED }));
}
Why would the setStatus never seem to fire? I think it's because setUser updates the Provider which is a higher level component than the child SignIn Page. Any help would be great
You have a race condition. To resolve, you need to specify that your local state update should complete before the context state update.
Solution:
Use React's functional form to set local state.
Then call your context state update within the functional set state call.
This way, you know the local state has completed before you update context.
I have the following React component which i based on this documentation on Auth0 Website
here is my code i ommited some unrelated lines
import * as Auth0 from "#auth0/auth0-react";
import * as React from "react";
import * as ReactRedux from 'react-redux';
interface IProps{.....}
const AppComponent: React.SFC<IProps> = (props) => {
const { getAccessTokenSilently } = Auth0.useAuth0();
React.useEffect(() => {
(async () => {
try{
const token = await getAccessTokenSilently();
console.log(token);
Props.setAccessToken(token); // this is a dispatch to save the token into redux state
}
catch(error){console.log(error);}
})();
}, [getAccessTokenSilently]);
return(
<> /*some jsx */ </>
);
}
const mapDispatchToProps //standard redux mapping
const mapStateToProps // standard redux mapping
export default ReactRedux.connect(mapStateToProps,mapDispatchToProps)(AppComponent);
the behavior i am getting is that once the component renders it logs the token to the console however after the token expiry the useffect is not happening to get a new token which is what i am trying to achieve any idea what could be wrong or if there is a better way to achieve the result of updating access token on expiry with out it being tightly coupled to specific action.
Edit1: i missed adding that having the token refreshed on intervals is not possible this would ship as a whole product for different clients and each client has their own Auht0 tenant i cannot assume a specific token life span
I use a hook I made called useInterval to update the token in the background at specific intervals.
useInterval.ts
import { useCallback, useEffect } from "react";
export const useInterval = (func: () => void, delay: number, startImmediately: boolean, funcDeps: React.DependencyList) => {
const callback = useCallback(func, funcDeps);
useEffect(() => {
const handler = setInterval(callback, delay);
startImmediately && callback();
return () => clearInterval(handler);
}, [callback, delay, startImmediately]);
}
Usage in app component
useInterval(() => {
// code to update access token
}, 300000, true, [dependencies]);
It is my first application using react context with hooks instead of react-redux and would like to get help of the structure of the application.
(I'm NOT using react-redux or redux-saga libraries.)
Context
const AppContext = createContext({
client,
user,
});
One of actions example
export const userActions = (state, dispatch) => {
function getUsers() {
dispatch({ type: types.GET_USERS });
axios
.get("api address")
.then(function(response) {
dispatch({ type: types.GOT_USERS, payload: response.data });
})
.catch(function(error) {
// handle error
});
}
return {
getUsers,
};
};
Reducer (index.js): I used combineReducer function code from the redux library
const AppReducer = combineReducers({
client: clientReducer,
user: userReducer,
});
Root.js
import React, { useContext, useReducer } from "react";
import AppContext from "./context";
import AppReducer from "./reducers";
import { clientActions } from "./actions/clientActions";
import { userActions } from "./actions/userActions";
import App from "./App";
const Root = () => {
const initialState = useContext(AppContext);
const [state, dispatch] = useReducer(AppReducer, initialState);
const clientDispatch = clientActions(state, dispatch);
const userDispatch = userActions(state, dispatch);
return (
<AppContext.Provider
value={{
clientState: state.client,
userState: state.user,
clientDispatch,
userDispatch,
}}
>
<App />
</AppContext.Provider>
);
};
export default Root;
So, whenever the component wants to access the context store or dispatch an action, this is how I do from the component :
import React, { useContext } from "react";
import ListMenu from "../common/ListMenu";
import List from "./List";
import AppContext from "../../context";
import Frame from "../common/Frame";
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
return (
<Frame>
<ListMenu
dataList={userState.users}
selectDataList={selectUserList}
/>
<List />
</Frame>
);
};
export default Example;
The problem I faced now is that whenever I dispatch an action or try to access to the context store, the all components are re-rendered since the context provider is wrapping entire app.
I was wondering how to fix this entire re-rendering issue (if it is possible to still use my action/reducer folder structure).
Also, I'm fetching data from the action, but I would like to separate this from the action file as well like how we do on redux-saga structure. I was wondering if anybody know how to separate this without using redux/redux-saga.
Thanks and please let me know if you need any code/file to check.
I once had this re-rendering issue and I found this info on the official website:
https://reactjs.org/docs/context.html#caveats
May it will help you too
This effect (updating components on context update) is described in official documentation.
A component calling useContext will always re-render when the context value changes. If re-rendering the component is expensive, you can optimize it by using memoization
Possible solutions to this also described
I see universal solution is to useMemo
For example
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
const users = userState.users;
return useMemo(() => {
return <Frame>
<ListMenu
dataList={users}
selectDataList={selectUserList}
/>
<List />
</Frame>
}, [users, selectUserList]); // Here are variables that component depends on
};
I also may recommend you to completly switch to Redux. You're almost there with using combineReducers and dispatch. React-redux now exposes useDispatch and useSelector hooks, so you can make your code very close to what you're doing now (replace useContext with useSelector and useReducer with useDispatch. It will require minor changes to arguments)