Component renders twice on async React context update - reactjs

I have React application which manages state using React context. I've created simple counter incrementation reproduction.
There are two contexts. One for storing state, and the second one for dispatching. It's a pattern taken from this article.
The state context just stores single number and there is only one action that can be invoked on this state:
function reducer(state: State, action: Action): State {
switch (action.type) {
case "inc": {
return state + 1;
}
}
}
I also have two helper functions for incrementing value synchronously and asynchronously:
async function asyncInc(dispatch: React.Dispatch<Action>) {
await delay(1000);
dispatch({ type: "inc" });
dispatch({ type: "inc" });
}
function syncInc(dispatch: React.Dispatch<Action>) {
dispatch({ type: "inc" });
dispatch({ type: "inc" });
}
And here is how you use it in the component:
const counter = useCounterState();
const dispatch = useCounterDispatch();
return (
<React.Fragment>
<button onClick={() => asyncInc(dispatch)}>async increment</button>
<button onClick={() => syncInc(dispatch)}>sync increment</button>
<div>{counter}</div>
</React.Fragment>
);
Now, when I click the sync increment button everything will work as expected. It will invoke the inc operation twice, incrementing counter by 2 and perform only one rerender of the component.
When I click the async increment button it will first wait for one second and perform inc operation twice but it will rerender component twice.
You have to open console to see logs from rendering components.
I kinda understand why is that. When it's a synchronous operation, everything happens during component rendering, so it will first manipulate state and render at the end. When it's an asynchronous operation, it will first render component and after one second it will update state once triggering rerender and it will update for the second time triggering next rerender.
So is it possible to perform asynchronous state update doing only one rerender? In the same reproduction, there is similar example but using React.useState that is also having the same problem.
Can, we somehow batch updates? Or I have to create another action that would perform several operations on the state at once?
I've also created this reproduction that solves this problem by taking array of actions but I'm curious if it can be avoided.

