React Navigate to page based on useEffect dependency - reactjs

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.

Related

React Context API current

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.

firebase onAuthStateChanged executes while sign-in state doesn't change, it also returns null before returning user

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;
}, []);

React hooks useState getting diferrent value from redux state

I have react component look like this following code:
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";
import { createClient, getClients } from "../redux/actions/clients";
function UpdateClient(props) {
let params = useParams();
const { error, successSubmit, clients } = useSelector(
(state) => state.clients
);
const [client, setClient] = useState(clients[0]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getClients({ id: params.id }));
}, []);
const submitClient = () => {
dispatch(createClient(client));
};
return (
<div>{client.name} {clients[0].name}</div>
);
}
export default UpdateClient;
And the result is different client.name return test1,
while clients[0].name return correct data based on route parameter id (in this example parameter id value is 7) which is test7
I need the local state for temporary saving form data. I don't know .. why it's become different?
Can you please help me guys? Thanks in advance
You are referencing a stale state which is a copy of the clients state.
If you want to see an updated state you should use useEffect for that.
useEffect(() => {
setClient(clients[0]);
}, [clients]);
Notice that duplicating state is not recommended.
There should be a single “source of truth” for any data that changes in a React application.

Reset context from helper service

I have created a service class that I use to handle authentication in my React app. Data about the current logged in user is stored in the user context. When a user clicks the logout button, logout() is called in the authencation service. The user context can then be reset in the same click event with this.context.updateUser().
But, there are times where a user needs to be logged out even if they didn't click the logout button, for example if their session is invalid. In this case forceLogout() would be called from inside the authentication service. However, because this does not occur within a component this leaves me with no way to reset the user context. The current code gives the error: Error: Invalid hook call. Hooks can only be called inside of the body of a function component..
How can I reset the user context from inside forceLogout()?
// user.context.js
import React, { createContext, Component } from 'react';
import AuthService from '../services/auth.service';
export const UserContext = createContext();
class Context extends Component {
state = {
user: AuthService.getCurrentUser(),
updateUser: (user) => {
this.setState({ user });
}
};
render() {
return (
<UserContext.Provider value={this.state}>
{this.props.children}
</UserContext.Provider>
);
}
}
export default Context;
// auth.service.js
import { useContext } from 'react';
import { UserContext } from '../contexts/user.context';
import history from '../history';
Class AuthService {
getCurrentUser() {
return JSON.parse(localStorage.getItem('user'));
}
logout() {
localStorage.removeItem('user');
}
forceLogout() {
const userContext = useContext(UserContext);
localStorage.removeItem('user');
userContext.updateUser();
history.push('/');
}
...
}
Create a Logout component that will be common for all types of logout, and apply your logout logic inside componentDidMount or useEffect of the Logout component, since it is a component you can use hooks here, and whenever you want to logout just redirect to the Logout component and if you want to redirect back to the homepage after logout do this inside Logout component.
You can use custom events:
Class AuthService {
// ...
forceLogout() {
localStorage.removeItem('user');
// Dispatch logout event.
const event = new Event('force-logout');
window.dispatchEvent(event);
}
}
Then inside a component under UserContext.Provider :
const SomeComponent = () => {
const userContext = useContext(UserContext);
useEffect(() => {
const handleLogout = () => {
userContext.updateUser();
history.push('/');
};
window.addEventListener('force-logout', handleLogout);
return () => window.removeEventListener('force-logout', handleLogout);
}, [userContext, history]);
// ...
}

createContext using a dynamic object

1. Static object
To create context based on a static object, I use this code:
import React, { createContext } from 'react';
const user = {uid: '27384nfaskjnb2i4uf'};
const UserContext = createContext(user);
export default UserContext;
This code works fine.
2. Dynamic object
But if I need to create context after fetching data, I use this code:
import React, { createContext } from 'react';
const UserContext = () => {
// Let's suppose I fetched data and got user object
const user = {uid: '18937829FJnfmJjoE'};
// Creating context
const context = createContext(user);
// Returning context
return context;
}
export default UserContext;
Problem
When I debugg option 1, console.log(user) returns the object. Instead, option 2, console.log(user) returns undefined. What I'm missing?
import React, { useEffect, useState, useContext } from 'react';
import UserContext from './UserContext';
const ProjectSelector = (props) => {
const user = useContext(UserContext);
console.log(user);
return(...);
}
export default App;
one thing i would suggest is move this logic to a react component itself.
anhow you need to use a Provider in which you will set value to be the value consumers need to consume.useEffect is greatway to do asynchronous updates, like your requirment.
so , use a state variable as value of provider.in useEffect you fetch the data and update the state variable which in turn will update context value.
following is the code
UserContext.js
import { createContext } from "react";
const UserContext = createContext();
export default UserContext;
App.js
export default function App() {
const [user, setUser] = useState();
useEffect(() => {
console.log("here");
fetch("https://reqres.in/api/users/2")
.then(response => {
return response.json();
})
.then(data => {
setUser(data);
});
}, []);
return (
<div className="App">
<UserContext.Provider value={user}>
<DummyConsumer />
</UserContext.Provider>
</div>
);
}
DummyConsumer.js
import React, { useContext } from "react";
import UserContext from "./UserContext";
const DummyConsumer = () => {
const dataFromContext = useContext(UserContext);
return <div>{JSON.stringify(dataFromContext)}</div>;
};
export default DummyConsumer;
demo:anychronus context value providing

Resources