useState Hook Concurrency Issue With Async Functions - reactjs

This is an issue I faced, investigated, and fixed and would like to share my experience with you.
I found that when you are using useState HOOK to maintain state and then update the state using the style setState({...state, updatedProperty: updatedValue}) in an async function, you may run into some concurrency issues.
This may lead the application in some cases to lose some data due to the fact that async function keeps an isolated version of the state and may overwrite data some other component stored in the state.
The fix in short:
You need to use either a reducer to update state or use the function updater version of set state, if you are going to update the state from an async function because the function updater gets the latest updated version of state as an argument (prevState)
setState(prevState => ({...prevState, updatedProperty: updatedValue});
Long Description:
I was developing data context to manage user's contacts saved on a database which is hosted on a cloud MongoDB cluster and managed by a back end web service.
In the context provider, I used useState hook to maintain the state and was updating it like the following
const [state, setState] = useState({
contacts: [],
selectedContact: null
});
const setSelected = (contact) => setState({...state, selectedContact: contact});
const clearSelected = ()=> setState({...state, selectedContact: null};
const updateContact = async(selectedContact) => {
const res = await [some api call to update the contact in the db];
const updatedContact = res.data;
// To check the value of the state inside this function, I added the following like and found
// selectedContact wasn't null although clearSelected was called directly after this function and
// selectedContact was set to null in Chrome's React dev tools state viewer
console.log('State Value: ' + JSON.stringify(state));
//The following call will set selectedContact back to the old value.
setState({
...state,
contacts: state.contacts.map(ct=> ct._id === updatedContact._id? updatedContact : ct)
});
}
//In the edit contact form submit event, I called previous functions in the following order.
updateContact();
clearSelected();
Problem
It was found that after selecteContact is set to null then set back to the old value of the selected contact after updateContact finishes awaiting for the api call's promise.
This issue was fixed when I updated the state using the function updater
setState(prevState => ({
...prevState,
contacts: prevState.contacts.map(ct=> ct._id === updatedContact._id? updatedContact : ct)
}));
I also tried using reducer to test the behavior given this issue and found reducers are working fine even if you are going to use regular way (without the function updater) to update the sate.

Related

How to invalidate react-query whenever state is changed?

I'm trying to refetch my user data with react-query whenever a certain state is changed. But ofcourse I can't use a hook within a hook so i can't figure out how to set a dependency on this state.
Current code to fetch user is:
const {data: userData, error: userError, status: userStatus} = useQuery(['user', wallet], context => getUserByWallet(context.queryKey[1]));
This works fine. But I need this to be invalidated whenever the gobal state wallet is changed. Figured I could make something like
useEffect(
() => {
useQueryClient().invalidateQueries(
{ queryKey: ['user'] }
)
},
[wallet]
)
but this doesn't work because useQueryClient is a hook and can't be called within a callback.
Any thoughts on how to fix this?
General idea is wallet can change in the app at any time which can be connected to a different user. So whenever wallet state is changed this user needs to be fetched.
thanks
useQueryClient returns object, which you can use later.
For example:
const queryClient = useQueryClient()
useEffect(
() => {
queryClient.invalidateQueries(
{ queryKey: ['user'] }
)
},
[wallet]
)

Hook with abortable fetch

note: I am aware of the useAbortableFetch hook. Trying to recreate a simple version of it.
I am trying to create a hook that returns a function that can make an abortable fetch request.
Idea being I want this hook to hold the state and update it when needed.
The update part is controlled by another competent on input change
What I am working on currently is
function useApiData(baseUrl){
const [state, setState] = use state({
data: null,
error: null,
loading: false
})
const controller = useRef(new AbortController)
const fetchData = searchTerm => {
if(state.loading){
controller.current.abort()
controller.current = new AbortController;
}
const signal = controller.signal;
setState(state => ({...state, loading: true})
fetch(url + searchTerm, {signal})
.then(res => res.json())
.then(data => {
setState(state => ({...state, data}))
return data
})
.catch(error => {
setState(state => ({...state, error}))
})
.finally(() => setState({...state, loading: false}))
}
const fetchCallback = useCallback(debounce(fetchData, 500), [])
return {...state, search: fetchCallback}
}
Usage
function App(){
const dataState = useApiData(url);
return ComponentWithInputElement {...dataState} />
}
function ComponentWithInputElement(props){
const [value, setValue] = useState('')
const onInput = ev => {
setValue(ev.target.value)
props.search(ev.tagert.value)
}
return (
<>
<input value={value} on input={onInput}>
{props.data?.length && <render datacomp>}
</>
)
}
This seems to fail to even send the first request.
Any way to make this pattern work?
Doing this in a useEffect would be very simple but I won't have access to the input value to have it as a dep
useEffect(()=>{
const controller = new AbortController();
const signal = controller.signal
fetch(url + value, {signal})
return () => controller.abort()
},[value])
Part of what you are trying to do does not feel "right". One of the things you are trying to do is have the state of the input value (like the form state) stored in the same hook. But those are not the same bits of state, as when the user types, it is (temporarily until its saved back to the server) different to the state fetched from the server. If you reuse the same state item for both, in the process of typing in the field, you lose the state fetched from the server.
You may think, "but I don't need it any more" -- but that often turns out to be a false abstraction later when new requirements come about that require it (like you need to display some static info as well as an editable form). In that sense, in the long term it would likely be less reusable.
It's a classic case of modelling an abstraction around a single use case -- which is a common pitfall.
You could add a new state item to the core hook to manage this form state, but then you have made it so you can only ever have the form state at the same level as the fetched data -- which may work in some cases, but be "overscoping" in others.
This is basically how all state-fetch libs like react query work -- Your fetched data is separate to the form data. And the form data is just initialised from the former as its initial value. But the input is bound to that "copy".
What you want is possible if you just returned setState from an additional state item in the core hook then passed down that setState to the child to be used as a change handler. You would then pass down the actual form string value from this new state from the parent to the child and bind that to the value prop of the input.
However, I'd encourage against it, as its an architectural flaw. You want to keep your local state, and just initialise it from the fetched state. What I suggested might be OK if you intend to use it only in this case, but your answer implies reuse. I guess I would need more info about how common this pattern is in your app.
As for abort -- you just need to return the controller from the hook so the consumer can access it (assuming you want to abort in the consumers?)

Redux dispatch in useEffect causes numerous redux errors

Edit - Fixed The problem was absolute garbage redux code as it was my first time using it. I studied Redux and rewrote my code and it works fine. It wasn't working because there was way too much garbage code working at once.
I'm using nextJS and when visiting a shared URL such as /username/p/postID I want to display the initial post modal/popover.
async function presentInitialPost(postID: string) {
const postSnap = await db.collection("posts").doc(postID).get();
const postData = postSnap.data();
if(postData){
dispatch(showModalPostView(postData as Post));
}
}
useEffect(() => {
if(initialPostID){
presentInitialPost(initialPostID)
}
}, [initialPostID])
Errors (there ae numerous of each):
"You may not unsubscribe from a store listener while the reducer is executing."
"You may not call store.getState() while the reducer is executing."
I use the same dispatch throughout my app just fine - and if I call presentInitialPost on a button click instead - it works completely fine.
I've tried delays and debugging where the error is coming from but I haven't figured it out at all, any help is appreciated, thank you.
Redux code:
showModalPostView(state, action) {
state.showModalPostViewFunction(action.payload);
},
setShowModalPostViewFunction(state, action) {
state.showModalPostViewFunction = action.payload;
return state;
},
showModalPostViewFunction: (post: Post) => {},
showModalPostViewFunction comes from my overlay wrapper component
showModalPostView = (post: Post) => {
console.log(this.state)
this.setState({
showModalPostView: true,
modalPostViewPost: post,
});
};
When the modal post view is shown here, it contains multiple useSelectors which cause the errors - but only when i present the modal post view in useEffect - it works just fine throughout the app when dispatched through a click action.
In modal post view various selectors throw the errors:
const likedPosts = useSelector((state: ReduxRootState) => state.likedPosts);
It appears you are not using redux the way it was intended to be used. What you pass to dispatch() is supposed to be an action. A reducer receives that action and returns new state, and then then new state is sent to your subscriber (the React component).
You are instead calling a function showModalPostView which is calling some setState function in the parent component and is returning who knows what. That return value is being passed as an argument dispatch, which kicks off a reducer. However, that setState is likely causing your child component to unsubscribe from the store so it can re-render.
It like you aren't actually dispatching an action; you're just calling a function, so you shouldn't be using dispatch at all.
async function presentInitialPost(postID: string) {
const postSnap = await db.collection("posts").doc(postID).get();
const postData = postSnap.data();
if(postData){
showModalPostView(postData as Post);
}
}

How to use zustand to store the result of a query

I want to put the authenticated user in a zustand store. I get the authenticated user using react-query and that causes some problems. I'm not sure why I'm doing this. I want everything related to authentication can be accessed in a hook, so I thought zustand was a good choice.
This is the hook that fetches auth user:
const getAuthUser = async () => {
const { data } = await axios.get<AuthUserResponse>(`/auth/me`, {
withCredentials: true,
});
return data.user;
};
export const useAuthUserQuery = () => {
return useQuery("auth-user", getAuthUser);
};
And I want to put auth user in this store:
export const useAuthStore = create(() => ({
authUser: useAuthUserQuery(),
}));
This is the error that I get:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons.
you can read about it in the react documentation:
https://reactjs.org/warnings/invalid-hook-call-warning.html
(I changed the name of some functions in this post for the sake of understandability. useMeQuery = useAuthUserQuery)
I understand the error but I don't know how to fix it.
The misunderstanding here is that you don’t need to put data from react query into any other state management solution. React query is in itself a global state manager. You can just do:
const { data } = useAuthUserQuery()
in every component that needs the data. React query will automatically try to keep your data updated with background refetches. If you don’t need that for your resource, consider setting a staleTime.
—-
That being said, if you really want to put data from react-query into zustand, create a setter in zustand and call it in the onSuccess callback of the query:
useQuery(key, queryFn, { onSuccess: data => setToZustand(data) })

Graphql subscriptions inside a useEffect hook doesn't access latest state

I'm building a basic Slack clone. So I have a "Room", which has multiple "Channels". A user subscribes to all messages in a Room, but we only add them to the current message list if the new message is part of the user's current Channel
const [currentChannel, setCurrentChannel] = useState(null);
const doSomething = (thing) => {
console.log(thing, currentChannel)
}
useEffect(() => {
// ... Here I have a call which will grab some data and set the currentChannel
Service.joinRoom(roomId).subscribe({
next: (x) => {
doSomething(x)
},
error: (err: any) => { console.log("error: ", err) }
})
}, [])
I'm only showing some of the code here to illustrate my issue. The subscription gets created before currentChannel gets updated, which is fine, because we want to listen to everything, but then conditionally render based on currentChannel.
The issue I'm having, is that even though currentChannel gets set correctly, because it was null when the next: function was defined in the useEffect hook, doSomething will always log that currentChannel is null. I know it's getting set correctly because I'm displaying it on my screen in the render. So why does doSomething get scoped in a way that currentChannel is null? How can I get it to call a new function each time that accesses the freshest state of currentChannel each time the next function is called? I tried it with both useState, as well as storing/retrieving it from redux, nothing is working.
Actually it is related to all async actions involving javascript closures: your subscribe refers to initial doSomething(it's recreated on each render) that refers to initial currentChannel value. Article with good examples for reference: https://dmitripavlutin.com/react-hooks-stale-closures/
What can we do? I see at least 2 moves here: quick-n-dirty and fundamental.
We can utilize that useState returns exact the same(referentially same) setter function each time and it allows us to use functional version:
const doSomething = (thing) => {
setCurrentChannel(currentChannelFromFunctionalSetter => {
console.log(thing, currentChannelFromFunctionalSetter);
return currentChannelFromFunctionalSetter;
}
}
Fundamental approach is to utilize useRef and put most recent doSomething there:
const latestDoSomething = useRef(null);
...
const doSomething = (thing) => { // nothing changed here
console.log(thing, currentChannel)
}
latestDoSomething.current = doSomething; // happens on each render
useEffect(() => {
Service.joinRoom(roomId).subscribe({
next: (x) => {
// we are using latest version with closure on most recent data
latestDoSomething.current(x)
},

Resources