I've been playing around with the new hook system in React 16.7-alpha and get stuck in an infinite loop in useEffect when the state I'm handling is an object or array.
First, I use useState and initiate it with an empty object like this:
const [obj, setObj] = useState({});
Then, in useEffect, I use setObj to set it to an empty object again. As a second argument I'm passing [obj], hoping that it wont update if the content of the object hasn't changed. But it keeps updating. I guess because no matter the content, these are always different objects making React thinking it keep changing?
useEffect(() => {
setIngredients({});
}, [ingredients]);
The same is true with arrays, but as a primitive it wont get stuck in a loop, as expected.
Using these new hooks, how should I handle objects and array when checking weather the content has changed or not?
Passing an empty array as the second argument to useEffect makes it only run on mount and unmount, thus stopping any infinite loops.
useEffect(() => {
setIngredients({});
}, []);
This was clarified to me in the blog post on React hooks at https://www.robinwieruch.de/react-hooks/
Had the same problem. I don't know why they not mention this in docs. Just want to add a little to Tobias Haugen answer.
To run in every component/parent rerender you need to use:
useEffect(() => {
// don't know where it can be used :/
})
To run anything only one time after component mount(will be rendered once) you need to use:
useEffect(() => {
// do anything only one time if you pass empty array []
// keep in mind, that component will be rendered one time (with default values) before we get here
}, [] )
To run anything one time on component mount and on data/data2 change:
const [data, setData] = useState(false)
const [data2, setData2] = useState('default value for first render')
useEffect(() => {
// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
}, [data, data2] )
How i use it most of the time:
export default function Book({id}) {
const [book, bookSet] = useState(false)
const loadBookFromServer = useCallback(async () => {
let response = await fetch('api/book/' + id)
response = await response.json()
bookSet(response)
}, [id]) // every time id changed, new book will be loaded
useEffect(() => {
loadBookFromServer()
}, [loadBookFromServer]) // useEffect will run once and when id changes
if (!book) return false //first render, when useEffect did't triggered yet we will return false
return <div>{JSON.stringify(book)}</div>
}
I ran into the same problem too once and I fixed it by making sure I pass primitive values in the second argument [].
If you pass an object, React will store only the reference to the object and run the effect when the reference changes, which is usually every singe time (I don't now how though).
The solution is to pass the values in the object. You can try,
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [Object.values(obj)]);
or
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [obj.keyA, obj.keyB]);
If you are building a custom hook, you can sometimes cause an infinite loop with default as follows
function useMyBadHook(values = {}) {
useEffect(()=> {
/* This runs every render, if values is undefined */
},
[values]
)
}
The fix is to use the same object instead of creating a new one on every function call:
const defaultValues = {};
function useMyBadHook(values = defaultValues) {
useEffect(()=> {
/* This runs on first call and when values change */
},
[values]
)
}
If you are encountering this in your component code the loop may get fixed if you use defaultProps instead of ES6 default values
function MyComponent({values}) {
useEffect(()=> {
/* do stuff*/
},[values]
)
return null; /* stuff */
}
MyComponent.defaultProps = {
values = {}
}
Your infinite loop is due to circularity
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({}); will change the value of ingredients(will return a new reference each time), which will run setIngredients({}). To solve this you can use either approach:
Pass a different second argument to useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants will run when timeToChangeIngrediants has changed.
I'm not sure what use case justifies change ingrediants once it has been changed. But if it is the case, you pass Object.values(ingrediants) as a second argument to useEffect.
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
As said in the documentation (https://reactjs.org/docs/hooks-effect.html), the useEffect hook is meant to be used when you want some code to be executed after every render. From the docs:
Does useEffect run after every render? Yes!
If you want to customize this, you can follow the instructions that appear later in the same page (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). Basically, the useEffect method accepts a second argument, that React will examine to determine if the effect has to be triggered again or not.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
You can pass any object as the second argument. If this object remains unchanged, your effect will only be triggered after the first mount. If the object changes, the effect will be triggered again.
I'm not sure if this will work for you but you could try adding .length like this:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
In my case (I was fetching an array!) it fetched data on mount, then again only on change and it didn't go into a loop.
If you include empty array at the end of useEffect:
useEffect(()=>{
setText(text);
},[])
It would run once.
If you include also parameter on array:
useEffect(()=>{
setText(text);
},[text])
It would run whenever text parameter change.
I often run into an infinite re-render when having a complex object as state and updating it from useRef:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);
In this case eslint(react-hooks/exhaustive-deps) forces me (correctly) to add ingredients to the dependency array. However, this results in an infinite re-render. Unlike what some say in this thread, this is correct, and you can't get away with putting ingredients.someKey or ingredients.length into the dependency array.
The solution is that setters provide the old value that you can refer to. You should use this, rather than referring to ingredients directly:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.
I believe they are trying to express the possibility that one could be using stale data, and to be aware of this. It doesn't matter the type of values we send in the array for the second argument as long as we know that if any of those values change it will execute the effect. If we are using ingredients as part of the computation within the effect, we should include it in the array.
const [ingredients, setIngredients] = useState({});
// This will be an infinite loop, because by shallow comparison ingredients !== {}
useEffect(() => {
setIngredients({});
}, [ingredients]);
// If we need to update ingredients then we need to manually confirm
// that it is actually different by deep comparison.
useEffect(() => {
if (is(<similar_object>, ingredients) {
return;
}
setIngredients(<similar_object>);
}, [ingredients]);
The main problem is that useEffect compares the incoming value with the current value shallowly. This means that these two values compared using '===' comparison which only checks for object references and although array and object values are the same it treats them to be two different objects. I recommend you to check out my article about useEffect as a lifecycle methods.
The best way is to compare previous value with current value by using usePrevious() and _.isEqual() from Lodash.
Import isEqual and useRef. Compare your previous value with current value inside the useEffect(). If they are same do nothing else update. usePrevious(value) is a custom hook which create a ref with useRef().
Below is snippet of my code. I was facing problem of infinite loop with updating data using firebase hook
import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
useUserStatistics
} from '../../hooks/firebase-hooks'
export function TMDPage({ match, history, location }) {
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
const userId = match.params ? match.params.id : ''
const teamId = location.state ? location.state.teamId : ''
const [userStatistics] = useUserStatistics(userId, teamId)
const previousUserStatistics = usePrevious(userStatistics)
useEffect(() => {
if (
!isEqual(userStatistics, previousUserStatistics)
) {
doSomething()
}
})
In case you DO need to compare the object and when it is updated here is a deepCompare hook for comparison. The accepted answer surely does not address that. Having an [] array is suitable if you need the effect to run only once when mounted.
Also, other voted answers only address a check for primitive types by doing obj.value or something similar to first get to the level where it is not nested. This may not be the best case for deeply nested objects.
So here is one that will work in all cases.
import { DependencyList } from "react";
const useDeepCompare = (
value: DependencyList | undefined
): DependencyList | undefined => {
const ref = useRef<DependencyList | undefined>();
if (!isEqual(ref.current, value)) {
ref.current = value;
}
return ref.current;
};
You can use the same in useEffect hook
React.useEffect(() => {
setState(state);
}, useDeepCompare([state]));
You could also destructure the object in the dependency array, meaning the state would only update when certain parts of the object updated.
For the sake of this example, let's say the ingredients contained carrots, we could pass that to the dependency, and only if carrots changed, would the state update.
You could then take this further and only update the number of carrots at certain points, thus controlling when the state would update and avoiding an infinite loop.
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
An example of when something like this could be used is when a user logs into a website. When they log in, we could destructure the user object to extract their cookie and permission role, and update the state of the app accordingly.
my Case was special on encountering an infinite loop, the senario was like this:
I had an Object, lets say objX that comes from props and i was destructuring it in props like:
const { something: { somePropery } } = ObjX
and i used the somePropery as a dependency to my useEffect like:
useEffect(() => {
// ...
}, [somePropery])
and it caused me an infinite loop, i tried to handle this by passing the whole something as a dependency and it worked properly.
Another worked solution that I used for arrays state is:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);
This is my first post here, so nice to meet you all. I have recently started my adventure with React, so my question probably will not be very challenging (it could also be very basic stuff), however I got stuck with this problem for a while.
So I am working with WebSocket that when connected, sends one-time message with all products currently in stock. Then, every few seconds, it sends object with update of product stock changes.
I managed to update state with first recieved message, but then, when I try to access state in a function handleData, state is empty array. This is happening despite the fact that the data rendered on the page using state is still visible, state is visible in Firefox Developer Edition React DevTools, and the useEffect that is associated with the state change fires only once - at the start, so it doesn't change.
I want to be able to access data I put before in state, in function handleData, so that I can update the state with stock changes. It is interesting that when the websocket loads again, the "products" status variable is extended by the previous, unloaded products.
Here is code of my component:
import React, { useState, useEffect, useRef } from 'react';
import Product from './Product';
export default function ProductsList() {
const [products, setProducts] = useState([]);
const ws = useRef(null);
useEffect(() => {
ws.current = new WebSocket(<HERE IS WEBSOCKET URL BUT I CENSORED IT>);
ws.current.onmessage = (message) => handleData(message);
return () => ws.current.close();
},[]);
useEffect(() => {
console.log("Products useEffect:", products)
}, [products]) //prints fine here
const handleData = (message) => {
const data = JSON.parse(message.data);
switch (data.operation) {
case 'product.stock.decreased': {
console.log("Products Switch:", products) // prints empty array here
break;
}
default: {
setProducts(prevState => [...prevState, ...data]) // here state updates
break;
}
}
}
return (
<div className="ProductsList">
{products.map(p => <Product key={p.productId} productId={p.productId} name={p.name} price={p.price} stock={p.stock} />)}
</div>
);
}
Many thanks in advance for your help.
I make an request in react js and i get data using useSelector. So in useEffect i show the message from backend after the request.
useEffect(() => {
if (selector.response ? .message) {
console.log(selector);
message.success(selector.response?.message, 2);
setLoading(false);
}
console.log('render');
}, [selector.response]);
The code works fine, but appears an issue when i change the page(route). Clicking on another menu item i go to another page and when i come back, the useEffect is triggered again and user again sees the message from message.success(selector.response?.message, 2);. Question: How to stop showing the message each time after i come back to my route and to show the message just one time?
You have to use User Effects with Cleanup
You need to clean up the effects from the previous render or actions. Otherwise it will hold the previous state
You will find more details in official tutorial
userEffect Cleanup Process
Update Answer
import React, {useState, useEffect} from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
function handleChange(val) {
setCount(val)
}
window.addEventListener('load', handleChange(1))
return () =>{
handleChange(0);
console.log(count)
};
})
return (
<div className="App">
{count}
</div>
);
}
Here I take a count variable and setCount methods to set the count variable
Inside useEffect I create a function which will be responsible for setting up count value
Then I create an addEventListner when the page will load it will set the count value to 1
Then I call an anonymous function for clean up things which will remove the previously set value to 0
After that I set a console to just check if its sets the value
So when user come back to your page again initially it will find the default value then set the dynamic value.
You can make it more efficient way. I just give you an example
Below I share a link which will help you to handle api response
How to call api and cleanup response using react usereffect hooks
It seems like in your case your component unmounts upon page change and remounts when the user comes back. When the componet remounts, the useEffect always fires. That's simply how it works.
Answering your question, this is a simple thing you can do for your case.
// this will create a function that will return true only once
const createShouldShowSuccessMessage = () => {
let hasShownMessage = false;
()=> {
if(hasShownMessage) {
return false;
} else {
hasShownMessage = true;
return true;
}
}
};
const shouldShowSuccessMessage = createShouldShowSuccessMessage();
const SomeComponent = () => {
useEffect(() => {
if(shouldShowSuccessMessage()) {
message.success(selector.response?.message, 2);
}
setLoading(false);
}, [selector.response]);
}
I would advise you got with a better setup that performing your side effects inside your components but I hope my answer helps you till you get there
This question already has answers here:
react useEffect comparing objects
(7 answers)
Closed 2 years ago.
I have an app that checks the user id on startup and loads the list of todo items based on the user logged in. I have the useEffect change only when data changes, but I have setData in the body of useEffect() meaning data changes and it re-runs infinitum.
However if I change [data] to [] in the second parameter, then it renders once BUT I have to refresh the page everytime I add a todo item for it to render rather than it render automatically. How can I have it render automatically without looping infinitely?
const [data, setData] = useState([])
useEffect(() => {
UserService.getUserById(localStorage.getItem("userId")).then(res => {
if (res.data !== null) {
setData(res.data.todos)
}
})
}, [data])
You can add a condition in the call back function that checks if a certain condition is met, e.g. if data is empty. If it is empty, then fetch data, otherwise do nothing. This will prevent the infinite loop from happening.
const getData = useEffect(()=>{
const fetchData = () => {
UserService.getUserById(localStorage.getItem("userId"))
.then(res => {
if (res.data !== null) {
setData(res.data.todos)
}
})
.catch(error => {
// do something with error
})
}
if (data.length === 0)
fetchData()
},[data]);
Alternatively, you use an empty dependency array so that the callback function in the useEffect is called once.
useCallback Hook can be used with slight modifications in your code.
You will need to import useCallback from "react" first.
import {useCallback} from "react";
And then use this useCallback around our getData function. (Have modified the answer a bit)
const getData = useCallback(()=>{
UserService.getUserById(localStorage.getItem("userId")).then(res => {
if (res.data !== null) {
setData(res.data.todos)
}
})
},[data]);
useEffect(() => {
getData();
}, [data])
This React Hook will make sure that the getData() function is only created when the second argument data changes.
In your code UserService.getUserById(localStorage.getItem("userId")) return a promise and it get data one time so you just have to call getUserById one time at the time of load by using [] and if you want to call it again make a function and use it wherever on refresh function or on adding todos item or update or delete function. Otherwise you have to use observable or useCallBack hook
You need to pass the reset param to prevent loop. once callback trigger reset value false. so that execution not running again until reset the value
Codesanbox
export default function App() {
let i = 1;
const [data, setData] = useState([]);
const [reset, setReset] = useState(true);
useEffect(() => {
if (reset) {
setTimeout(() => {
//callback
setReset(false);
setData(Math.random());
}, 1000);
}
}, [data]);
return (
<div className="App">
<h1>{data}</h1>
<button
onClick={() => {
setReset(true);
setData("");
}}
>
Click this and see the data render again. i just reset the data to empty
</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
Use a condition to stop the loop by setting a condition to stop it. You can check if a certain value is set or check if there are any values sent at all.
I've a function (loadList) in my component to load my list data from API
And at run-time I want to get all data, so I've this code:
useEffect(() => {
loadList();
}, []);
And I've a state (changedFilter) to handle reload list again :
useEffect(() => {
loadList();
}, [changedFilter]);
But after checking the DevTools > Network tab, I've 2 XHR request in this scenario ...
Apparently by default React consider initial values for useState as a change and run the second useEffect with sense on changedFilter state ...
Is any trick to handle it ?
Thanks
useEffect(() => { loadList(); }, [changedFilter]);
will also be trigger on first render as well along with first useEffect therefore your fist useeffect with [] dependency is redundant. Only second useEffect might solve your purpose.
You can avoid the useEffect from running on initial render in 2 ways.
First: Use a ref to track initial render
const initialRender = useRef(true);
useEffect(() => {
if(!initialRender.current) {
loadList();
} else {
initialRender.current = false;
}
}, [changedFilter]);
Second: Check for the existence of changedFilter if its not defined or empty initially
useEffect(() => {
if(changedFilter) { // Change it to changeFilter.length > 0 if its an empty array initially
loadList();
}
}, [changedFilter]);