How to keep the state variable value intact upon screen reload - reactjs

I am new to React and I tried to toggle the Login/Logout based on the current state of authentication. I've used Google OAuth to perform the authentication.
I have a state variable to say if the user is authenticated or not and is defaulted to false. Upon successful authentication, I set the state to true.
Now the problem is, after completing a successful authentication, when I refresh the screen, the screen reloads and I see the console.log printing false and login appears momentarily. And after a second the console.log prints true and then the logout appears. How do I avoid showing login screen (for that one second after refreshing the screen) when the authentication is completed? Can someone help me please? Thanks.
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(false)
}, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}

If your variable goes from false to true it means your code is doing something, probably an AJAX call, my recommendation is to show a loading scree/message until the AJAX request is completed.

There is no way to keep the variable intact on reload but you can keep a variable that tracks if the user authentication has been initialized and show a loading indicator in the meantime
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [authLoaded, setAuthLoaded] = useState(false)
// this does nothing, passing false to useState above sets the initial value
// useEffect(() => {
// setIsAuthenticated(false)
// }, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
setAuthLoaded(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
setAuthLoaded(true)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if (!authLoaded) {
return <div>loading...</div>
}
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}

I believe olivier-boisse alluded to using localStorage to persist your state. You can use an useEffect hook to persist your isAuthenticated to localStorage when the value updates, and use a state initializer function to read in the initial state from local storage.
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return !!JSON.parse(localStorage.getItem('auth');
});
useEffect(() => {
localStorage.setItem('auth', JSON.stringify(isAuthenticated));
}, [isAuthenticated]);

Related

Can't set the location to the state

I'm trying to set the location to the state, but the first time i click the button the state is null not the current location. Why?
function Geolocation() {
const [myLocation, setMyLocation] = useState(null)
const getLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(setMyLocation);
console.log(myLocation)
} else {
alert("Geolocation is not supported by this browser.")
}
}
return (
<div>
<button onClick={getLocation}>Get geolocation</button>
</div>
)
}
export default Geolocation
There is not a propblem. Just Console.log works faster than getLocation() function. You can see it in this code:
function Geolocation() {
const [myLocation, setMyLocation] = useState(null)
const getLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(setMyLocation);
console.log(myLocation)
} else {
alert("Geolocation is not supported by this browser.")
}
}
return (
<div>
<button onClick={getLocation}>{myLocation?.timestamp || "null"}</button>
</div>
)
}
export default Geolocation
but let me guess, in the second time you click it log the current location.
setMyLocation() does not immediately mutate myLocation but creates a pending state transition. Accessing myLocation after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setMyLocation() and calls may be batched for performance gains.
you can think of your myLocation state as a "snapshot" of the state related to the current render. after mutate the state, the component will re-render with the new state you set.
in your example you are trying to use the state you changed the line before but it will update just after the next render.
In conclusion, the state indeed changing. you can check it this way:
function Geolocation() {
const [myLocation, setMyLocation] = useState(null);
useEffect(() => {
console.log(myLocation);
}, [myLocation]);
const getLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(setMyLocation);
} else {
alert("Geolocation is not supported by this browser.");
}
};
return (
<div>
<button onClick={getLocation}>Get geolocation</button>
</div>
);
}

Why does my state affect the emitted events from a click handler?

