ES lint expecting to pass all the dependencies to useEffect hook - javascript

I have a react component using a useState hook. This hook is called inside a useEffect hook
const [items, setItems] = useState({});
useEffect(() => {
const newItem = {
[item._id]: options
};
setItems({...items, ...newItem});
}, [options]);
this works as expected, when options changes, I create a new item with those options.
However, when I run lint I get an alert saying the useEffect is missing some dependency, but I can't pass item as a dependency as this will result in an infinite loop.
Am I missing something or there's a better way to do this and I should always pass all the properties used inside the useEffect hook as dependencies?

If you do not want to have to pass items into the dependency array, and create an infinite loop, you can pass a callback function to setItems in order to access the items state.
const [items, setItems] = useState({});
useEffect(() => {
setItems((currentItems) => { // Access the current `items` state
const newItem = {
[currentItem._id]: options
};
return { ...currentItems, ...newItem };
});
}, [options]);

Related

Updating a dependency variable after execution of useEffect without triggering that useEffect hook again

As shown in the following react code snippet, I have a useEffect hook with a dependency 'fetchQuery'. The useEffect hook will execute if 'fetchQuery' variable changes. But right after execution of it, I want to clear the value (set to '') of that dependency variable 'fetchQuery' without triggering an infinite loop. Is there a way to do that?
useEffect(() => {
// An async function to fetch some data
getDataModules(fetchQuery);
}, [fetchQuery]);
Since 'fetchQuery' is a state variable and is given as a dependency for this hook, simply clearing its value using setFetchQuery(''); will trigger useEffect again, which I dont want.
Only call the API if fetchQuery exists.
useEffect(() => {
if (fetchQuery) {
getDataModules(fetchQuery);
}
}, [fetchQuery]);
Do setFetchQuery(''); to clear.
const [shouldRefetch, setShouldRefetch] = useState(false);
const [fetchQuery, setFetchQuery] = useState(...);
useEffect(() => {
if (shouldRefetch) {
getDatamodules(fetchQuery);
setShouldRefetch(false);
setFetchQuery('');
}
}, [shouldRefetch]);
const someOtherFunction = () => {
setFetchQuery(...);
SetShouldRefetch(true);
};

useEffect hook spamming requests [duplicate]

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]);

ReactJS component won't update after first API call

