React useEffect Hook when only one of the effect's deps changes, but not the others
I have a functional component using Hooks:
function Component(props) {
const [ items, setItems ] = useState([]);
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, []);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ])
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
The component fetches some items
on mount and saves them to state.
The component receives an itemId
prop (from React Router).
Whenever the props.itemId
changes, I want this to trigger an effect, in this case logging it to console.
The problem is that, since the effect is also dependent on items
, the effect will also run whenever items
changes, for instance when the items
are re-fetched by pressing the button.
This can be fixed by storing the previous props.itemId
in a separate state variable and comparing the two, but this seems like a hack and adds boilerplate. Using Component classes this is solved by comparing current and previous props in componentDidUpdate
, but this is not possible using functional components, which is a requirement for using Hooks.
What is the best way to trigger an effect dependent on multiple parameters, only when one of the parameters change?
PS. Hooks are kind of a new thing, and I think we all are trying our best to figure out how to properly work with them, so if my way of thinking about this seems wrong or awkward to you, please point it out.
The React Team says that the best way to get prev values is to use useRef: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
function Component(props) {
const [ items, setItems ] = useState([]);
const prevItemIdRef = useRef();
useEffect(() => {
prevItemIdRef.current = props.itemId;
});
const prevItemId = prevItemIdRef.current;
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, []);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(() => {
if(prevItemId !== props.itemId) {
console.log('diff itemId');
}
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ])
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
I think that this could help in your case.
Note: if you don't need the previous value, another approach is to write one useEffect more for props.itemId
React.useEffect(() => {
console.log('track changes for itemId');
}, [props.itemId]);
⚠️ NOTE: This answer is currently incorrect and could lead to unexpected bugs / side-effects. The
useCallback
variable would need to be a dependency of theuseEffect
hook, therefore leading to the same problem as OP was facing.I will address it asap
Recently ran into this on a project, and our solution was to move the contents of the useEffect
to a callback (memoized in this case) - and adjust the dependencies of both. With your provided code it looks something like this:
function Component(props) {
const [ items, setItems ] = useState([]);
const onItemIdChange = useCallback(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [items, props.itemId]);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(onItemIdChange, [ props.itemId ]);
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
So the useEffect
just has the ID prop as its dependency, and the callback both the items and the ID.
In fact you could remove the ID dependency from the callback and pass it as a parameter to the onItemIdChange
callback:
const onItemIdChange = useCallback((id) => {
if (items) {
const item = items.find(item => item.id === id);
console.log("Item changed to " item.name);
}
}, [items]);
useEffect(() => {
onItemIdChange(props.itemId)
}, [ props.itemId ])
An easy way out is to write a custom hook to help us with that
// Desired hook
function useCompare (val) {
const prevVal = usePrevious(val)
return prevVal !== val
}
// Helper hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
and then use it in useEffect
function Component(props) {
const hasItemIdChanged = useCompare(props.itemId);
useEffect(() => {
if(hasItemIdChanged) {
// …
}
}, [props.itemId, hasItemIdChanged])
return <></>
}