Re-rendering app with custom hook not working - javascript

I seek advice regarding a small project I work on. I have a problem with re-rendering/reloading app when my state changes. The issue occurred when I changed useState to custom hook that uses session storage. There is the code of the hook:
const useStateWithSessionStorage = (localStorageKey) => {
const [value, setValue] = React.useState(
JSON.parse(sessionStorage.getItem(localStorageKey)) || {
screen: "signin",
loading: false,
user: null,
response: null,
}
);
React.useEffect(() => {
sessionStorage.setItem(localStorageKey, JSON.stringify(value));
}, [value]);
return [value, setValue];
};
and this is my App.js:
export default function App() {
const [appState, setAppState] = useStateWithSessionStorage("appState");
const renderApp = () => {
if (appState.screen == "signin") return <Signin />;
if (appState.screen == "hub") return <Hub />;
};
return <div className="App">{renderApp()}</div>;
}
I also tried to load screen value into another state, which would be re-rendered in useEffect, however, without success.
renderApp function returns either or . Signin component shows only email and password input, which values are sent do flask endpoint working with sql.
That endpoint updates appState. Concretely it changes appState.screen to "hub".
There's the problem I mentioned. Although state changes (visibly on page displayed by JSON.stringify). The app wont re-render and stays on component. To work properly and show I always must refresh the page.
I'm fairly new into this, could anyone give me an advice, please?

You will need to listen to Window: storage event to get notified when some component changes the session key value:
const useStateWithSessionStorage = (localStorageKey) => {
const [value, setValue] = React.useState(
JSON.parse(sessionStorage.getItem(localStorageKey)) || {
screen: "signin",
loading: false,
user: null,
response: null,
}
);
React.useEffect(()=> {
const onStorage = () => {
const data = JSON.parse(sessionStorage.getItem(localStorageKey)));
if(data.screen !== value.screen) { /* data changes*/
setValue(data);
}
}
window.addEventListener('storage', onStorage);
return () => window.removeEventListener("storage", onStorage);
}, []);
React.useEffect(() => {
sessionStorage.setItem(localStorageKey, JSON.stringify(value));
}, [value]);
return [value, setValue];
};

Related

Getting stale value of local state variable after response returned from promise

I have a react application with two buttons, which on click load user name from server. The behaviour works if I click buttons one at a time and wait for response, however, if I click both, the response from API for second button writes value to state which is stale due to which the first button gets stuck in loading state. How can I resolve this to always have latest data when promise resolves?
Code sandbox demo: https://codesandbox.io/s/pensive-frost-qkm9xh?file=/src/App.js:0-1532
import "./styles.css";
import LoadingButton from "#mui/lab/LoadingButton";
import { useRef, useState } from "react";
import { Typography } from "#mui/material";
const getUsersApi = (id) => {
const users = { "12": "John", "47": "Paul", "55": "Alice" };
return new Promise((resolve) => {
setTimeout((_) => {
resolve(users[id]);
}, 1000);
});
};
export default function App() {
const [users, setUsers] = useState({});
const availableUserIds = [12, 47];
const loadUser = (userId) => {
// Mark button as loading
const updatedUsers = { ...users };
updatedUsers[userId] = {
id: userId,
name: undefined,
isLoading: true,
isFailed: false
};
setUsers(updatedUsers);
// Call API
getUsersApi(userId).then((userName) => {
// Update state with user name
const updatedUsers = { ...users };
updatedUsers[userId] = {
...updatedUsers[userId],
name: userName,
isLoading: false,
isFailed: false
};
setUsers(updatedUsers);
});
};
return (
<div className="App">
{availableUserIds.map((userId) =>
users[userId]?.name ? (
<Typography variant="h3">{users[userId].name}</Typography>
) : (
<LoadingButton
key={userId}
loading={users[userId]?.isLoading}
variant="outlined"
onClick={() => loadUser(userId)}
>
Load User {userId}
</LoadingButton>
)
)}
</div>
);
}
The problem is that useState's setter is asynchronous, so, in your loader function, when you define const updatedUsers = { ...users };, user is not necessary updated.
Luckily, useState's setter provides allows us to access to the previous state.
If you refactor your code like this, it should work:
const loadUser = (userId) => {
// Mark button as loading
const updatedUsers = { ...users };
updatedUsers[userId] = {
id: userId,
name: undefined,
isLoading: true,
isFailed: false
};
setUsers(updatedUsers);
// Call API
getUsersApi(userId).then((userName) => {
// Update state with user name
setUsers(prevUsers => {
const updatedUsers = { ...prevUsers };
updatedUsers[userId] = {
...updatedUsers[userId],
name: userName,
isLoading: false,
isFailed: false
};
return updatedUsers
});
});
};
Here a React playground with a simplified working version.