So I recently started to discover ReactJS. I have a simple Spring Boot api which has a few methods. One of these returns a list of objects. I get these in my frontend by using Axios to make the HTTP call.
export function getItems() {
const [items, setItems] = useState([]);
useEffect(async () => {
await client.get('items').then((result) => {
setItems(result.data);
});
}, []);
return items;
The items are mapped in a gallery component and shown on screen. Next to this, I have an addItem function which posts an item object obtained through a form to my api.
export async function addPet(newPet) {
await client.post(
'/pets',
newPet,
);
}
My AddPet component is loaded inside my Gallery component. The form is shown on the right side of the screen and when I click the "Add Item" button, I want the item to be added and my list of items reloaded to show the newly added item. Right now, I can not get this working in a correct way. If I remove the "[]" from the final part of my useEffect() in my getItems() functions, everything seems to work but in reality the app is making the getItems call over and over again. If I add "[]", the call is only made once at the start, but will not re-render the gallery component when an item is added. The handleSubmit() for my "Add item" button is as follows:
const handleSubmit = () => {
const newItem = new Item();
newItem .name = formValue.name;
newItem .image = formValue.image;
newItem .pitemText = formValue.itemText;
addItem(newItem);
};
So my question here is: how can I get that gallery component to re-render whenever I add a new item or delete one of the items? I figure I need a way to change the state of the component but I can't seem to get it done.
The second parameter of useEffect (the Array) has an important role: the items in that array trigger the useEffect to re-run.
Some cases:
useEffect(() => {}, []): runs once, after the component is mounted
useEffect(() => {}, [var1, var2,..., varn]): runs when var1 or var2 or varn is updated
useEffect(() => {}): runs on every completed re-render (default behavior)
More on useEffect: useEffect hook
So, your code works as expected:
useEffect(() => {
client.get('items').then((result) => {
setItems(result.data);
});
}, []); // -> runs once, when component is mounted
useEffect(() => {
client.get('items').then((result) => {
setItems(result.data);
});
}, [item]); // -> runs when the variable named item changes
you need to organize your code in such a way, that this useEffect hook can run on the update of the variable whose change you want to watch.
dont pust async as the first parameter of useEffect hook as below, wont work well
useEffect(async () => {
await client.get('items').then((result) => {
setItems(result.data);
});
}, []);
instead you can use external function or IIEF function as below
useEffect(() => {
(async () => {
await client.get('items').then((result) => {
setItems(result.data);
});
})()
}, []);

Why does adding a memoized function in useEffect dependency array cause an error?

I have a function that is being used in my useEffect hook and I had to add in the dependency array. I also have some other dependencies that are being passed as props from an external component. Because of that dependency function, I had to wrap my function in a useCallback for it to avoid to change on every render. But I'm confused why the error is still popping out? And also how should I get accross it ?
import _ from "lodash";
import React, { useCallback, useEffect, useState } from "react";
function App({uniqueCol,data,handleSelect, unselectAll = false}) {
const [selected, setSelected] = useState(new Set(""));
const [currentRows, setCurrentRows] = useState(data);
//here it is fine when I add currentRows and uniqueCol as dependencies
const changeSelect = useCallback(
(id: string, status: boolean) => {
const cr = currentRows.map((row) => {
if (uniqueCol) {
// #ts-ignore
if (row[uniqueCol] == id) row.selected = status;
}
return row;
});
setCurrentRows(cr);
},
[currentRows, uniqueCol]
);
useEffect(() => {
selected.forEach((sel) => changeSelect(sel, true));
setCurrentRows(data);
}, [changeSelect, data, selected]);
/*here it causes errors when I add changeSelect, currentRows,
handleSelect as dependencies but works fine when I remove them*/
const unSelectAll = useCallback(() => {
setSelected(new Set(""));
_.map(currentRows, "id").forEach((val) => {
changeSelect(val, false);
});
if (handleSelect) handleSelect([]);
}, [changeSelect, currentRows, handleSelect]);
//another useEffect that is using the function unSelectAll as a dependency
useEffect(() => {
unSelectAll();
}, [unSelectAll, unselectAll]);
return <h1>Anything here</h1>;
}
The error I'm getting is:
react-dom.development.js:67 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
The code that you have posted doesn't include "the full story", but I feel it is reasonable to assume the following, because you said "I had to wrap my function in a useCallback for it to avoid to change on every render",
but your usage of useCallback() has practically no effect here:
changeSelect will change on every render, because:
changeSelect has currentRows in the dependencies, and
changeSelect calls setCurrentRows()
unSelectAll will change on every render, because:
unSelectAll has changeSelect in the dependencies, and
changeSelect changes on every render
So a simplified version to illustrate the "recursive" dependencies would be:
function App(){
const [currentRows, setCurrentRows] = useState(data);
const changeSelect = useCallback(
() => { setCurrentRows([]); },
[ currentRows, uniqueCol ]
);
const unSelectAll = useCallback(
() => { changeSelect(); },
[ changeSelect, currentRows ]
);
return <>...</>;
}
This is only a guess, but probably some other code - probably the useEffect which you mentioned, but haven't posted the code - is using unSelectAll
as a dependency, and changes maybe uniqueCol, so that
App re-renders if uniqueCol changes
changeSelect changes on every render
unSelectAll changes because changeSelect changes
uniqueCol changes because unSelectAll changes (probably ? in some other code ?)
At least probably this is the root cause, and the actual recursive chain (the infinite loop) is closed somewhere else, for this or a similar reason.

state is not updating using React hook

This is a function which has to be called when the component mounts on DOM
const [dashboardData, setDashboardData] = useState('');
const loadDashboardData = () => {
console.log("Loading Dashboard Data ", campaign);
Utils.request({
url: `campaign/user/info`
}).then(
res => {
console.log("dashboard data" , res.data)
setDashboardData(res.data);
},
err => console.log(err)
)
}
useEffect(() => {
loadDashboardData();
console.log("campaigndata",dashboardData);
}, []);
when I console dashboardData in useEffect, it shows nothing but a string i.e campaigndata which I passed as the first argument in console.log. what I think that my dashboard state variable is not getting updated
Answer
Write another useEffect just for dashboardData.
useEffect(() => {
console.log("campaigndata",dashboardData);
}, [dashboardData]);
Explanation
Your useEffect is taking an empty array as the second argument, which makes it run only the first time (as DidMount), so it won't re-run on component re-render after changing the state, that's why it is showing empty string as the initial state.
Writing another useEffect for the variable dashboardData will run as many times as the dashboardData changes.
You can just pass the 'dashboard' to the existing useEffect as dependency. The existing useEffect will work as both componentDidMount and componentDidUpdate.
So no need to write another useEffect.
useEffect(() => {
loadDashboardData();
console.log("campaigndata",dashboardData);
}, [dashboardData]);

Categories

Resources