Does the dependency array in React hooks really need to be exhaustive? - reactjs

According to the React docs:
every value referenced inside the effect function should also appear in the dependencies array
If my effect function references a few variables from the outer scope but I only want it to be executed when one of them changes, why do I need to specify all the other variables in the dependency array? Yes, the closure will become stale if the other variables change but I don't care because I don't need the function to be called yet. When the variable that I care about changes then the new closure with the values at that moment can be called. What am I missing?
Here's a working example (to my knowledge) where the useEffect dependency arrays are not exhaustive:
import React, { useEffect, useState } from "react";
const allCars = {
toyota: ["camry", "corolla", "mirai"],
ford: ["mustang", "cortina", "model T"],
nissan: ["murano", "micra", "maxima"],
};
function CarList() {
const [cars, setCars] = useState([]);
const [brand, setBrand] = useState("toyota");
const [filterKey, setFilterKey] = useState("");
useEffect(() => {
// I don't want to run this effect when filterKey changes because I wanna wrap that case in a timeout to throttle it.
setCars(allCars[brand].filter(model => model.startsWith(filterKey)));
}, [brand]);
useEffect(() => {
// This effect is only called when filterKey changes but still picks up the current value of 'brand' at the time the function is called.
const timeoutId = setTimeout(() => {
setCars(allCars[brand].filter(model => model.startsWith(filterKey)));
}, 500);
return () => clearTimeout(timeoutId);
}, [filterKey]);
const handleChangeFilterKey = event => {
setFilterKey(event.target.value);
};
return (
<div>
{`${brand} cars`}
<div>Select brand</div>
<input type="radio" value="toyota" checked={brand === "toyota"} onChange={() => setBrand("toyota")} />
<input type="radio" value="ford" checked={brand === "ford"} onChange={() => setBrand("ford")} />
<input type="radio" value="nissan" checked={brand === "nissan"} onChange={() => setBrand("nissan")} />
<div>Filter</div>
<input label="search" value={filterKey} onChange={handleChangeFilterKey} />
<ul>
{cars.map(car => (
<li>{car}</li>
))}
</ul>
</div>
);
}
Are there any pitfalls to the above example?

Yes, you should follow this rule all the time, when you find your code break with following it, that means good practice is not followed. That is the meaning of this rule, make sure you design code well.
I imagine in your case the code looks like this:
const Test = () => {
const [wantToSync] = useState(0)
const [notWantToSync] = useState(0) // notWantToSync also might come from props, i'll use state as example here
useEffect(() => {
fetch('google.com', {
body: JSON.stringify({wantToSync, notWantToSync})
}).then(result => {
// ...
})
}, [wantToSync]) // component is supposed to be reactive to notWantToSync, missing notWantToSync in dep is dangerous
}
If notWantToSync was defined as a state of the component, the component is supposed to be reactive to it, including useEffect. If that is not what you want, notWantToSync shouldn't be state from start.
const Test = () => {
const [wantToSync] = useState(0)
const notWantToSyncRef = useRef(0) // hey I don't want notWantToSync to be reactive, so i put it in useRef
useEffect(() => {
fetch('google.com', {
body: JSON.stringify({wantToSync, notWantToSync: notWantToSyncRef.current})
}).then(result => {
// ...
})
}, [wantToSync, notWantToSyncRef]) // yeah, now eslint doesn't bother me, and notWantToSync doesn't trigger useEffect anymore
}
Normally you don't need to do if else in useEffect to stop re-rendering, there're other approaches like useMemo or useCallback to do similar things in addition to useRef although they have different using context.
I see your struggle in the new example, so you want to do a throttle, and if filterKey is added to the first useEffect dep, the throttle will be broken. My opinion is that when you find yourself in a situation like this, that often means there's better practice(eslint exhaustive help to identify it), for example, extract throttle logic to a hook: https://github.com/streamich/react-use/blob/master/src/useThrottle.ts.
Exhaustive deps is not something vital to follow, it's just a good practice to make sure your code is designed well. The above example proves it that

In your case there is no side-effect, it's just data derived from state, so you should use different tools instead of useEffect.
First filter could be implemented with just getting value by key:
const carsByBrand = allCars[brand];
Complexity of this operation is constant, so memoization will be too expensive.
And second filter is debounced, but you might want to memoize it because of filtering with linear complexity. For these purposes you can use for example useDebouncedMemo hook:
const filteredCars = useDebouncedMemo(() => {
return carsByBrand.filter(model => model.startsWith(filterKey)
}, [filterKey, carsByBrand], 500)
In this case dependencies of hook are exhaustive and logic is much more declarative.

Related

How to properly implement "subscription"-like fetches with React useEffect

I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"
If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}
This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);

react-hooks/exhaustive-deps and empty dependency lists for "on mount" [duplicate]

