Custom react hook can not be called in useEffect with empty dependencies - reactjs

Im new to react hook, Im doing a project with new feature "Hooks" of react.
I've faced a problem and I need an explain for it.
As document, to implement "componentDidMount", just pass empty array in dependencies argument.
useEffect(() => {
// some code here
}, []);
And I can call dispatch function to updateState inside this useEffect.
const [flag, setFlag] = useState(false);
useEffect(() => {
setFlag(true);
}, []);
Above code works perfectly without warning or any errors.
Now I have my custom hook, but I can not call my dispatch inside the effect.
const [customFlag, setCustomFlag] = useCustomHook();
useEffect(() => {
setCustomFlag(true);
}, []);
This is my custom hook.
function useCustomHook() {
const [success, setSuccess] = useState(false):
const component = <div>{ success ? "Success" : "Fail" }</div>;
const dispatch = useCallback(success => {
setSuccess(success);
}, []);
return [component, dispatch];
}
With above code, it requires me to put setCustomFlag inside the dependencies array.
I do not understand why. What is different between them?
Thanks for sharing.

Probably, your custom hook returns different instance of setCustomFlag on each call. It means, that useEffect() will always use first value (returned on first render). Try to memoize it by calling useCallback()/useMemo() hooks:
function useCustomHook() {
...
const setCustomFlag = useCallback(/* setCustomFlag body here */, []);
}
It would be nice to have your custom hook source to say more.

The reason is setFlag from useState is a known dependency, its value won't change between render, hence you don't have to declare it as a dependency
React eslint-plugin-react-hooks can't be sure about your custom hook, that's why you need to put that into the dependency list
This is taken from eslint-plugin-react-hooks
// Next we'll define a few helpers that helps us
// tell if some values don't have to be declared as deps.
// Some are known to be static based on Hook calls.
// const [state, setState] = useState() / React.useState()
// ^^^ true for this reference
// const [state, dispatch] = useReducer() / React.useReducer()
// ^^^ true for this reference
// const ref = useRef()
// ^^^ true for this reference
// False for everything else.
function isStaticKnownHookValue(resolved) {
}

Related

useEffect on infinite loop using async fetch function

I am trying to understand why the following useEffect is running in an infinite loop. I made the fetchSchedule helper function to call the getSchedule service (using Axios to query the API endpoint). Reason I did not define this function inside the useEffect hook is because I would like to alternatively also call it whenever the onStatus function is invoked (which toggles a Boolean PUT request on a separate endpoint).
The eslinter is requiring fetchSchedule be added to the array of dependencies, which seems to be triggering the infinite loop.
The way it should work is fetching the data from the database on first render, and then only each time either the value prop is updated or the onStatus button is toggled.
So far my research seems to point that this may have something to do with the way useEffect behaves with async functions and closures. I’m still trying to understand Hooks and evidently there’s something I’m not getting in my code…
import React, { useEffect, useCallback } from 'react';
import useStateRef from 'react-usestateref';
import { NavLink } from 'react-router-dom';
import { getSchedule, updateStatus } from '../../services/scheduleService';
import Status from './status';
// import Pagination from './pagination';
const List = ({ value }) => {
// eslint-disable-next-line
const [schedule, setSchedule, ref] = useStateRef([]);
// const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
}, [value, setSchedule]);
const onStatus = (id) => {
updateStatus(id);
fetchSchedule();
console.log('fetch', ref.current[0].completed);
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
return (...)
Update March 2021
After working with the repo owner for react-usestateref, the package now functions as originally intended and is safe to use as a replacement for useState as of version 1.0.5. The current implementation looks like this:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(state);
var dispatch = React.useCallback(function(val) {
ref.current = typeof val === "function" ?
val(ref.current) : val;
setState(ref.current);
}, []);
return [state, dispatch, ref];
};
You would be fine if it weren't for this react-usestateref import.
The hook returns a plain anonymous function for setting state which means that it will be recreated on every render - you cannot usefully include it in any dependency array as that too will be updated on every render. However, since the function is being returned from an unknown custom hook (and regardless, ESLint would correctly identify that it is not a proper setter function) you'll get warnings when you don't.
The 'problem' which it tries to solve is also going to introduce bad practice into your code - it's a pretty way to avoid properly handling dependencies which are there to make your code safer.
If you go back to a standard state hook I believe this code will work fine. Instead of trying to get a ref of the state in onStatus, make it async as well and return the data from fetchSchedule as well as setting it.
const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
return data;
}, [value]);
const onStatus = async (id) => {
updateStatus(id);
const data = await fetchSchedule();
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
Alternatively, although again I wouldn't really recommend using this, we could actually write a safe version of the useStateRef hook instead:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(defaultValue);
ref.current = state;
return [state, setState, ref];
}
A state setter function is always referentially identical throughout the lifespan of a component so this can be included in a dependency array without causing the effect/callback to be recreated.