Supabase onAuthStateChanged - How do I properly wait for the request to finish prevent flickering with useEffect?

Everything auth-wise is working fine. I even have a loading state setup so that the loader shows until the state is changed, but I still get this flickering on reload. This flickering only happens with Supabase. I was using the Firebase version before and it worked perfectly with my code.
Here is a video for reference: https://imgur.com/a/5hywXj5
Edit: Updated code to current version
export default function Navigation() {
const { user, setUser } = useContext(AuthenticatedUserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const session = supabase.auth.session();
setUser(session?.user ?? null);
const { data: listener } = supabase.auth.onAuthStateChange((_: any, session: any) => {
setUser(session?.user ?? null);
});
setIsLoading(false);
return () => {
listener?.unsubscribe();
};
}, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator color={Theme.colors.purple} size="large" />
</View>
);
}
return (
<NavigationContainer linking={LinkingConfiguration}>{user ? <AppStack /> : <AuthStack />}</NavigationContainer>
);
}
To recap for others, onAuthStateChange will not execute on first page load so you are triggering it using the getUserAuthStatus async function. However session() function is not async and will immediately return a result of null if there is no user session, or return a session that has been stored in localStorage.
In this case the result of the getUserAuthStatus will always return null. Then onAuthStateChange will trigger with the SIGNED_IN event and a session which will then set the user.
Furthermore the onAuthStateChange function should be registered before you perform the session step so as to capture any events triggered. In the current form an event may be triggered directly after the session() call but before the handler is registered.
So to recap the rendering steps will be:
Step 1
isLoading: true
user: null
Step 2
isLoading: false
user: null
Step 3
isLoading false
user: {...}
So far as I can tell, using session directly without thinking it's async will do the trick.
Ok, Supabase has released some updates since I first asked this question. Here is how I am now able to stop flickering when loading the application.
First, we need to set up our AuthContext for our application. Be sure to wrap your App.tsx with the <AuthContextProvider>.
AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Session, User } from '#supabase/supabase-js';
import { supabase } from '../config/supabase';
export const AuthContext = createContext<{ user: User | null; session: Session | null }>({
user: null,
session: null,
});
export const AuthContextProvider = (props: any) => {
const [userSession, setUserSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUserSession(session);
setUser(session?.user ?? null);
});
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
console.log(`Supabase auth event: ${event}`);
setUserSession(session);
setUser(session?.user ?? null);
});
return () => {
authListener.subscription;
};
}, []);
const value = {
userSession,
user,
};
return <AuthContext.Provider value={value} {...props} />;
};
export const useUser = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useUser must be used within a AuthContextProvider.');
}
return context;
};
Now, if you're using React Navigation like me we need to check if we have a valid user to send them to the logged-in home screen. Here's how I do it.
Navigation.tsx
export default function Navigation() {
const { user } = useUser();
return (
<NavigationContainer linking={LinkingConfiguration}>
{user ? <AppStackNavigator /> : <AuthStackNavigator />}
</NavigationContainer>
);
}

What can I use in functional components to have same behavior as componentDidMount?

