React patterns: How to deal with functions as useEffect dependencies
Why functions as dependencies require some thought
Sometimes your effects invoke another function and assuming you have the exhaustive-deps
ESLint rule enabled, you’ll get warnings about missing dependencies if you don’t include the function in the dependencies array.
But if you include the function as a dependency, you effect will likely run on every render because the function identity will be different on each render (unless you wrap the function in useCallback
).
These are some approaches I usually take, though the React docs mention a few others that can be useful.
1. Move functions that don’t need props or state outside your component
If the function doesn’t read props or state values, you can move it outside the component and safely use it inside useEffect
.
2. Move your function inside the effect
When your function is outside the useEffect
hook, it can be difficult to keep track of what state or props that function depends on.
For example, in the following snippet, you must check the fetchProduct
for any props or state it depends on (productId
in this case) and remember to pass those in the dependencies array to useEffect
.
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId); // Uses productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 Invalid because `fetchProduct` uses `productId`
// ...
}
It’s easy to forget to pass a valid dependency.
You can make your life easier by moving the fetchProduct
function inside the useEffect
hook and ensuring you have the exhaustive-deps
ESLint rule enabled. Now, ESLint can analyze your code for you and warn you if you forgot to add a dependency.
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// By moving this function inside the effect, we can clearly see the values it uses.
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ Valid because our effect only uses productId
// ...
}
By delegating these checks to ESLint, you’ll have fewer bugs caused by forgotten dependencies.
3. Wrap the function in a useCallback
hook
Sometimes, it’s not possible to move a function inside the useEffect
. In these cases, you can add the function to the effect’s dependencies, but wrap its definition into a useCallback
.
Wrapping the function in useCallback
will ensure that the function identity remains the same between renders unless one of the functions’ dependencies changes.
function ProductPage({ productId }) {
// ✅ Wrap with useCallback to avoid change on every render
const fetchProduct = useCallback(() => {
// ... Does something with productId ...
}, [productId]); // ✅ All useCallback dependencies are specified
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ All useEffect dependencies are specified
// ...
}
Sources
Thanks for your comment 🙏. Once it's approved, it will appear here.
Leave a comment