React hook missing dependency of custom hook setter

I'm well aware of what the Hook has missing dependency is, what it means and why it's important to watch all dependencies, but this one is just weird.
export function Compo() {
const [value, setValue] = useState<number>();
useEffect(() => {
setValue(Date.now());
}, []);
return (
<>{value}</>
);
}
works fine, but:
function useValue() {
return useState<number>();
}
export function Compo() {
const [value, setValue] = useValue();
useEffect(() => {
setValue(Date.now());
}, []);
return (
<>{value}</>
);
}
show the well known React Hook useEffect has a missing dependency: 'setValue'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
What you've noticed in your example is a quirk of the rule react-hooks/exhaustive-deps. It gives special privilege to hooks it is aware of, and knows to be "stable" under certain circumstances.
Quoting the implementation:
// Next we'll define a few helpers that helps us
// tell if some values don't have to be declared as deps.
// Some are known to be stable based on Hook calls.
// const [state, setState] = useState() / React.useState()
// ^^^ true for this reference
// const [state, dispatch] = useReducer() / React.useReducer()
// ^^^ true for this reference
// const ref = useRef()
// ^^^ true for this reference
// False for everything else.
source: https://github.com/facebook/react/blob/v17.0.1/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js#L152
Specifically, this part of the rule seems to be what is exempting the useState hook's setter under these circumstances:
if (name === 'useState') {
const references = resolved.references;
for (let i = 0; i < references.length; i++) {
setStateCallSites.set(
references[i].identifier,
id.elements[0],
);
}
}
// Setter is stable.
return true;
The unfortunate result of the hook being helpfuln/clever is that it can lead to confusion where its inference doesn't work, like the scenario you just described.

Custom hooks not working properly with useEffect

I wasn't sure on a title for this issue, but I can better explain it here. I have a custom hook that is relying on some information. Part of the information must rely on an async call.
I have three situations happening.
Tried conditionally rendering the custom hook, but react does not like that due to rendering more hooks on a different render.
The custom hook is only mounting once and not passing in the updated information it needs.
I tried passing the dependency to the custom hook and it causes an infinite loop.
Here is a small example of what I'm doing.
Custom Hook:
export function useProducts(options){
const [products, setProducts] = useContext(MyContext)
useEffect(() => {
// only gets called once with `options.asyncValue` === null
// needs to be recalled once the new value is passed in
const loadProducts = async () => {
const data = await asyncProductReq(options)
setProducts(data)
}
loadProducts()
}, []) // if I pass options here it causes the infinite loop
return [products, setProducts]
}
Inside function calling:
export function(props){
const [asyncValue, setValue] = useState(null)
useEffect(() => {
const loadValue = async () => {
const data = await asyncFunc()
setValue(data)
}
loadValue()
}, []}
const options = {...staticValues, asyncValue}
const [products] = useProducts(options)
return (
<h2>Hello</h2>
)
}
I know that I need to pass the options to be a dependency, but I can't figure out why it's causing an infinite reload if the object isn't changing once the async call has been made inside the func.
You were correct in adding options in the dependencies list for your custom hook.
The reason it is infinitely looping is because options IS constantly changing.
The problem is you need to take it one step further in the implementation and make use of the useMemo hook so options only changes when the async value changes, instead of the whole component changing.
So do this:
const options = React.useMemo(() => ({...staticValues, asyncValue}), [asyncValue])

Best practises React hooks HTTP loading

I recently started another project with react, as I had a little time to fiddle around, I used functional components with hooks. I had no problem whatsoever, there's just one thing I'm not sure I use correctly, here is an example :
function MyComponent() {
const [data, setData] = useState([]);
const [dataLoaded, setDataLoaded] = useState(false);
var getDataFromHTTP = async () { ... }
var loadData = async () => {
if (!dataLoaded) {
setDataLoaded(true);
setData(await getDataFromHTTP());
}
}
loadData();
return( ... );
}
If I like how everything is done, I suppose it's dirty to use loadData(); like in the preceding example, and I tried to useEffect with something like this :
useEffect(() => {
loadData();
}, []);
but then I got a warning like "loadData should be a dependency of useEffect". If I omit the the second argument to useEffect, it looks like it's the same as putting it directly in MyComponent. So basically, my question, in this example what is the best practise to load data once when the component is mounted ? and of course, when props/state change, what is the best practise to reload it if needed ?
EDIT:
The warning I have with useEffect is :
[Warning] ./src/list/main.js (1.chunk.js, line 25568)
Line 53: React Hook useEffect has a missing dependency: 'loadData'. Either include it or remove the dependency array react-hooks/exhaustive-deps
The way useEffect works is whenever something in the dependencies array change React will run that effect
useEffect(() => {
loadData();
}, [loadData]); // <-- dependencies array
But as you have declared loadData as a normal function it will get re-assigned to a new function on every render and it will trigger the effect.
Best way would be to wrap your loadData function in an useCallback hook
const loadData = useCallback(async () => {
if (!dataLoaded) {
setDataLoaded(true);
setData(await getDataFromHTTP());
}
}, [])

