I want to recive the state isAuthenticated in Home.js but when I change it before , in Login.js to true (I saw it in useEffect -->true) , the useAppContext() in Home.js return the old state -->false (the Old one in App.js) not the Last change in Login?
//----------------------in App.js----------------------
var [isAuthenticated, userHasAuthenticated] = useState('false');
<AppContext.Provider value={{ isAuthenticated, userHasAuthenticated }}>
//...
</AppContext.Provider>
//-------------- in Login.js---------------------
const { isAuthenticated, userHasAuthenticated } = useAppContext();
useEffect(() => {
console.log(isAuthenticated); // true
}, [isAuthenticated]);
const login = () => {
userHasAuthenticated('true');
window.location.assign("/Home");
}
// ---------------------in Home.js------------------------
const { isAuthenticated,userHasAuthenticated } = useAppContext();
console.log(isAuthenticated);// --->false ?
Related
I built a custom hook to get a user from firebase:
export function useAuth() {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return currentUser;
}
In my components I use it as:
const currentUser = useAuth();
useEffect(() => {
const localScore = JSON.parse(localStorage.getItem("score"));
setPush(!(localScore[currentUser.name] === tasks.length));
// ^ TypeError: Cannot read properties of undefined (reading 'name')
}, []);
I think I get the error, because when the components gets rendered the first time, there is no currentUser. But I load the component after the user logged in (what sets the user), thus I am confused.
I think I need to await the user somehow, but how?
From the comments:
I moved my onAuthStateChanged to a context, but this is checking the login state only once and is not updating:
const AppContext = createContext(null);
export function AppWrapper({ children }) {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return (
<AppContext.Provider value={currentUser}>{children}</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
function MyApp({ Component, pageProps }) {
const currentUser = useAppContext();
return (
<>
<AppWrapper>
{Component.secure && !currentUser ? (
<Login />
) : (
<>
<Component {...pageProps} />
</>
)}
</AppWrapper>
</>
);
}
I have Login functionality as below -
function SignIn() {
const loginInfo = useSelector(state => state.loginDetails);
const iLoginCreds ={
userName:'',
password:'',
isLoggedIn:false
}
const dispatch = useDispatch();
const [loginCreds, setLoginCredentials] = useState(iLoginCreds)
useEffect(() => {
alert("state changed : "+loginCreds.isLoggedIn);
}, [loginCreds])
function checkIfSignedIn()
{
axios.get(`https://localhost:44301/api/login/ValidateLogin`)
.then(res=>{
console.log(JSON.stringify(res.data));
setLoginCredentials({
...loginCreds,
isLoggedIn:res.data
});
dispatch(StoreUserAuthenticationStatusAction(res.data));
});
}
if(loginInfo.isLoggedIn==true)
{
return (
<MainPage></MainPage>
)
}
else
{
return (
...
...
<FormGroup>
<Button style={{width:'100%',backgroundColor:"#FCB724",color:"black",fontWeight:"bold"}} onClick={checkIfSignedIn} >Sign in using our secure server</Button>
</FormGroup>
)
}
Reducer Index :-
import { combineReducers } from "redux";
import {SaveLoginStatusReducer} from "./LoginReducers"
export const reducers=combineReducers({
loginDetails:SaveLoginStatusReducer
})
Issue -
When if(loginInfo.isLoggedIn==true) , which I am fetching from useSelector at the beginning , I want to render to MainPage. But somehow cannot see , data is been fetched from central store. Page is not getting rendered to MainPage eventhough the state is been updated.
I am able to get alert for useEffect I have used when state changes. It shows "true".
EDIT 1 :-
When I am using if(loginCreds.isLoggedIn==true) , I am able to see MainPage , but when I try to retrieve it from store through if(loginInfo.isLoggedIn==true) , I dont get true.
Edit 2 :-
Action.js -
export const StoreUserAuthenticationStatusAction=(loginPayload)=>{
return {
type:'SaveLoginStatus',
payload:loginPayload
}
}
export const SetProductList=(productListPayload)=>{
return {
type:'SetProductList',
payload:productListPayload
}
}
Reducer.js -
const iLoginCreds ={
userName:'',
password:'',
isLoggedIn:false
}
export const SaveLoginStatusReducer =(state=iLoginCreds,action)=>{
switch (action.type) {
case 'SaveLoginStatus':
return {
...state,
user:action.paylod
}
break;
default:
return state;
}
}
Code pattern looks bad, you have to make authGuard to protect private routes.
That should redirects to auth page if the user is not signed in.
Redirects to main page after sign in.
const handleLogin = (e) => {
e.preventDefault();
if (user.email === email && user.password === password) {
login();
history.push("/main-page");
}
};
<Route
{...rest}
render={(props) =>
isLogin() ? <Component {...props} /> : <Redirect to="/login" />
}
/>
I have a functional component that sets state with an onSubmit function. The problem is that the setState function won't update the user state in time for it to be passed down to the user prop before the authenticated state is changed to true. Here is the basic gist of my component:
import React, { useState } from 'react';
import axios from 'axios';
import { LoginForm } from './LoginForm';
import { LoggedIn } from './LoggedIn';
const { login } = require('../utils/login');
const { getUser } = require('../utils/getUser');
export const Splash = () => {
const [user, setUser] = useState(null);
const [authenticated, setAuthenticated] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const _handleEmail = (e) => {
setEmail(e.target.value);
};
const _handlePass = (e) => {
setPassword(e.target.value);
};
const _handleSubmit = async (e) => {
e.preventDefault();
const token = await login(email, password);
const fetchUser = await getUser(token);
setAuthenticated(true);
setUser(fetchUser);
console.log(fetchUser) <---- logs user fine
console.log(user) <----- logs empty object
};
const _handleLogout = async (e) => {
const logout = await
axios.get('http://localhost:5000/api/v1/auth/logout');
console.log(
`Logout status: ${logout.status}, Success: ${logout.data.success}`
);
setAuthenticated(false);
setUser({});
};
return (
<div>
{!authenticated ? (
<LoginForm
handleEmail={_handleEmail}
handlePass={_handlePass}
handleSubmit={_handleSubmit}
/>
) : (
<LoggedIn
user={user}
authenticated={authenticated}
logout={_handleLogout}
/>
)}
</div>
);
};
For whatever reason, my setAuthenticated function will change the authenticated state from false to true, hence triggering the render of <LoggedIn />, but my user state will not update in time to be passed down to <LoggedIn /> through the user prop. I suspect the authenticated prop I send down isn't updated either. Thoughts?
Thanks!
EDIT: Here's my <LoggedIn /> component and it's <LoggedInNav />component.
/// LoggedIn.js
export const LoggedIn = ({ logout, user, authenticated }) => {
LoggedIn.propTypes = {
logout: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
authenticated: PropTypes.bool.isRequired
};
const [loggedInUser, setUser] = useState({});
const [loggedInAuth, setAuthenticated] = useState(false);
useEffect(() => {
try {
if (user) {
setAuthenticated(true);
setUser(user);
}
} catch (err) {
console.log(err);
}
}, []);
return (
<div>
{!authenticated ? (
<Spinner animation='border' variant='primary' />
) : (
<LoggedinNav navUser={user} logoutButton={logout} />
)}
</div>
);
};
// LoggedInNav.js
export const LoggedinNav = ({ navUser, logoutButton }) => {
LoggedinNav.propTypes = {
navUser: PropTypes.object.isRequired,
logoutButton: PropTypes.func.isRequired
};
return (
<div>
<Navbar bg='light' variant='light'>
<Navbar.Brand href='#home'>
<img
src={logo}
width='120'
height='auto'
className='d-inline-block align-top ml-3'
alt='React Bootstrap logo'
/>
</Navbar.Brand>
<Nav className='mr-auto'>
<Nav.Link href='#feedback'>Submit Feedback</Nav.Link>
</Nav>
<Navbar.Collapse className='justify-content-end mr-4'>
<Navbar.Text>
Signed in as: {navUser.name.first} {navUser.name.last} - Role:{' '}
{navUser.role}
</Navbar.Text>
</Navbar.Collapse>
<Button onClick={logoutButton} variant='outline-primary mr-4'>
Logout
</Button>
</Navbar>
</div>
);
};
According to React Docs, useState, same as setState, triggers a new render.
Next render, the new value will be in user.
Because you are in an async method, youre out of the loop, and cannot see new state value in the next line.
See codesandbox poc:
In it the async function awaits 2 seconds and then set state.
You can see in the console that the render is triggered after the set state, the render sees the new state, but the async method prints the old one.
useState, same as setState, are syncronous, meaning they will directly trigger a render.
For async work, you can use useEffect
I would like to rewrite this life cycle method into a hook but it does'nt work as expected.
when the componentdidmounted, if the user id exists in the local storage,the user is connected and his name is displayed in the navbar. And when he disconnects and reconnects his name is displayed in the navbar.
So i am trying to convert this class Component with hooks, when the username changes nothing is displayed in the navbar so i have to refresh the page and that way his name is displayed
The real problem is the componentDidUpdate
how can i get and compare the prevProps with hooks
The class Component
const mapStateToProps = state => ({
...state.authReducer
}
);
const mapDispatchToProps = {
userSetId,
userProfilFetch,
userLogout
};
class App extends React.Component {
componentDidMount() {
const userId = window.localStorage.getItem("userId");
const {userSetId} = this.props;
if (userId) {
userSetId(userId)
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
const {userId, userProfilFetch, userData} = this.props; //from redux store
if(prevProps.userId !== userId && userId !== null && userData === null){
userProfilFetch(userId);
}
}
render() {
return (
<div>
<Router>
<Routes/>
</Router>
</div>
);
}
}
export default connect(mapStateToProps,mapDispatchToProps)(App);
With hooks
const App = (props) => {
const dispatch = useDispatch();
const userData = useSelector(state => state.authReducer[props.userData]);
const userId = window.localStorage.getItem("userId");
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfilFetch(userId))
}
}, [userData, userId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
export default App;
How to get the previous props or state?
Basically create a custom hook to cache a value:
const usePrevious = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Usage:
const App = (props) => {
const dispatch = useDispatch();
const userData = useSelector(state => state.authReducer[props.userData]);
const userId = window.localStorage.getItem("userId");
// get previous id and cache current id
const prevUserId = usePrevious(userId);
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfileFetch(userId))
}
// do comparison with previous and current id value
if (prevUserId !== userId) {
dispatch(userProfileFetch(userId));
}
}, [userData, userId, prevUserId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
FYI: You may want to refactor the code a bit to do the fetch from local storage in an effect hook that runs only on mount. If I understand your app flow correctly it would look something like this:
const App = (props) => {
const dispatch = useDispatch();
const { userId } = useSelector(state => state.authReducer[props.userData]);
useEffect(() => {
const userId = window.localStorage.getItem("userId");
userId && dispatch(userSetId(userId));
}, []);
// get previous id and cache current id
const prevUserId = usePrevious(userId);
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfileFetch(userId))
}
// do comparison with previous and current id value
if (prevUserId !== userId) {
dispatch(userProfileFetch(userId));
}
}, [userId, prevUserId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
now i resolve it, i made this
const App = props => {
const userId = window.localStorage.getItem("userId");
const dispatch = useDispatch();
const userData = useSelector(state=> state.authReducer[props.userData]);
const isAuthenticated = useSelector(state=> state.authReducer.isAuthenticated);
useEffect(()=> {
if(userId){
dispatch(userSetId(userId))
dispatch(userProfilFetch(userId))
}
}, [userId])
return(
<div>
<Router>
<Routes/>
</Router>
</div>
)
};
I am implementing authentication functionality in my app and when I try to save auth token which I get from my backend to reducer state it does nothing... I am new to this so there may be some dumb error.
This is my store.js file:
import React from 'react';
export const initialState = { access_token: null };
export const reducer = (state, action) => {
switch (action.type) {
case "SET_TOKEN":
console.log(action.data) // this does return the token which means data is passed correctly
return { access_token: action.data };
case "REMOVE_TOKEN":
return { access_token: null };
default:
return initialState;
}
};
export const Context = React.createContext();
This is my root component file AppRouter.js:
function AppRouter() {
const [store, dispatch] = useReducer(reducer, initialState);
const access_token = store.access_token;
console.log(access_token);
const AuthenticatedRoute = GuardedRoute(access_token);
return (
<Context.Provider value={{store, dispatch}}>
<Router>
<Switch>
<Route exact path="/" component={HomeScreen}/>
<Route exact path="/register" component={RegisterScreen}/>
<Route exact path="/login" component={LoginScreen}/>
<AuthenticatedRoute component={DashboardScreen} exact path={"/dashboard"}/>
</Switch>
</Router>
</Context.Provider>
)
}
So to me all this looks fine, and then this is the _login function in which I send the dispatch() to save the token(EDIT: this is everything between start of component function and return():
const [afterSuccessRegister, setAfterSuccessRegister] = useState(false);
const [emailInput, setEmailInput] = useState("");
const [passwordInput, setPasswordInput] = useState("");
const [loginErrorMessage, setLoginErrorMessage] = useState("");
const [createdUserEmail, setCreatedUserEmail] = useState("");
const { store, dispatch } = useContext(Context);
const _login = () => {
axios.post(`${ROOT_API}/v1/users/login`, {
"user": {
"email": emailInput,
"password": passwordInput
}
}, {}).then(res => {
console.log(res.data);
dispatch({type: 'SET_TOKEN', data: res.data.meta.access_token});
}).catch(err => {
console.log(err.response);
setLoginErrorMessage(err.response.data.message)
})
};
const _handleEmailChange = (e) => {
setEmailInput(e.target.value);
};
const _handlePasswordChange = (e) => {
setPasswordInput(e.target.value);
};
useEffect(() => {
if(typeof props.location.state !== "undefined") {
if (typeof props.location.state.success_register === 'undefined' || props.location.state.success_register === null || props.location.state.success_register === false) {
console.log("login");
} else {
setAfterSuccessRegister(true);
setCreatedUserEmail(props.location.state.created_user_email);
delete props.location.state;
}
}
}, [props.location.state]);
I really don't know why is it not saving it even though data is passed correctly. I tried adding console.log(store.access_token) after my login request has finished to see if it was saved, but it returns null.
Thanks!