This is a React style question.
TL;DR Take the set function from React's useState. If that function "changed" every render, what's the best way to use it in a useEffect, with the Effect running only one time?
Explanation We have a useEffect that needs to run once (it fetches Firebase data) and then set that data in application state.
Here is a simplified example. We're using little-state-machine, and updateProfileData is an action to update the "profile" section of our JSON state.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
}, []);
return (
<p>Hello, world!</p>
);
}
However, ESLint doesn't like this:
React Hook useEffect has a missing dependency: 'actions'. Either include it or remove the dependency array
Which makes sense. The issue is this: actions changes every render -- and updating state causes a rerender. An infinite loop.
Dereferencing updateProfileData doesn't work either.
Is it good practice to use something like this: a single-run useEffect?
Concept code that may / may not work:
const useSingleEffect = (fxn, dependencies) => {
const [ hasRun, setHasRun ] = useState(false);
useEffect(() => {
if(!hasRun) {
fxn();
setHasRun(true);
}
}, [...dependencies, hasRun]);
};
// then, in a component:
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useSingleEffect(async () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
}, [actions]);
return (
<p>Hello, world!</p>
);
}
But at that point, why even care about the dependency array? The initial code shown works and makes sense (closures guarantee the correct variables / functions), ESLint just recommends not to do it.
It's like if the second return value of React useState changed every render:
const [ foo, setFoo ] = useState(null);
// ^ this one
If that changed every render, how do we run an Effect with it once?
Ignore the eslint rule for line
If you truly want the effect to run only once exactly when the component mounts then you are correct to use an empty dependency array. You can disable the eslint rule for that line to ignore it.
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
// NOTE: Run effect once on component mount, please
// recheck dependencies if effect is updated.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Note: If you later update the effect and it needs to run after other dependencies then this disabled comment can potentially mask future bugs, so I suggest leaving a rather overt comment as for the reason to override the established linting rule.
Custom hook logic
Alternatively you can use a react ref to signify the initial render. This is preferable to using some state to hold the value as updating it would trigger unnecessary render.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
const initialRenderRef = useRef(true);
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
if (initialRenderRef.current) {
initialRenderRef.current = false;
get_data_async();
}
}, [actions]); // <-- and any other dependencies the linter complains about
return (
<p>Hello, world!</p>
);
}
And yes, absolutely you can factor this "single-run logic" into a custom hook if it is a pattern you find used over and over in your codebase.

Logical understanding react hooks, difference between useState and useEffect (or state and lifecycle methods)

I cannot understand the difference between useState and useEffect. Specifically, the difference between state and lifecycle methods. For instance, I have watched tutorials and seen this example for useEffect:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
const add = () => {
setVal((x) => { return x + 1 })
}
useEffect(() => {
if (value > 0) {
document.title = `Title: ${value}`
}
},[value])
return <>
<h1>{value}</h1>
<button className="btn" onClick={add}>add</button>
</>;
};
When we click the button, the title of the document shows us increasing numbers by one. When I removed the useEffect method and did this instead:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
document.title = `Title: ${value}`
const add = () => {
setVal((x) => { return x + 1 })
}
return <>
<h1>{value}</h1>
<button className="btn" onClick={add}>add</button>
</>;
};
It worked same as the previous code.
So, how does useEffect actually work? What is the purpose of this method?
P.S. Do not send me links of documentation or basic tutorials, please. I have done my research. I know what I am missing is very simple, but I do not know what is it or where to focus to solve it.
Using useEffect to track stateful variable changes is more efficient - it avoids unnecessary calls by only executing the code in the callback when the value changes, rather than on every render.
In the case of document.title, it doesn't really matter, since that's an inexpensive operation. But if it was runExpensiveFunction, then this approach:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
runExpensiveOperation(value);
would be problematic, since the expensive operation would run every time the component re-renders. Putting the code inside a useEffect with a [value] dependency array ensures it only runs when needed - when the value changes.
This is especially important when API calls that result from state changes are involved, which is pretty common. You don't want to call the API every time the component re-renders - you only want to call it when you need to request new data, so putting the API call in a useEffect is a better approach than putting the API call in the main component function body.

Debounce or throttle with react hook

