Too many re-renders on setting state - reactjs

I have the following code which is causing too many renders.
const passAcrossSelectedGame = props => {
if (props.passedGamesFlag === 1) {
props.setPassedGamesFlag(0);
gameDetails = {
blackKingSquare: '',
whiteKingSquare: '',
};
plyViewed = 0;
setHistory(game.history());
const auxGame = new Game();
gameHistory = [];
gameHistory.push(auxGame.fen());
game.history().forEach(move => {
auxGame.move(move);
fenHistory.push(auxGame.fen());
});
}
};
passAcrossSelectedGame(props);
I've identified the offending line as setHistory(game.history());
When I comment out that line, I do not get the constant re-rendering. But I need it in there! What solution might be suitable for this?

You should put your function call modifying the state inside a useEffect hook:
const passAcrossSelectedGame = props => {
if (props.passedGamesFlag === 1) {
props.setPassedGamesFlag(0);
gameDetails = {
blackKingSquare: '',
whiteKingSquare: '',
};
plyViewed = 0;
setHistory(game.history());
const auxGame = new Game();
gameHistory = [];
gameHistory.push(auxGame.fen());
game.history().forEach(move => {
auxGame.move(move);
fenHistory.push(auxGame.fen());
});
}
};
useEffect(() => {
passAcrossSelectedGame(props);
}, [props]
You get this error because setState triggers a rerender, that calls again the setState. Thus creating an infinite loop.

Related

How to synchronous useState with passing state to localstorage

I ran into an asynchronous useState problem.
I have a situation where I first need to add an object to the state array in the handler. And then add this state to the localStorage.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
If I delete the entire localStorage first and run the handler. So an empty array is added to my localStorage (the state shows me updated). After the action again, the required object is finally added to my array.
I tried something like this, but still the same problem.
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
localStorage.setItem(storageItemName, JSON.stringify(tempArray));
How do you solve this, please?
/// EDIT
I have something like this
const FavoriteSong = () => {
const song = { id: 1, name: "Lala", length: "3:20" };
const [favoritedSongs, setFavoritedSongs] = useState([]);
const [isFavorited, setIsFavorited] = useState(false);
useEffect(() => {
if (localStorage.getItem("storageItemName")) {
const storageSongs = JSON.parse(
localStorage.getItem("storageItemName") || ""
);
setFavoritedSongs(storageSongs);
const foundSong = storageSongs?.find((song) => song.id === song.id);
foundSong ? setIsFavorited(true) : setIsFavorited(false);
}
}, [song]);
const handleClick = () => {
if (isFavorited) {
const filteredSong = favoritedSongs.filter(
(song) => song.id !== song.id
);
localStorage.setItem("storageItemName", JSON.stringify(filteredSong));
setIsFavorited(false);
} else {
setFavoritedSongs((prev) => [...prev, song]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
setIsFavorited(true);
}
};
return <div onClick={handleClick}>CLICK</div>;
};
export default FavoriteSong;
Just place your localStorage.set logic inside a useEffect to make sure it runs after the state actually changes.
useEffect() => {
localStorage.setItem(...);
}, [favoritedSongs]};
For that you can Use the condition If data in the array then It will set in localStorage otherwise not
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
tempArray.length && localStorage.setItem(storageItemName, JSON.stringify(tempArray));
.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
FavoritedSongs.length(your state name) && localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));

Too many re-renders with useSelector hook closure

Considering this state, I need to select some data from it:
const initialState: PlacesStateT = {
activeTicket: null,
routes: {
departure: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
arrival: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
},
};
so, I came up with two approaches:
first:
const useCoaches = (dir: string) => {
const name = mapDirToRoot(dir);
const carType = useAppSelector((state) => state.places.routes[name].carriageType);
const infoT = useAppSelector((state) => {
return state.places.activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
});
const coaches = infoT.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carType;
});
return coaches;
};
and second:
const handlerActiveCoaches = (name: string) => (state: RootState) => {
const { carriageType } = state.places.routes[name];
const { activeTicket } = state.places;
const trainInfo = activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
return trainInfo.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carriageType;
});
};
const useActiveInfo = (dir: string) => {
const routeName = mapDirToRoot(dir);
const selectActiveCoaches = handlerActiveCoaches(routeName);
const coaches = useAppSelector(selectActiveCoaches);
return coaches;
};
Eventually, if the first one works ok then the second one gives a lot of useless re-renders in component. I suspect that there are problems with selectActiveCoaches closure, maybe react considers that this selector is different on every re-render but I am wrong maybe. Could you explain how does it work?
selectActiveCoaches finishes with return seatsTrainInfo.filter(). This always returns a new array reference, and useSelector will force your component to re-render whenever your selector returns a different reference than last time. So, you are forcing your component to re-render after every dispatched action:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
One option here would be to rewrite this as a memoized selector with Reselect:
https://redux.js.org/usage/deriving-data-selectors

MutationObserver is reading old React State

