Why do multiple setState calls in an async function cause multiple renders? - reactjs

The issue is summarized well here; basically, if you have an async function in useEffect (which is where you'd expect such functions), you cause a re-render of your component for every state that is updated. I generally don't want to be bunching up things as in the authors solution/workaround, and to me this behavior doesn't make sense (you'd expect all your state updates to happen together).
Is this by design? If so, is there a better way to deal with this, such that I can perform all my updates without having to worry about ordering, etc? This feels like a bug, but maybe just a flaw in my understanding.
Code for reference:
export default function App (): JSX.Element {
const [user, setUser] = useState(null)
const [pending, setPending] = useState(false)
useEffect(() => {
setPending(true)
fetchUser().then((fetchedUser) => {
setPending(false)
setUser(fetchedUser) // This updated value won't initially be seen by other effects dependant on 'pending'
})
}, [])
// …
}

Ended up figuring this out; see this. You need to manually batch things currently, with ReactDOM.unstable_batchedUpdates(() => { ... }). Despite the name, it is seemingly widely regarded as quite stable.
It is also corrected in the currently in-development Concurrent Mode for React.

Related

How many times a component render before display?

When I read the document for the useEffect the component will render only one time if I recall correctly.
But in the bottom scenario the initial render is empty.
But after a sec or two it will render again to show that data coming from api.
So it basically render two times before showing the data am I wrong?
const SomeComponent = () => {
const [data, setData] = useState([]);
useEffect(() => {
axios.get('...').then((res) => setData(res.data)).catch((err) => console.log(err))
}, []);
return (
<div>{data.map((e) => (e...)}</div>
)
}
Your problem is possibly from React.StrictMode
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:...
Well, it's helpful in debugging during development but this side effect makes useEffect called twice intentionally.
One side note is these double callings won't be happening in production, so you don't need to worry about it.
If you want to avoid intentionally double-invoking in development, you can remove React.StrictMode permanently in your code.

Best practice for useEffect for a update action you want to use in multiple places in code

Let's say I have a useEffect like this:
useEffect(() => {
getMarket(web3, marketaddress, marketindex).then(setMarket);
}, [web3Ref, marketaddress, marketindex]);
This works OK, but now say I need to have this line in a few places (because various async actions require a refresh of the market:
getMarket(web3, marketaddress, marketindex).then(setMarket);
Are there any better options than what I can think of:
Copy and paste getMarket(web3, marketaddress, marketindex).then(setMarket); in each place.
Declare a function, but because of the react-hooks/exhaustive-deps I will get a lint error unless the function is a static outside of the component, in which case it would need to take setMarket as an argument, which seems silly.
Use an additional piece of pseudo-state to track changes, e.g. a number called version. Increment it whenever I want to refresh and then add this as a dependency to the useEffect above. Something like:
useEffect(() => {
getMarket(web3, marketaddress, marketindex).then(setMarket);
}, [web3Ref, marketaddress, marketindex, changeIndex]);
Is there a better/alternative way than anything I have described?
Create custom hook with refetch data
const useMarket = (web3, marketaddress, marketindex) => {
const [market, setMarket] = useState();
const fetchMarket = useCallback(
() => getMarket(web3, marketaddress, marketindex).then(setMarket),
[marketaddress, marketindex, web3]
);
useEffect(() => {
fetchMarket();
}, [marketaddress, marketindex, fetchMarket]);
return { market, refetch: fetchMarket };
};
I would recommend a custom hook which you can import to all your components, a custom hook can use its own state so there's no need to send the setState function.
It would look something like this:
//useMarket.js
const useMarket = (web3, marketaddress, marketindex)=>{
const [market, setMarket] = useState();
useEffect(() => {
getMarket(web3, marketaddress, marketindex).then(setMarket);
}, [web3Ref, marketaddress, marketindex]);
return market;
}
export default useMarket;
That's it based on the info you provided, now I'll give some advice which comes to mind but may be not so useful for you.
I usually use named parameters in hooks so I can edit them later withou having to change every place I use them, so the declaration would be like const useMarket = ({web3, marketaddress, marketindex})
It seems to me that web3 is the the js library to communicate with a blockchain, so you may find a better way to get inside the hook so you don't need to send it every time you call it.
If you call this in many components that require the same info, I would suggest to use come kind of cache inside you hook to prevent several api calls for the same information
You can change what you return to be an object if you want to expose something more than just the market state. Something like the setter, or the cache control methods if you happen to implement it.
You're actual code is missing error handling, so you could return another variable with a "state" of the hook, there you may tell the ui if it's loading or if there was an error getting the data
#3. People expect useEffect() to alter state so this is the ideal place for your code.
You can introduce a const [marketIsDirty, setMarketIsDirty] = useState(true) and if a user action or external condition should cause the market to be updated, call setMarketIsDirty(true) to trigger the state refresh.
call inside useEffect, setMarketIsDirty(false) once you've updated the market.
Also, do not update the market when marketIsDirty is false to avoid infinite loops.
Better, introduce a 'marketState' state with an enumeration of DIRTY, UPDATING, SUCCESS, ERROR so you can manage the UI in concert with the state changes.

Mid Component Life Cycle API Call

From my understanding fetching data is considered a side effect and should be within the use effect. But I'm wondering if this rule can be broken mid component lifecycle. It's obvious why useEffect is important for fetching data when the component initially mounts. It's less obvious after.
Here is an example of what I'm doing. It seems to work fine, and after the data is fetched the component rerenders.
function ComponentThatFetchesList() {
const [list, setList] = useState([]);
const fetchData = () => {
asyncFetchData().then(data => {
setList(list);
});
}
return (
<div>
<button onClick={fetchData}>fetch data</button>
<ui>{list.map(l => (<li>{l}</li>))}</ul>
</div>
);
}
But from my understanding fetching data is considered a side effect and should be within the use effect. So perhaps something like the following is recommended (I added searchText to have something to trigger the useEffect).
function ComponentThatFetchesList() {
const [list, setList] = useState([]);
const [searchText, setSearchText] = useState("");
useEffect(() => {
asyncFetchData(searchText).then(data => {
setList(list);
});
}, [searchText]);
return (
<div>
<input box to enter search text>
<button that sets search text>
<ui>{list.map(l => (<li>{l}</li>))}</ul>
</div>
);
}
Am I at risk of introducing bugs to be found later if I go with the first method?
Welcome #daniel-longfellow to Stack Overflow πŸ‘‹
The method looks fine as the data loading occurs on a user event.
(And to micro-optimization, you might wrap fetchData in useCallback but if there is no performance issue, it'd only it make it harder to read).
You'd use the second (useEffect) when the component needs to fetch automatically or a state changes.
But as I do not know the exact use-cases, you can go with either one and refactor later on.
And lastly, this question might not be a good Stack Overflow topic as it's not a specific problem but can be opinionated (a recommend question).
Please refer to What topics can I ask about here?
I πŸ‘ as you are a new contributor so won't be aware of it πŸ˜‰.

Multiple state changes from multiple effect hooks

I can't seem to find a good pattern for one scenario...
Lets say we have this kind of order in component:
const component = ({propslist}) => {
const [state1, changeState1] = useState();
const [state2, changeState2] = useState();
useEffect(() => {
//this effect does something and updates state 1
const someVar = someOperation();
changeState1(someVar);
});
useEffect(() => {
//this effect does something and updates state 2
const someVar = someOtherOperation();
changeState2(someVar);
});
return (<div>...</div>);
}
Now, if i understand correctly and from what i see in my tests, the moment first useEffect changes the state, the component will re-render.
The thing that makes me think so is that if i put it that way i get error: Rendered fewer hooks than expected.
2 questions:
Is it the case that the moment something changes the state that component stops execution and goes into re-render?
How to change multiple states from multiple effects? Is there some good pattern about it? Should we have remodel things to pack all state changes into single effects hook or pack all 'chunks' into single state monolith object and change it from single place?
Any suggestions & best practices would be appreciated.
[UPDATE]
My apologies.
I was testing different versions and posted wrong code example.
This is the code example that causes error Rendered fewer hooks than expected.:
const component = ({propslist}) => {
const [state1, changeState1] = useState();
const [state2, changeState2] = useState();
if(someCondition)
changeState1(something);
useEffect(() => {
//this effect does something and updates state 2
const someVar = someOperation();
changeState2(someVar);
});
return (<div>...</div>);
}
So, i guess call to changeState1() starts re-render immediately and prevents useEffect from being called thus causing the error. Right?
To avoid the "Rendered fewer hooks than expected" error, you need to put your useEffect hooks after the if statement.

React JS Freezes Browser

I have a React component which has 2000 elements and based on some filter conditions I update my state, which internally causes re-rendering. Everything seems to be working fine. But when I togglefilter from 2000 elements to say 1000 elements and back&forth, the rendering takes a lot of time and sometimes the browser freezes. I did chrome timeline profiling, the major time consuming piece is rendering. Any help would be appreciated.
As suggested by #enjoylife is a great step but what if you have many components structures in your view, that would be very difficult to debug even memoising the component won't be able to subside the continuous or loop rendering.
I learnt this after I ran into strange freezing and weird error that wouldn't stop any time a user logged in on the homepage. Imagine of all screens. Sometimes, you would hardly notice your component re-rending.
Detect your screen/page (loop) re-rendering with console log
const Home = () => {
conso.log('home re-rending')
// some hooks
return <BigComponent />
}
As written above. The logs must not show more than a limited time deemed after a component has mounted. In my case, it's once. But if it is too much(logs) and would certainly freeze your pc. Therefore, follow the below steps carefully and retrace your steps.
Tips and prerequisite before trying out this proposed solution. Please make sure you have style guide setup e.g. Eslint, it's great. In my case, I reproduced the source code with cra, then sorted out the first and last listed problem which I encountered.
Be careful with the use of React hooks such as useEffect especially. Avoid causing a side effect in a component.
In my case, I created a reusable useUpdateEffect hook and what I intend it to solve as par the name was to detect an update of React props or window props, but it backfires, I won't share the code.
Also, do extra check if you passed correct and expected dependencies, on this Eslint deserve an accolade.
Avoid random keys in React list. Use unique and constant keys in a component list as react depend on it to identify each item. According to react library
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity. You may use the item index as a key as a last resort:
Avoid variable name conflict in your reducer and React component. Please consider the use of style guides as your friend to avoid this fall.
I made the stupid mistake to create a Foo class and use in its render function, which also leads to the freezing scene. Write here for anyone who could meet this problem again.follow this thread.
Avoid infinite loops, Imagine rendering a lot of data at a go. this happen
just in case you share my fate, I urge you to check your loops and make sure you do not have a += instead of -= (or vice versa). Those infinite loops can be quite a big pain in the neck.
Keep your reducer as a reducer, Avoid Action creator, an API call in your reducer or using another reducer in your reducer so, for instance, reducerA in reducerB. When you call to update reducerA in reducerB, the update in reducerA would trigger an update in reducerB whereby cause page/screen to re-render multiple times. for example
// this react reducer in my case
// reducer js file - reducerB
const useBusinesses = () => {
// reducerB as discussed above - the loading context
const { loading } = useLoadingContext(); // the culprit
const [data, setData] = useState(initialState); // initial state,
const [state, dispatch] = useReducer(reducer, data);
useEffect(() => setData(state), [state, setData]);
const { businesses, errorMessage } = state;
const setBusinesses = (payload) => dispatch({ type: `${FETCH_BUSINESSES}_SUCCESS`, data: payload });
const setBusinessesError = (payload) => dispatch({ type: `${FETCH_BUSINESSES}_ERROR`, data: payload });
const fetchBusinesses = async (lglt, type = 'food', limit = 12) => {
try {
// update reducerB: triggers multiple update in reducerA while requesting is pending
loading(FETCH_BUSINESSES, true);
const request = await API.businesses.getWithquery(
`long=${lglt[0]}&latt=${lglt[1]}&limit=${limit}&type=${type}`
);
loading(FETCH_BUSINESSES, false);
setBusinesses(request.data);
} catch (err) {
loading(FETCH_BUSINESSES, false);
// if (!err.response) dispatch(alertMessage(FETCH_BUKKAS, true, 'Please check your network'));
setBusinessesError(err.response.data);
}
});
return { businesses, errorMessage, fetchBusinesses };
};
export const [BusinessesProvider, useBusinessesContext] = constate(useBusinesses);
//home js file
Home = () => {
const { fetchBusinesses } = useBusinessContext();
conso.log('home re-rending')
// some hooks
useEffect(() => {
console.log('am i in trouble, yes!, how many troubles')
fetchBusinesses(coordinates)
}, [fetchBusinesses, coordinates])
return <BigComponent />
}
A quick fix is to implement shouldComponentUpdate See the docs, for whichever child component is being rendered ~2000 times.
shouldComponentUpdate: function(nextProps, nextState) {
return this.props.value !== nextProps.value;
}
Another quick check is to ask yourself if your following the convention of using small, stateless children, passing only props. If not, it might be time to refactor.

Resources