How to handle dependencies array for custom hooks in react

I'm creating a custom hook and would like to define an optional param so that I can pass in extra dependencies when needed. My code looks like the following snippet:
import { useEffect } from 'react';
function useCustomHook(param1, extraDeps) {
useEffect(() => {
// do something with param1 here
}, [param1, ...extraDeps])
}
The react-hooks/exhaustive-deps throws a warning saying
React Hook useEffect has a spread element in its dependency array. This means we can't statically verify whether you've passed the correct dependencies
Anyone have an idea about how to address that warning? Or it's not a good practice to pass deps array to custom hook?
For those who are interested in why extraDeps is needed. here's an example:
const NewComponent = (props) => {
[field1, setField1] = useState()
[field2, setField2] = useState()
// I only want this to be called when field1 change
useCustomHook('.css-selector', [field1]);
return <div>{field1}{field2}</div>;
}
I've found a useful alternative to the solutions proposed here. As mentioned in this Reddit topic, the React team apparently recommends something like:
// Pass the callback as a dependency
const useCustomHook = callback => {
useEffect(() => { /* do something */ }, [callback])
};
// Then the user wraps the callback in `useMemo` to avoid running the effect too often
// Whenever the deps change, useMemo will ensure that the callback changes, which will cause effect to re-run
useCustomHook(
useMemo(() => { /* do something */ }, [a, b, c])
);
I've used this technique and it worked very nicely.
I had a similar issue, I wanted an effect to be executed whenever some extra dependencies were changed.
I didn't manage to give those extra dependencies, but instead I made it the way around by giving the caller the callback I wanted to be executed and let him use it when he needs.
Example :
// This hook uses extraDeps unknown by EsLint which causes a warning
const useCustomEffect = (knowDep, extraDeps) => {
const doSomething = useCallback((knownDep) => {/**/}, [])
useEffect(() => {
doSomething(knownDep)
}, [doSomething, knownDep, ...extraDeps]) // Here there is the warning
}
//Instead of this, we give the caller the callback
const useCustomEffect = (knownDep) => {
const doSomething = useCallback((knownDep) => {/**/}, [])
useEffect(() => {
doSomething(knownDep)
}, [doSomething, knownDep]) // no more warning
return { doSomething }
}
// Use it like this
const { doSomething } = useCustomEffect(foo)
useEffect(doSomething, [bar, baz]) // now I can use my callback for any known dependency
The way you have defined your custom hook makes sense to me. In this case eslint cannot check your dependencies, but that does not mean that they are wrong. Simply disable the rule for this line to get rid of the warning:
function useCustomHook(param1, extraDeps) {
useEffect(() => {
// do something with param1 here
}, [param1, ...extraDeps]) // eslint-disable-line react-hooks/exhaustive-deps
}
Depending on the type of param1, it makes sense to enable the dependency check for your custom hook by defining it in your .eslintrc.cjs:
'react-hooks/exhaustive-deps': ['warn', {
additionalHooks: '(useCustomHook|useAnotherHook|...)'
}]
Here's whay you could do:
Move the state to your custom hook, run effects on it and return it.
Something like:
Component.js
function Component() {
const [field,setField] = useCustomHook(someProps);
}
useCustomHook.js
import {useState, useEffect} from 'react';
function useCustomHook(props) {
const [field,setField] = useState('');
useEffect(()=>{
// Use props received and perform effect after changes in field 1
},[field1]);
return([
field,
setField
]);
}
If you want to provide extra-deps you can use useDeepCompareEffect instead of useEffect.
https://github.com/kentcdodds/use-deep-compare-effect
I think the problem lies in how you are creating the dependency array on your custom hook. Every time you do [param1, ... extraDeps] you are creating a new Array, so React always see them as different.
Try changing your custom hook to:
function useCustomHook(deps) {
useEffect(() => {
// do something with param1 here
}, deps)
}
And then use it like
const NewComponent = (props) => {
[field1, setField1] = useState()
[field2, setField2] = useState()
// I only want this to be called when field1 change
useCustomHook(['.css-selector', field1]);
return <div>{field1}{field2}</div>;
}
Hope it helps!

Resources