My UI was working fine until it was using a class component. Now I am refactoring it to a functional component.
I have to load my UI based on the data I receive from an API handler. My UI will reflect the state of the camera which is present inside a room. Every time the camera is turned on or off from the room, I should receive the new state from the API apiToGetCameraState.
I want the console.log present inside the registerVideoStateUpdateHandlerWrapper to print both on UI load for the first time and also to load every time the video state is changed in the room. However, it doesn't work when the UI is loaded for the first time.
This is how my component looks like:
const Home: React.FunctionComponent<{}> = React.memo(() => {
const [video, setToggleVideo] = React.useState(true);
const registerVideoStateUpdateHandlerWrapper = React.useCallback(() => {
apiToGetCameraState(
(videoState: boolean) => {
// this log does not show up when the UI is loaded for the first time
console.log(
`Video value before updating the state: ${video} and new state is: ${videoState} `
);
setToggleVideo(videoState);
}
);
}, [video]);
React.useEffect(() => {
//this is getting called when the app loads
alert(`Inside use effect for Home component`);
registerVideoStateUpdateHandlerWrapper ();
}, [registerVideoStateUpdateHandlerWrapper ]);
return (
<Grid>
<Camera
isVideoOn={video}
/>
</Grid>
);
});
This was working fine when my code was in class component. This is how the class component looked like.
class Home extends Component {
registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
console.log(`ToggleVideo value before updating the state: ${this.state.toggleCamera} and new state is: ${videoState}`);
this.setStateWrapper(videoState.toString());
})
}
setStateWrapper = (toggleCameraUpdated) => {
console.log("Inside setStateWrapper with toggleCameraUpdated:" + toggleCameraUpdated);
this.setState({
toggleCamera: (toggleCameraUpdated === "true" ) ? "on" : "off",
});
}
constructor(props) {
super(props);
this.state = {
toggleCamera: false,
};
}
componentDidMount() {
console.log(`Inside componentDidMount with toggleCamera: ${this.state.toggleCamera}`)
this.registerVideoStateUpdateHandlerWrapper ();
}
render() {
return (
<div>
<Grid>
<Camera isVideoOn={this.state.toggleCamera} />
</Grid>
);
}
}
What all did I try?
I tried removing the useCallback in the registerVideoStateUpdateHandlerWrapper function and also the dependency array from React.useEffect and registerVideoStateUpdateHandlerWrapper. It behaved the same
I tried updating the React.useEffect to have the code of registerVideoStateUpdateHandlerWrapper in it but still no success.
Move registerVideoStateUpdateHandlerWrapper() inside the useEffect() callback like this. If you want to log the previous state when the state changes, you should use a functional update to avoid capturing the previous state through the closure:
const Home = () => {
const [video, setVideo] = useState(false);
useEffect(() => {
console.log('Inside useEffect (componentDidMount)');
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo((prevVideo) => {
console.log(`Video value before updating the state: ${prevVideo} and new state is: ${videoState}`);
return videoState;
});
});
};
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
When you no longer actually need to log the previous state, you should simplify registerVideoStateUpdateHandlerWrapper() to:
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
import React from 'react'
const Home = () => {
const [video, setVideo] = useState(null);
//default video is null, when first load video will change to boolean, when the Camera component will rerender
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
useEffect(() => {
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
export default Home
componentDidMount() === useEffect()
'useEffect' => import from 'react'
// componentDidMount()
useEffect(() => {
// Implement your code here
}, [])
// componentDidUpdate()
useEffect(() => {
// Implement your code here
}, [ update based on the props, state in here if you mention ])
e.g:
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
// Implement the code here
}, [ loggedIn ]);
the above code will act as equivalent to the componentDidUpdate based on 'loggedIn' state

react custom hook state is not updating when re-rendering

I'm having an issue when repopulating form for edit using react hooks.
Parent Component : Edit.js
const EditData = (props) => {
const { Id } = props.match.params;
const dispatch = useDispatch();
// calling redux action to get the data
useEffect(() => {
dispatch(getDataById(Id));
}, [Id]);
const data = useSelector((state) => state.data);
const initialState = {
Id: data.cardId || '',
Number: data.Number || '',
Date: data.Date,
};
//calling custom hook
const { handleChange, handleSubmit, values,errors } = useForm(
initialState,// passing initial state to custom hook
validateOnSubmit,
submit
);
// used to submit the data
function submit() {
dispatch(updateCard(values));
}
return (<DateForm
handleSubmit={handleSubmit}
handleChange={handleChange}
values={values}
/>);
};
Custom hook: useform.js
const useForm = (initialState, validateOnSubmit, callback) => {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setValues({
...values,
[name]: value
});
};
const handleSubmit = (event) => {
event.preventDefault();
setErrors(validateOnSubmit(values));
setIsSubmitting(true);
};
useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
}
}, [errors]);
return {
handleChange,
handleSubmit,
values,
errors
};
};
when the API call get finished the react re-render the custom but the local state of the hook is not updating.
const useForm = (initialState, validateOnSubmit, callback) => {
console.log(initialState);
on second render, here i can receive data from the API
const [values, setValues] = useState(initialState);
but values is not getting updated, values is still holding the state from the initial render
I cannot figure out why this is. I'm just started to use react hooks, please help me.
As OP stated in a comment:
the initialState variable is updated when the API call get completed,I'm passing that initialState variable to const [values, setValues] = useState(initialState); , so it should update the values variable right?. but it's not!
It is should update the state, the initial state is assigned once until the component unmounts.
See useState API, it stated in lazy initialization:
The initialState argument is the state used during the initial render. In subsequent renders, it is disregarded.

Losing Local Storage on Page Refresh in React/Redux

I'm using React and Redux and storing data in a loggedUser variable upon user login.
my login reducer looks like this:
const loginReducer = (state = null, action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};
export const fetchUser = () => {
return dispatch => {
const userStr = window.localStorage.getItem("loggedVintageUser");
const user = JSON.parse(userStr);
if (user) {
dispatch({ type: "SET_USER", data: user });
}
};
};
export const setUser = data => {
return dispatch => {
dispatch({ type: "SET_USER", data });
};
};
export const login = data => {
return async dispatch => {
const user = await loginService.login({
username: data.username,
password: data.password
});
window.localStorage.setItem("loggedVintageUser", JSON.stringify(user));
dispatch({ type: "SET_USER", data: user });
};
};
In my core App component i'm dispatching the fetchUser and setUser creators
useEffect(() => {
fetchUser();
}, [props.fetchUser]);
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem("loggedVintageUser");
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON);
props.setUser(user);
userService.setToken(user.token);
}
}, []);
I'm displaying a list of favorite items for a user and when i go to refresh the page, i'm getting the following error:
TypeError: Cannot read property 'favorites' of null
Here is relevant code for my Favorites component. The error is triggered on the loggedUser.favorites data. I can see when visiting the favorites page, the loggedUser field is there and data displays fine but on refresh the loggedUser variable turns to null.
const searchCards = ({ loggedUser, search }) => {
const favorites = loggedUser.favorites;
console.log("FAVORITES", favorites);
return search
? favorites.filter(a =>
a.title
.toString()
.toLowerCase()
.includes(search.toLowerCase())
)
: favorites;
};
const Cards = props => {
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const [filteredData, setData] = useState(props.cardsToShow);
const mapStateToProps = state => {
return {
baseball: state.baseball,
loggedUser: state.loggedUser,
page: state.page,
entries: state.entries,
query: state.query,
pageOutput: state.pageOutput,
search: state.search,
cardsToShow: searchCards(state)
};
};
const mapDispatchToProps = {
searchChange,
fetchData,
updateUser
};
I tried to add this before i render the data, but it's not working
if (!props.loggedUser) return null;
How can i retain that state if a user is refreshing the page. The odd part is that on my home page where i have a similar sort of display a refresh isn't causing the same problems.
check once loggedUser is exist in state or not. Print state using console.log(state). you may also open inspect tool and go to application tab and click on local storage, you will get localStorage data.
Well, i figured this out and got some help from this post here. Redux store changes when reload page
My loggedUser state was disappearing after reload, so i just loaded the inital state for loggedUser pulling the data from the local storage:
function initState() {
return {
token: localStorage.token,
firstName: localStorage.firstName,
id: localStorage.id,
favorites: localStorage.favorites,
username: localStorage.username
};
}
const loginReducer = (state = initState(), action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};

Categories