I need to make an api request when a search query param from an input fields changes, but only if the field is not empty.
I am testing with several answers found on this site, but can't get them working
Firstly this one with a custom hook
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
now in my component I do this
const debouncedTest = useDebounce(() => {console.log("something")}, 1000);
but this seems to gets called every rerender regardless of any parameter, and I need to be able to call it inside a useEffect, like this
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
which of course does not work
Another approach using lodash
const throttledTest = useRef(throttle(() => {
console.log("test");
}, 1000, {leading: false}))
But how would i trigger this from the useEffect above? I don't understand how to make this work
Thank you
Your hook's signature is not the same as when you call it.
Perhaps you should do something along these lines:
const [state, setState] = useState(''); // name it whatever makes sense
const debouncedState = useDebounce(state, 1000);
useEffect(() => {
if (debouncedState) functionCall(debouncedState);
}, [debouncedState])
I can quickly point out a thing or two here.
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
technically you can't do the above, useEffect can't be nested.
Normally debounce isn't having anything to do with a hook. Because it's a plain function. So you should first look for a solid debounce, create one or use lodash.debounce. And then structure your code to call debounce(fn). Fn is the original function that you want to defer with.
Also debounce is going to work with cases that changes often, that's why you want to apply debounce to reduce the frequency. Therefore it'll be relatively uncommon to see it inside a useEffect.
const debounced = debounce(fn, ...)
const App = () => {
const onClick = () => { debounced() }
return <button onClick={onClick} />
}
There's another common problem, people might take debounce function inside App. That's not correct either, since the App is triggered every time it renders.
I can provide a relatively more detailed solution later. It'll help if you can explain what you'd like to do as well.

Is it wrong to fetch an API without using the useEffect Hook?

I've been doing it this way but some colleges told me that I should use the useEffect Hook instead. The problem is that I don't see the benefit of that approach and I think that my approach is cleaner.
import React, { useState, useEffect } from "react";
const fetchTheApi = () =>
new Promise(res => setTimeout(() => res({ title: "Title fetched" }), 3000));
const UseEffectlessComponent = () => {
const [data, setData] = useState();
!data && fetchTheApi().then(newData => setData(newData));
return <h1>{data ? data.title : "No title"}</h1>;
};
const UseEffectComponent = () => {
const [data, setData] = useState();
useEffect(() => {
fetchTheApi().then(newData => setData(newData));
}, []);
return <h1>{data ? data.title : "No title"}</h1>;
};
const MyComponent = () => (
<div>
<UseEffectlessComponent />
<UseEffectComponent />
</div>
);
Edit based on responses:
I changed the code to re render, like this:
import React, { useState, useEffect } from 'react';
const fetchTheApi = (origin) => {
console.log('called from ' + origin);
return new Promise((res) =>
setTimeout(() => res({ title: 'Title fetched' }), 3000)
);
};
const UseEffectlessComponent = () => {
const [data, setData] = useState();
!data &&
fetchTheApi('UseEffectlessComponent').then((newData) => setData(newData));
return <h1>{data ? data.title : 'No title'}</h1>;
};
const UseEffectComponent = () => {
const [data, setData] = useState();
useEffect(() => {
fetchTheApi('UseEffectComponent').then((newData) => setData(newData));
}, []);
return <h1>{data ? data.title : 'No title'}</h1>;
};
const MyComponent = () => {
const [counter, setCounter] = useState(0);
counter < 3 && setTimeout(() => setCounter(counter + 1), 1000);
return (
<div>
<p>counter is: {counter}</p>
<UseEffectlessComponent />
<UseEffectComponent />
</div>
);
};
In the console I got:
called from UseEffectlessComponent
called from UseEffectComponent
called from UseEffectlessComponent
called from UseEffectlessComponent
called from UseEffectlessComponent
So, I finally found the benefit to that approach. I've got some code to change... Thanks a lot for the answers!
How you've written it does work, kind of. You're saying "If the fetch fails and the component re-renders, then try again, else don't". Personally I think that is an unreliable system - depending on a re-render to try again, and can easily have unintended side-effects:
What if your data is falsy? What if it fails (which you didn't handle). In this case it will keep trying to re-fetch.
What if the parent renders 3 times in a row (a very common situation). In that case your fetch will happen 3 times before the first fetch is complete.
So with that in mind you actually need more careful checks to ensure you code doesn't have unexpected consequences by not using useEffect. Also if your fetch wanted to re-fetch on prop changes your solution also doesn't work.
Right now, if your component re-renders before it has set the data it will attempt to fetch the data again leading to multiple fetches. Considering you only want to fetch data once and not accidentally multiple times it would be better to put it in the useEffect.
You should use useEffect, because what you do is anti-pattern. From react's website you can clearly see why useEffect is there:
Data fetching, setting up a subscription, and manually changing the
DOM in React components are all examples of side effects. Whether or
not you’re used to calling these operations “side effects” (or just
“effects”), you’ve likely performed them in your components before.
https://reactjs.org/docs/hooks-effect.html
React components are just functions, takes some props and returns some jsx. If you want to have a side effect, you shouldn't have it directly in your component. It should be in a lifecycle method.
Image your condition check (!data), was a complex one looping over arrays etc. It'd have a bigger performance impact. But useEffect will be more performant and you can even use the second argument for kind of 'caching' results.
There is technically no difference between your two components, except the condition check will run on every render in your version. Whereas useEffect will be called only in 'mounted', 'updated' states of the component.

Resources