I am trying to set up authenticated routes in react and have a useState hook to change to true once the user is authenticated. When I get the user data from my server, I can update the current user information but the useState hook for my authentication will not change from true to false and vice versa.
import { useState, useEffect, useContext } from "react";
import { UserContext } from "./UserContext";
import ApiHandler from "../ApiHandler/ApiHandler";
const api = new ApiHandler();
export const useAuth = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const userContext = useContext(UserContext);
const { currentUser, setCurrentUser } = userContext;
useEffect(() => {
api
.get("/is-loggedin")
.then(res => {
setCurrentUser(res.data.currentUser);
setIsLoggedIn(true);
})
.catch(err => {
setCurrentUser(null);
setIsLoggedIn(false);
});
}, [setCurrentUser]);
return { isLoggedIn, currentUser };
};
I think the problem is useEffect 2nd argument dependencies array does not have correct dependencies.
useEffect(() => { /* rest of the code */ }, [setCurrentUser]);
change to
useEffect(() => { /* rest of the code */ }, [setCurrentUser, isLoggedIn]);
Declare your method inside the hook, and then call. Add the three lines having comments. Also currentUser is the dependency because if user changes then will you check if it's logged-in. Your mistakes were
1)- passing setCurrentUser (it will never change as it's a function/handler) in dependency.
2)- neither declared the method nor called it, instead just passing statements to useEffect, which only runs once.
import { useState, useEffect, useContext } from "react";
import { UserContext } from "./UserContext";
import ApiHandler from "../ApiHandler/ApiHandler";
const api = new ApiHandler();
export const useAuth = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const userContext = useContext(UserContext);
const { currentUser, setCurrentUser } = userContext;
useEffect(() => {
function getStatus() { //method body starts
api.get("/is-loggedin")
.then(res => {
setCurrentUser(res.data.currentUser);
setIsLoggedIn(true);
})
.catch(err => {
setCurrentUser(null);
setIsLoggedIn(false);
});
}; //method body closed.
getStatus(); //this is the call
}, [currentUser]);
return { isLoggedIn, currentUser };
};
Just put an empty array to useEffect
useEffect(() => {
api
.get("/is-loggedin")
.then(res => {
setCurrentUser(res.data.currentUser);
setIsLoggedIn(true);
})
.catch(err => {
setCurrentUser(null);
setIsLoggedIn(false);
});
}, []);
Related
I'm learned that React will re-render after state changed e.g. setState from useState(), calling the function or variable from useContext() variable. But now I'm don't understand that why I get the ESLint warning call the context function inside the useCallback() without dependency in the list. If I put the dependency in the list, useCallback() will be re-rendered and useEffect() dependency from useCallback() variable will do again. So how to fix the react-hooks/exhaustive-deps when calling the function inside the useContext() variable?
Auth.js
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "../Page/Loading"
const AuthContext = createContext()
export const AuthProvider = ({children}) => {
const [user,setUser] = useState()
const [loadingInitial,setLoadingInitial] = useState(true)
useEffect(()=>{
AuthAPI.getCurrentUser()
.then((user)=>setUser(user))
.catch((error)=>{console.log(error)})
.finally(()=>setLoadingInitial(false))
},[])
const login = async (email,password) => {
const user = await AuthAPI.login({email,password})
setUser(user)
return user
}
const register = async (firstname,lastname,email,password) => {
const user = await AuthAPI.register({firstname,lastname,email,password})
setUser(user)
return user
}
const logout = async () => {
const response = await AuthAPI.logout()
setUser(undefined)
}
const value = useMemo(()=>({
user,
setUser,
login,
register,
logout
}),[user])
return (
<AuthContext.Provider value={value}>
{loadingInitial ? <Loading/> : children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
return useContext(AuthContext)
}
Logout.js
import { useCallback, useEffect, useState } from "react";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../Hooks/Auth";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "./Loading";
function Logout() {
const auth = useAuth()
const location = useLocation()
const navigate = useNavigate()
const [isLoggedOut,setIsLoggedOut] = useState(false)
const logout = useCallback(async () => {
console.log("Logging out!")
await AuthAPI.logout()
auth.setUser((prevState)=>(undefined))
setIsLoggedOut(true)
},[auth]) // --> re-rendered bacause `auth` context in re-rendered when set `user` state.
useEffect(()=>{
logout()
},[logout]) // --> this also to run again from `logout` callback is being re-rendered.
if (!isLoggedOut) {
return <Loading/>
}
return (
<Navigate to="/login" replace/>
)
}
export default Logout
Any help is appreciated.
How about destructuring your auth context, since you are only using setUser inside useEffect?
const { setUser } = useAuth()
useEffect(() => {
....
}, [setUser])
There is no need for creating a memoized logout callback function if logout isn't used/passed as a callback function. Just apply the logging out logic in the useEffect hook.
Render the Loading component and issue the imperative redirect from the resolved Promise chain of the return AuthAPI.logout Promise.
Example:
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../Hooks/Auth";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "./Loading";
function Logout() {
const auth = useAuth();
const navigate = useNavigate();
useEffect(() => {
console.log("Logging out!");
AuthAPI.logout()
.then(() => auth.setUser(undefined))
.finally(() => navigate("/login", { replace: true }));
}, []);
return <Loading />;
}
export default Logout;
Can you try to replace your useEffect code into this:
useEffect(logout, [])
When using "await" on "dispatch(saveItem(item))" it's not supposed to have any effct ,
meanwhile if i don't use the "await" both functions will run in the same time resulting a saved item but not a component rerender.
Although the state changes in the redux store the view doesn't,
whilst using the await actually waits for the dispatch to complete and then runs the navigation.
My main question is how to properly navigate after a redux dispatch?
import { useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from '../hooks/useForm';
import { getById } from '../services/itemService';
import { saveItem } from '../store/actions/itemActions';
export function ItemEdit() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [item, handleChange, setItem] = useForm(null);
const itemId = useParams().id;
useEffect(async () => {
await loadItem();
}, []);
const loadItem = async () => {
try {
const item = await getById(itemId)
setItem(item);
} catch(err) {
setErrMsg(err.name + ': ' + err.message);
}
};
const onSaveItem = async (ev) => {
ev.preventDefault();
await dispatch(saveItem(item));
navigate('/item')
}
return (
<form onSubmit={onSaveItem}>
<button>Save</button>
</form>
);
}
You can try it this way:
dispatch(saveItem(item))
.unwrap()
.then(() => navigate('/item'))
.catch(error => 'handle error')
It works for me.
Goal: Fetch data from api then assign it to a state for further processing.
Issue: After setting the data to my useState it is still undefined.
Questions:
How would one solve this problem?
Am I misunderstanding the useState hook?
import "./styles.css";
import axios from "axios";
import { useEffect, useState } from "react";
export default function App() {
const [userData, setUserData] = useState();
const functionz = () => {
return axios
.get("https://randomuser.me/api/")
.then(({ data }) => data.results);
};
useEffect(async () => {
const data = await functionz();
setUserData(data);
}, []);
if (userData) {
console.log(userData);
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Edit to see some magic happen!</h2>
</div>
);
}
You have to make sure your function is returning the axios call. Then await whatever comes out of it in your useEffect. Then proceed to adding it to your state. See example.
import React, { useState, useEffect } from 'react'
import axios from "axios";
const Api = () => {
const [usersData, setUsersData] = useState(null)
const fetchRandomUserData = () => axios.get('the-url')
useEffect(() => {
fetchRandomUserData()
.then(resp => {
setUsersData(resp.data.results)
})
.catch(e => {
console.log('Error: ', e)
})
}, [])
console.log(usersData)
return <div></div>
}
export default Api
I found different already answered questions to my question, but the don't help.
I use a custom context to call the firebase.auth().onAuthStateChanged() and set the currentUser.
import React, { useState, useEffect } from "react";
import app from "../firebase";
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
app.auth().onAuthStateChanged(setCurrentUser);
}, []);
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
);
};
In my component I call the AuthContext and the currentUser:
import React, { useContext, useEffect, useState } from "react";
import app from "./firebase";
import { AuthContext } from "./Auth/Auth";
function MyComponent() {
const [invoices, setInvoices] = useState([]);
const { currentUser } = useContext(AuthContext);
const getInvoices = () => {
const database = app.firestore();
const unsubscribe = database
.collection("invoices")
.where("uid", "==", currentUser.uid) // HERE currentUser IS NULL
.orderBy("date", "desc")
.onSnapshot((snapshot) => {
setInvoices(
snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
);
});
return () => {
unsubscribe();
};
};
useEffect(() => {
getInvoices();
}, []);
return (<> ... </>);
}
export default MyComponent;
I believe my issue has something to do with promises and the user is not yet loaded. But still I don't know what to do here.
The potential issue could be the value of currentUser returns a bit later so you need to add an extra check in your MyComponent component.
I would add null check for currentUser and extend the dependency array as:
useEffect(() => {
if (currentUser) {
getInvoices();
}
}, [currentUser]);
Probably in the first round the useEffect callback was running once currentUser was still null.
I can use it with class component using this.auth, but how do I accomplish that using function based component and hooks
import React, { useEffect, useState } from 'react';
const GoogleAuth = () => {
const [ isSignedIn, setIsSignedIn ] = useState(null);
useEffect(() => {
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: '#ID',
scope: 'email'
})
.then(() => {
//THIS AUTH VARIABLE
const auth = window.gapi.auth2.getAuthInstance();
setIsSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get());
auth.isSignedIn.listen(onAuthChange);
});
});
}, []);
const onAuthChange = () => {
setIsSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get());
};
The hook equivalent of this.auth would be useRef:
import React, { useEffect, useState, useRef } from 'react';
const GoogleAuth = () => {
const auth = useRef()
const [ isSignedIn, setIsSignedIn ] = useState(null);
useEffect(() => {
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: '#ID',
scope: 'email'
})
.then(() => {
auth.current = window.gapi.auth2.getAuthInstance();
setIsSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get());
auth.current.isSignedIn.listen(onAuthChange);
});
});
}, []);
const onAuthChange = () => {
setIsSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get());
};
However since this is auth data, you might also be interested to look at useContext or maybe having a custom hook that will store it in the sessionStorage or localStorage.