I'm attempting to use a MutationObserver with the Zoom Web SDK to watch for changes in who the active speaker is. I declare a state variable using useState called participants which is meant to hold the information about each participant in the Zoom call.
My MutationObserver only seems to be reading the initial value of participants, leading me to believe the variable is bound/evaluated rather than read dynamically. Is there a way to use MutationObserver with React useState such that the MutationCallback reads state that is dynamically updating?
const [participants, setParticipants] = useState({});
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if(name in participants) {
// do something
} else {
setParticipants({
...participants,
[name] : initializeParticipant(name)
})
}
})
}
...
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, [speechObserverOn])
If you are dealing with stale state enclosures in callbacks then generally the solution is to use functional state updates so you are updating from the previous state and not what is closed over in any callback scope.
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participants) {
// do something
} else {
setParticipants(participants => ({
...participants, // <-- copy previous state
[name]: initializeParticipant(name)
}));
}
})
};
Also, ensure to include a dependency array for the useEffect hook unless you really want the effect to trigger upon each and every render cycle. I am guessing you don't want more than one MutationObserver at-a-time.
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, []); // <-- empty dependency array to run once on component mount
Update
The issue is that if (name in participants) always returns false
because participants is stale
For this a good trick is to use a React ref to cache a copy of the current state value so any callbacks can access the state value via the ref.
Example:
const [participants, setParticipants] = useState([.....]);
const participantsRef = useRef(participants);
useEffect(() => {
participantsRef.current = participants;
}, [participants]);
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participantsRef.current) {
// do something
} else {
setParticipants(participants => ({
...participants,
[name]: initializeParticipant(name)
}));
}
})
};

Using useState of complex object not working as expected in ReactJS

I have a function component and I am declaring a useState for a complex object like this:
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
I now try to set the state of the order by calling setOrder in a useEffect like this:
useEffect(() => {
(async function() {
let dh = new DataInterface("some string");
let errMsg = "";
// Get the sales order.
try
{
await dh.FetchOrder();
}
catch(error: any)
{
errMsg = error;
};
setOrder(salesOrder => ({...salesOrder, IsRetrieving: false, ErrorMsg: errMsg, DataInterface: dh}));
})();
}, []);
As is, this seems to work fine. However, I have a setInterval object that changes the screen message while order.IsRetrieving is true:
const [fetchCheckerCounter, setFetchCheckerCount] = useState<number>(0);
const statusFetcherWatcher = setInterval(() => {
if (order.IsRetrieving)
{
if (fetchCheckerCounter === 1)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "This can take a few seconds..."}));
}
else if (fetchCheckerCounter === 2)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "Almost there!.."}));
}
setFetchCheckerCount(fetchCheckerCounter + 1);
}
else
{
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
The issue is that order.IsRetrieving is always true for that code block, even though it does change to false, and my website changes to reflect that, even showing the data from dh.FetchOrder(). That means my timer goes on an infinite loop in the background.
So am I setting the state of order correctly? It's incredibly difficult to find a definite answer on the net, since all the answers are invariably about adding a new item to an array.
Issues
You are setting the interval as an unintentional side-effect in the function body.
You have closed over the initial order.isRetreiving state value in the interval callback.
Solution
Use a mounting useEffect to start the interval and use a React ref to cache the state value when it updates so the current value can be accessed in asynchronous callbacks.
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
const orderRef = useRef(order);
useEffect(() => {
orderRef.current = order;
}, [order]);
useEffect(() => {
const statusFetcherWatcher = setInterval(() => {
if (orderRef.current.IsRetrieving) {
if (fetchCheckerCounter === 1) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "This can take a few seconds...",
}));
} else if (fetchCheckerCounter === 2) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "Almost there!..",
}));
}
setFetchCheckerCount(counter => counter + 1);
} else {
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
return () => clearInterval(statusFetcherWatcher);
}, []);

How to use a method in render reactjs?

i have a method set_data which is used to set data based on id. I know it could be easy to call this set_data in componentdidupdate when id changes. However in doing so it doesnt set some state variables in the parent component.
To get rid of that want to call set_data method in render . However since this set_data method sets state of data it enters into an infinite loop in render . Also cannot provide a condition (like prevprops.id!== this.props.id) to execute set_data method.
To prevent it thought of using this set_data method not to set state at all. and can call this set_data method in render.
Below is the code,
export default class child extends React.Component {
state = {
query: '',
data: null,
};
empty_id = 0xffffffff;
componentDidMount() {
this.set_open_data();
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.set_data();
}
}
set_data = () => {
if (!this.props.info) {
return;
}
if (this.props.id === this.empty_id) {
this.setState({data: null});
return;
}
let data = {
info: [],
values: [],
};
const info = this.props.info;
for (let i=0, ii=info.length; i < ii; i++) {
if (info[i].meshes.includes(this.props.id)) {
const info = info[i].info;
const values = info[i].values;
data = {
info: typeof info === 'string' ? info.split('\r\n') : [],
values: values ? values : [],
};
break;
}
}
this.setState({data: this.filter_data(data, this.state.query)});
};
render = () => {
const shown_data= this.state.data;
/* i want to call set_data method here*/};}
Could someone help me solve this. Thanks.
You can't call setData there, because that would be anti-pattern. It will trigger a loop that will continuously render as well as keeps setting state.
You can probably rewrite the component this way:
export default class child extends React.Component {
state = {
query: ''
};
empty_id = 0xffffffff;
componentDidMount() {
this.set_open_data();
}
set_data = () => {
let data = {};
if (!this.props.info) {
return data;
}
if (this.props.id === this.empty_id) {
return data;
}
let data = {
info: [],
values: [],
};
const info = this.props.info;
for (let i=0, ii=info.length; i < ii; i++) {
if (info[i].meshes.includes(this.props.id)) {
const info = info[i].info;
const values = info[i].values;
data = {
info: typeof info === 'string' ? info.split('\r\n') : [],
values: values ? values : [],
};
break;
}
}
data = this.filter_data(data, this.state.query);
return data;
};
render = () => {
const shown_data= this.state.data;
const data = this.set_data();
/* i want to call set_data method here*/};}
In this, we are not setting data in the state. For every new ID, it will get new data and will compute it from render thereby avoiding antipattern. I have also removed componentDidMount, since we are doing computation in render. Note: This solution means taking away data from the state, if you are not using data anywhere before render, this will work.
Let me know if this helps.

Resources