I have (what seems to be) a very peculiar situation where I seem to be getting extra events emitted based on my Redux state.
I have narrowed the behavior down to whether or not I make a successful request to my /users endpoint and retrieve a list of users which is then stored in Redux.
If the commented code is not active (as it is currently shown), I am able to successfully render the modal(s) reliably and step between states.
If the commented code is active, the (which is what is behind the as well) emits an onDismiss call immediately. This has the result of closing the modal immediately.
If the commented code is active, but the response from the thunk is a 401 and the user data is not loaded (i.e., the state of the user key in redux is a failure, not success, then the modal works -- though of course, there are no users to select.
I have confirmed this behavior is consistent no matter where I seem to make this fetch request (initially it was in the App.tsx to be called immediately. I also tried it in an intermediate component).
Question(s):
Can you explain why I might be getting different behavior in my click handlers based on what is in my state?
Is there something I'm missing and I'm conflating my Redux state with the actual behavior?
I know I can solve this by adding a event.stopPropagation() call in strategic places (e.g., on the first button that opens the <ConfirmationBox> and then again on the button in the <ConfirmationBox> that transitions to the SelectUser modal), but are there other solutions?
//pinFlow.tsx
type States =
| { state: 'Confirm' }
| { state: 'SelectUser' }
| { state: 'SubmitPin'; user: User };
export function pinFlow<T extends ConfirmationBoxProps>(
ConfirmationBox: React.FC<T>,
authorization: Authorization,
) {
const [state, setState] = React.useState<States>({ state: 'Confirm' });
// const dispatch=useDispatch();
// initialize users
// const users = useSelector((state: InitialState) => state.pinAuth.users);
// const fetchUsers = useCallback(() => {
// dispatch(fetchUsersThunk());
// }, [dispatch]);
// useEffect(() => {
// if (users.state === RemoteDataState.NotStarted) {
// fetchUsers();
// }
// }, [fetchUsers, users.state]);
return (props: T) => {
const users = useSelector((state: InitialState) =>
mapRemoteData(state.pinAuth.users, users =>
users.filter(user => user.authorizations.includes(authorization)),
),
);
switch (state.state) {
case 'Confirm': {
return (
<ConfirmationBox
{...props}
onSubmit={(_event: React.MouseEvent) => {
setState({ state: 'SelectUser' });
}}
/>
);
}
case 'SelectUser': {
return (
<Modal
title={'PIN Required'}
canClickOutsideToDismiss={true}
onDismiss={() => {
setState({ state: 'Confirm' });
}}
>
<p className={style.selectProfileText}>Select your profile:</p>
<pre>
<code>{JSON.stringify(users, null, 4)}</code>
</pre>
{/*
<UserList users={users.data} /> */}
</Modal>
);
}
default: {
return <Modal title="others">all others</Modal>;
}
}
};
}
The code is used in another component like so:
function Comp(){
const [selected, setSelected] = useState();
const [mode, setMode] = useState();
const ConfirmationModal =
protected
? pinFlow(MenuItemModal, permission)
: MenuItemModal;
return(
<ConfirmationModal
item={selected}
mode={mode}
disabled={availability.state === RemoteDataState.Loading}
errorMessage={tryGetError(availability)}
onCancel={() => {
setMode(undefined);
dispatch(resetAvailability());
}}
onSubmit={(accessToken: string) => {
dispatch(findAction(selected, mode, accessToken));
}}
/>
)
}

Retain callback between rerenders

I'm building an app with guarded actions and would like to achieve the following flow:
anonymous user tries to run guarded action (e.g. play video - essentially modify React state)
login modal pops up without any redirects
user fills in the credentials and hits login button
any kind of loader shows up (and replaces the previous app tree) and stays on the screen as long as user is being logged in
once logged in, loader disappears and the authenticated app renders
video starts playing, because the app knows the action user wanted to take
I haven't found a way to do that, because replacing the old app tree with loader and then putting it back on the screen makes the action run on an already unmnounted component.
I'm using a <UserProvider> component that wraps the whole tree to provide it with authenticated state.
Codesandbox to illustrate the issue:
https://codesandbox.io/s/react-context-callback-l9xe9?file=/src/App.js
const MyContext = React.createContext();
const MyProvider = ({ children }) => {
const [auth, setAuth] = React.useState(false);
const [reset, setReset] = React.useState(false);
const [callback, setCallback] = React.useState(null);
const guard = (cb) => {
auth ? cb() : setCallback(() => cb);
};
React.useEffect(() => {
if (auth) {
setReset(true);
setTimeout(() => {
setReset(false);
}, 1000);
}
}, [auth]);
if (reset) {
return <p style={{ color: "red" }}>I pretend I'm logging you in</p>;
}
return (
<MyContext.Provider
value={{
setAuth,
auth,
callback,
setCallback,
guard
}}
>
{children}
</MyContext.Provider>
);
};
const Child = () => {
const [count, setCount] = React.useState(0);
const mounted = React.useRef(false);
React.useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const myCallback = () => {
setCount((cnt) => cnt + 1);
};
const { auth, setAuth, guard, callback, setCallback } = React.useContext(
MyContext
);
return (
<div>
logged in: {`${auth}`}
<br />
count: {count}
<br />
{callback && (
<button
onClick={() => {
setAuth(true);
callback();
setCallback(null);
}}
>
pretend login
</button>
)}
<button onClick={() => guard(myCallback)}>
increment (when authenticated)
</button>
</div>
);
};
Is that even possible?
That's a use case for lifting the state up, you can't preserve the state after the component unmounts (Of course you can try reading from local storage but it's essentially the same logic as lifting the state).
Therefore lift the count state (to App or to the provider itself):
export default function App() {
const state = React.useState(0);
...

react setting state in a condition inside useEffect

I'm trying to avoid showing an alert in React Native more than once.
To do that, I am trying to update the state inside a condition which is inside a useEffect:
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
useEffect(() => {
function handleAppStateChange() {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}
AppState.addEventListener('change', handleAppStateChange);
}, [permissionStatus, permissionsAlertShown]);
My issue is that if I navigate away from my app and come back to it, AppState.currentState changes and since setPermissionsAlertShown(true) is ignored, I am shown the alert again.
How do I handle this situation?
The answer was to create a callback function that will remove the listener. I will share if someone else ever looks for this.
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
const handleAppStateChange = useCallback(() => {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}, [permissionStatus, permissionsAlertShown]);
useEffect(() => {
AppState.addEventListener('change', handleAppStateChange);
return () => AppState.removeEventListener('change', handleAppStateChange);
}, [handleAppStateChange]);
You just need to persist the state, with asyncStorage or some other storage.
The ideal case is to use what persisted of your action (an permission enabled, an API data saved etc).

tell render code to wait until object is recieved

I have the following code that grabs user from my firestore database and then returns some components based on some user logic, but because the call to the database is asynchronous, the logic will run before my user object is grabbed. Is there any way I can tell the if-statements to run after user is grabbed with my useEffect().
For example, console.log(user.isSeller) initially is undefined and then turns into true once the data is recieved.
function StorefrontPage(props) {
const auth = useAuth();
const router = useRouter();
const [user, setUser] = useState({});
const uid = auth.user && auth.user.uid;
useEffect(() => {
uid && getUser(uid).then(currentUser => {
setUser(currentUser);
})
}, [auth])
console.log(user.isSeller);
//logic with user object
if (uid) {
if (user.isSeller) {
if (props.sellerId === uid) {
return (
<div>
<Storefront sellerId={props.sellerId}></Storefront>
<style jsx>{
"background-image:linear-gradient(to bottom, #ffffff, #D0D8FD);"
}</style>
</div>
);
}
else {
return (
<div>
<PublicStorefront sellerId={props.sellerId}></PublicStorefront>
<style jsx>{
"background-image:linear-gradient(to bottom, #ffffff, #D0D8FD);"
}</style>
</div>
)
}
}
else {
router.push(`/buyersportal/${auth.user.uid}`)
return (
<div>
<p>
Sending you to the buyer's Portal.
</p>
</div>
)
}
}
else {
return (
<div>
<PublicStorefront sellerId={props.sellerId}></PublicStorefront>
</div>
)
}
}
export default StorefrontPage;
Any idea if there are some functions I can use to make it wait until user is called?
Typically the pattern to solve this is with an isLoading state
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true); // set loading to true again in case auth gets updated
uid && getUser(uid).then(currentUser => {
setUser(currentUser);
setIsLoading(false);
})
}, [auth])
then you can do conditional rendering like so:
if (isLoading) return <LoadingSpinner/>
However if auth never gets updated you can also initialise user to be null:
const [user, setUser] = useState(null);
and then just check if the user exists or not with
if (user === null) return <LoadingSpinner/>
Set user as 'undefined' or 'null' and Just return a loader (or whatever you want as long as it is jsx) while you fetch your data :
if(!user){
return <Loader />
}
// your logic if condition above isn't matched
When user will be updated your component will be re-rendered

Resources