Basically what you're seeing for syncInc() is batching for click events. Thus, you only see it render once.
React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.
For your asyncInc(), it is outside the scope of the event handler (due to async) so it is expected you get two re-renders (i.e doesn't batch state updates).
Can, we somehow batch updates?
Yes React can batch updates within an async function.

Unless this is causing a performance problem I would recommend not worrying about additional renders. I found this post about fixing slow renders before worrying about re-renders to be helpful on this topic.
However, it does look like React has an unstable (so it shouldn't be used) API for batching updates ReactDOM.unstable_batchedUpdates. However, using this could cause a headache in the future if it's removed or changed. But to answer your question "can we somehow batch updates?", yes.
const asyncInc = async () => {
await delay(1000);
ReactDOM.unstable_batchedUpdates(() => {
setCounter(counter + 1);
setCounter(counter + 1);
});
};

Related

How to fix memory leak when using redux in useEffect? [duplicate]

I want to cancel some functions after component unmount because it causes memory leak my code looks like this
componentDidUpdate(prevProps) {
if (prevProps.org.org !== this.props.org.org && this.mounted) {
this.props.clearGraph();
this.props.graphGet(this.props.org.org);
this.setState({ org: this.props.org.org });
}
}
componentDidMount() {
const abox = "a" + this.props.org.org.substr("1");
this.props.getHistory(abox);
this.props.graphGet(this.props.org.org);
}
componentWillUnmount() {
}
all I want is to cancel graphGet which is a redux action
You cannot cancel Redux actions by design. Once they get dispatched, they are processed by reducers and the state transition is performed.
You can however dispatch another action to revert the state changes or causing side effects. Normally you would use another library like redux-observable for the latter.
You can for example define 3 actions: START_GRAPH_GET, CANCEL_GRAPH_GET and FINISH_GRAPH_GET. On START you start your fetch, on CANCEL you cancel any outstanding fetches and once a fetch completes you dispatch FINISH and keep the result in the store.
In order to render the results you would need to use react-redux connect with a mapStateToProps function.
To cancel on unmount, you would just dispatch an CANCEL action, if necessary.
Since your code does not show anything related to Redux at all, I think a more general answer is reasonable here.

Cancel Redux Action after Unmount

I want to cancel some functions after component unmount because it causes memory leak my code looks like this
componentDidUpdate(prevProps) {
if (prevProps.org.org !== this.props.org.org && this.mounted) {
this.props.clearGraph();
this.props.graphGet(this.props.org.org);
this.setState({ org: this.props.org.org });
}
}
componentDidMount() {
const abox = "a" + this.props.org.org.substr("1");
this.props.getHistory(abox);
this.props.graphGet(this.props.org.org);
}
componentWillUnmount() {
}
all I want is to cancel graphGet which is a redux action
You cannot cancel Redux actions by design. Once they get dispatched, they are processed by reducers and the state transition is performed.
You can however dispatch another action to revert the state changes or causing side effects. Normally you would use another library like redux-observable for the latter.
You can for example define 3 actions: START_GRAPH_GET, CANCEL_GRAPH_GET and FINISH_GRAPH_GET. On START you start your fetch, on CANCEL you cancel any outstanding fetches and once a fetch completes you dispatch FINISH and keep the result in the store.
In order to render the results you would need to use react-redux connect with a mapStateToProps function.
To cancel on unmount, you would just dispatch an CANCEL action, if necessary.
Since your code does not show anything related to Redux at all, I think a more general answer is reasonable here.

How to Differentiate input vs button state changes in react redux

I've got a simple login dialog that uses redux (just the dialog is shown below for reference). Each time the user types a character into either of the input fields a state change is fired through redux and when the button is pressed a state change also fires.
I plan on changing the state directly when the button is pressed to be something like "LoginPending", and when the REST call returns, the state will change to either "LoggedIn" or "LoginFailed".
My current plan is to add an if statement to the MapStateToProps method that check the old LoginPending status to see if that changed, and if it did, then dispatch the proper action (In reality I will execute a toast notify message).
My question is, "Is this the proper way to check for something like "logged in""? It seems awkward to do it in MapStateToProps, but since there are so many state changes happening (because of the onchange in the input elements), I can't think of a better place to do it.
class App extends Component {
....
render() {
return (
<div className="App">
<input onChange={this.handleChange('username')}/><br/>
<input onChange={this.handleChange('password')}/><br/><br/>
<button onClick={this.handleClick}>Login</button>
</div>
);
}
}
There are several ways to do it. One of the most popular way is to use redux-thunk. docs and example / official recommendation
Then all the logic will reside on the action creator:
export const exampleAction = (username, password) => (dispatch, getState) => {
dispatch({type: "exampleLoading"}); // the store sets a state variable so the view can render something while this "loading" state is true
doApiCall(username,password).then(response => {
// here the feedback message is added on the store, so my feedback manager can render the new feedback message
dispatch({type: "addFeedback", payload:{message:"success!", type:"success"}});
// here the store cleans up the "loading" variable and uses the api response if needed
dispatch({type: "exampleLoaded", payload: response});
});
}

Multiple dispatch calls from component react/redux

I don't really know why I can't get this to work. All the evidence talks against it...This is the situation:
I have a grid of data and a search panel. When the search panel is changed the searchparams are updated and used for updating the data grid.
The thing which triggers the chain is when the user changes the search panel. In my component i handle search panel changes with this:
getPhotos(key, value) {
const change = [{ key: key, value: value},{ key: 'page', value: 1}]
this.props.dispatch(updateSearchParams(change))
console.log('payload.searchParams', this.props.searchParams);
this.props.dispatch(
getPhotos(
{ context:this.props.params.context,
searchParams: this.props.searchParams }
)
);
}
Thus two dispatch calls to action creators form the component. The problem is that the searchparams are not updated in time for the getPhotos call, so the grid is not updated accordingly.
I thought that dispatch calls were synchronous - thus one after the other. I guess that it is the round trip from the component, to the action creator, to the store and reducer which is "screwing" it up.
The first call does not involve any asynchronous calls.
What is the "right" way of doing this? Please be specific about what goes in the component, the action creator and the reducer.
Thanks
dispatch is synchronous (unless you are using some middleware like redux-thunk). But after this.props.dispatch(updateSearchParams(change))
, your component needs to be updated (a re-render) or the this.props.searchParams is still the old one.
You can write this.props.dispatch(getPhotos(...)) in componentWillReceiveProps(nextProps), so you can access the new props (nextProps)
If you are using redux-thunk and two actions updateSearchParams and getPhotos are always bind together, you can create another aggregated action creator for them.
const updateSearchParams = change => dispatch => {
// return a promise here
// or use callback style etc. whatever you prefered
}
const updateSearchParamsAndGetPhotos = (change, context) => dispatch => {
dispatch(updateSearchParams(change))
.then(res => {
dispatch(getPhotos({
context,
searchParams: res.data.searchParams
}))
})
}
So now after dispatching a single action, your component should receive the new photos.
I had it wrong from the beginning.
The searchparams should not go into the store. I can handle the in the component alone - in the state of the component.
This the simplifies and eliminates the problem I described above.
Of cause there could be a situation where the searchparams needed to be available for other components. In that case I would go for #CodinCat answer above with the thunk. It works, i managed to implement it before my realisation.
Thanks

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