This question already has answers here:
Unable to copy array using setstate hook
(3 answers)
Where should I declare functions that are called inside a useEffect() hook?
(2 answers)
Closed 2 years ago.
I have a certain query string I am looking for and when that gets passed on page loading I need to update my user's status to "premium". I have:
useEffect(() => {
const queryString = require('query-string');
const values = queryString.parse(window.location.search);
console.log('before' + user.premium);
if (values.premium !== ''){
setUser({...user, premium: true});
}
console.log('after' + user.premium);
}, []);
I am using React functional components and my user is an object that contains many other values, in addition to "premium" key. From what I understand, passing the empty array [] will cause useEffect to run only on page load. Doing so, however, I get an error:
React Hook useEffect has a missing dependency: 'user'.. BUT, when I include user in that array, the page continually reloads (since I am updating the value of it). I've also tried passing in setUser and that didn't work.
You can pass properties of object as dependencies for useEffect. I have created a working sandbox here.
Also, don't worry to much about the eslint rule react-hooks/exhaustive-deps, view it as more of a guideline and don't be afraid to add a eslint-disable-next-line if it is pushing you to add dependencies that you don't think should be added.
import React, { useEffect, useState } from "react";
export default function App() {
const [user, setUser] = useState({ count: 0, premium: false });
useEffect(() => {
console.log("when component mounts");
}, []);
useEffect(() => {
console.log("when component updates");
});
useEffect(() => {
console.log("when premium changes");
}, [user.premium]);
return (
<div>
<p>Count: {user.count}</p>
<p>Premium: {user.premium}</p>
<button
onClick={() => setUser((prev) => ({ ...prev, count: prev.count + 1 }))}
>
Increment Count
</button>
<button
onClick={() => setUser((prev) => ({ ...prev, premium: !prev.premium }))}
>
Toggle premium
</button>
</div>
);
}
Related
Why does not work the useState for array? i am passing a object for props, now i am printing inside of useEffect his value, at the console.log it have the correct value that value is an array.
i catch that array then i tryed to update the usestate but does not worked
const ComponentHeader = ({ csp }) => {
const [materials, setMaterials] = useState(null)
useEffect(() => {
const { mat } =csp
console.log('Console...', mat)
setMaterials(mat)
}, [csp])
in the template: i am using antd to show in a list
return (
{materials && (
<Form.List initialValue={materials}>
i put a console inside of useEffects
useEffect(() => {
const { mat } =cp
console.log('HEEERRREEE', mat)
}, [])
this is the output:
jsx:26 HEEERRREEE { mat: Array(1)}
jsx:26 HEEERRREEE { mat: Array(2)}
should print 2 elements, but only print 1 element
The useEffect is a hook that handles side effects, and having this second array parameter allows side effects to be run whenever any value changes. so you need to check if csp is changing. Hope this answer your question thanks!
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.
Consider the code :
import React, { useState, useEffect } from 'react';
........ More stuff
const ProductContext = React.createContext();
const ProductConsumer = ProductContext.Consumer;
const ProductProvider = ({ children }) => {
const [state, setState] = useState({
sideBarOpen: false,
cartOpen: true,
cartItems: 10,
links: linkData,
socialIcons: socialData,
cart: [],
cartSubTotal: 0,
cartTax: 0,
cartTotal: 0,
.......
loading: true,
cartCounter: 0,
});
const getTotals = () => {
// .. Do some calculations ....
return {
cartItems,
subTotal,
tax,
total,
};
};
const addTotals = () => {
const totals = getTotals();
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
};
/**
* Use Effect only when cart has been changed
*/
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
..... More code
return (
<ProductContext.Provider
value={{
...state,
............... More stuff
}}
>
{children}
</ProductContext.Provider>
);
};
export { ProductProvider, ProductConsumer };
This is a Context of a Shopping cart ,whenever the user add a new item to the cart
this piece of code runs :
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
And updates the state , however the setState function doesn't update state
when running :
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
Inside addTotals , even though this function is being called automatically when UseEffect detects that state.cartCounter has been changed.
Why aren't the changes being reflected in the state variable ?
Without a stripped down working example, I can only guess at the problems...
Potential Problem 1
You're calling a callback function in useEffect which should be added to it's [dependencies] for memoization.
const dep2 = React.useCallback(() => {}, []);
useEffect(() => {
if(dep1 > 0) {
dep2();
}
}, [dep1, dep2]);
Since dep2 is a callback function, if it's not wrapped in a React.useCallback, then it could potentially cause an infinite re-render if it's changed.
Potential Problem 2
You're mutating the state object or one of its properties. Since I'm not seeing the full code, this is only an assumption. But Array methods like: splice, push, unshift, shift, pop, sort to name a few cause mutations to the original Array. In addition, objects can be mutated by using delete prop or obj.name = "example" or obj["total"] = 2. Again, without the full code, it's just a guess.
Potential Problem 3
You're attempting to spread stale state when it's executed. When using multiple setState calls to update an object, there's no guarantee that the state is going to be up-to-date when it's executed. Best practice is to pass setState a function which accepts the current state as an argument and returns an updated state object:
setState(prevState => ({
...prevState,
prop1: prevState.prop1 + 1
}));
This ensures the state is always up-to-date when it's being batch executed. For example, if the first setState updates cartTotal: 11, then prevState.cartTotal is guaranteed to be 11 when the next setState is executed.
Potential Problem 4
If state.cartCounter is ever updated within this component, then this will cause an infinite re-render loop because the useEffect listens and fires every time it changes. This may or may not be a problem within your project, but it's something to be aware of. A workaround is to trigger a boolean to prevent addTotals from executing more than once. Since the prop name "cartCounter" is a number and is rather ambiguous to its overall functionality, then it may not be the best way to update the cart totals synchronously.
React.useEffect(() => {
if (state.cartCounter > 0 && state.updateCart) {
addTotals();
...etc
}
}, [state.updateCart, state.cartCounter, addTotals]);
Working demo (click the Add to Cart button to update cart state):
If neither of the problems mentioned above solves your problem, then I'd recommend creating a mwe. Otherwise, it's a guessing game.
I have a component that has a state which is an object, and I wanted to set the state in an useEffect where it loops through the keys of the object to update the state. The problem is that it gives me an error for infinite loops.
Uncaught Error: Maximum update depth exceeded. This can happen when a
component repeatedly calls setState inside componentWillUpdate or
componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
I figured that it is because the list of keys I am using as the dep of the useEffect is an array and that is different every time the component re-renders because it is not a primitive type. So I memo’ed it using useMemo but it still doesn’t work then I realized that it might be because the memo’s dep is the state, which is also NOT a primitive type, i.e. an object so it is different every time that results in the list of keys being different every time even with useMemo
Here is the code
const ManageIndexingPage: React.FC = () => {
const dispatch = useDispatch();
const stateFromStore = useSelector(
(state: RootState) => state.settings.indexing
);
const [indexingConfig, setIndexingConfig] = useState<IndexingConfig>(
getDefaultIndexingConfig()
);
const listOfKeys = useMemo(
() => Object.keys(indexingConfig) as Array<keyof typeof indexingConfig>,
[indexingConfig]
);
useEffect(() => {
dispatch(getIndexingConfig());
}, [dispatch]);
useEffect(() => {
listOfKeys.forEach((key) =>
setIndexingConfig((prevState) => ({
...prevState,
[key]: stateFromStore[key]
}))
);
}, [stateFromStore, listOfKeys]);
};
//....
function getDefaultIndexingConfig(): IndexingConfig {
return {
thingIndexingEnabled: false,
thingConnectivityEnabled: false,
thingDeviceShadowEnabled: false,
thingGroupIndexingEnabled: false
};
}
Here listOfkeys is really just an array of strings i.e. ["thingIndexingEnabled", "thingConnectivityEnabled"...]. So if I hardcoded it and use that in useEffect there wouldn't have this error and it's fine
i.e.
useEffect(() => {
["thingIndexingEnabled", "thingConnectivityEnabled"...].forEach((key) =>
setIndexingConfig((prevState) => ({
...prevState,
[key]: stateFromStore[key]
}))
);
}, [stateFromStore]);
How should I get rid of this error without hardcoding the list of keys? Or is there a better way to do it?
The problem lies here:
useEffect(() => {
....
setIndexingConfig((prevState) => ({
...prevState,
[key]: stateFromStore[key]
}))
);
}, [stateFromStore, listOfKeys]);
You should not setState in useEffect() because it is dependent on that an every change will cause the useEffect to reexecute again.
setThe state from another place or use useRef, as it doesnt cause rerender.
I've got the following use case in a React component.
It is a search user input that uses React Autosuggest. Its value is always an ID, so I only have the user ID as a prop. Therefore at first load to show the username value, I need to fetch it at first mount.
EDIT: I don't want to fetch the value again when it changes later, because I already have the value from my suggestions request.
type InputUserProps = {
userID?: string;
onChange: (userID: string) => void;
};
// Input User is a controlled input
const InputUser: React.FC<InputUserProps> = (props) => {
const [username, setUsername] = useState<string | null>(null);
useEffect(() => {
if (props.userID && !username) {
fetchUsername(props.userID).then((username) => setUsername(username));
}
}, []);
async function loadSuggestions(inputValue: string): Suggestion[] {
return fetchSuggestions(inputValue);
}
function selectSuggestion(suggestion: Suggestion): void {
props.onChange(suggestion.userID); // I don't want to rerun the effect from that change...
setUsername(suggestion.username); // ...because I set the username here
}
return (
<InputSuggest
value={props.userID}
label={username}
onSuggestionsRequested={loadSuggestions}
onSuggestionSelected={selectSuggestion}
/>
);
};
export default InputUser;
(I'm adding a simplified way this component is called)
const App: React.FC<AppProps> = (props) => {
// simplified, I use react-hook-form in the real code
const [userID, setUserID] = useState<string?>(any_initial_value_or_null);
return <InputUser userID={userID} onChange={(newValue)=>setUserID(newValue)} />
};
export default App;
It works, but I have the following warning on my useEffect hook
React Hook useEffect has missing dependencies: 'props.userID' and 'username'. Either include them or remove the dependency array.eslint(react-hooks/exhaustive-deps)
But if I do so, I will run it more than once as the username value is changed by the hook itself! As it works without all the dependencies, I'm wondering:
How can I solve my case cleanly?
What are those dependencies for and why is it advised to be exhaustive with them?
Looks like the userId should indeed be a dependency, since if it changes you want to run your query again.
I think you can drop the check for username and always fetch when, and only when the userId changes:
useEffect(() => {
if(props.userID) {
fetchUsername(props.userID)
.then((username) => setUsername(username))
}
}, [props.userID])
Generally speaking, you want to list all closure variables in your effect to avoid stale references when the effect is executed.
-- EDIT to address OP questions:
Since in your use case you know you only want to perform the action on the initial mount passing an empty dependency array is a valid approach.
Another option is to keep track of the fetched userID, e.g.
const fetchedIds = useRef(new Set())
whenever you fetch username for a new ID you can update the ref:
fetchedIds.current.add(newId)
and in your effect you can test:
if (props.userID && !fetchedIds.current.has(props.userID)) {
// do the fetch
}
What are those depdendencies?
useEffect takes an optional second argument which is an array of dependencies. Dependencies of the useEffect hook tell it to run the effect whenever one if its dependency changes.
If you don't pass an optional second argument to useEffect hook, it will execute every time the component re-renders. Empty dependency array specifies that you want to run the effect only once, after the initial render of the component. In this case, useEffect hook will almost behave like componentDidMount in class components.
why is it advised to be exhaustive on them ?
Effects see props and state from the render they were defined in. So when you use something from the scope of functional component that participates in react's data flow like props or state, inside the callback function of useEffect hook, that callback function function will close over that data and unless new effect is defined with the new values of props and state, your effect will see the stale props and state values.
Following code snippet demonstrates what could go wrong if you lie about the dependencies of useEffect hook.
In the following code snippet, there are two components, App and User. App component has three buttons and maintains the id of the user which is displayed by the User component. User id is passed as a prop from App to User component and User component fetches the user with the id, passed as prop, from the jsonplaceholder API.
Now the problem in the following code snippet is that it doesn't works correctly. Reason is that it lies about the dependency of the useEffect hook. useEffect hook depends on userID prop to fetch the user from the API but as i skipped adding userID as a dependency, useEffect hook doesn't executes every time userID prop changes.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, []);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Above code snippet shows one of the few problems that could arise if you lie about the dependencies and this is why you must not skip or lie about the dependencies of the useEffect hook or any other hook that has a dependency array, for example useCallback hook.'
To fix the previous code snippet, you just have to add userID as a dependency to the dependency array of useEffect hook so that if executes whenever userID prop changes and new user is fetched with the id equal to userID prop.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, [userID]);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In your case, if you skip adding props.userID in the dependency array of useEffect hook, your effect will not fetch the new data when the prop userID changes.
To learn more about the negative impacts of omitting the dependencies of useEffect hook, see:
Don’t Lie to React About Dependencies
How can I solve my case cleanly ?
Since your effect depends on the prop value userID, you should include it in the dependency array to always fetch the new data whenever the userID changes.
Adding props.userID as a dependency to useEffect hook will trigger the effect every time props.userID changes but the problem is that you are unnecessarily using username inside the useEffect. You should remove it because that is not needed since username value doesn't and shouldn't decide when new user data should be fetched. You just want the effect to run whenever props.userID changes.
You could also decouple the action from the state update by using the useReducer hook to manage and update the state.
Edit
Since you only want to run the effect even when userID is used by useEffect hook, in your case it is ok to have an empty array as the second argument and ignore the eslint warning. You could also not omit any dependencies of the useEffect hook and use some condition in useEffect hook that evaluates to false after the effect runs for the first time and updates the state.
Personally i would suggest you to try to change how your components are structured so that you don't have to deal with this kind of problem in the first place.
If you are sure that before mounting the InputUser component, all dependent props have been filled and they have the right value, then add eslint-disable-next-line react-hooks/exhaustive-deps right before }, []) line, but If depended props have no value at first component mounting, So you have to add them in useEffect dependencies.