React hook missing dependency of custom hook setter - reactjs

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.

Related

Wrapping useState hook with custom setter and clean-up functionality

I'd like to create a custom React hook that
wraps a state (created with useState),
runs a clean-up function up when the component unmounts,
executes a custom setter logic that derives a value,
and exports the custom setter and the internal state value.
export function useCustomState() {
const [value, setValue] = useState();
useEffect(() => {
return () => {
// do cleanup
};
}, [value]);
function customSetValue(newValue) {
if (value) {
// do cleanup for the previous value
}
const derivedValue = derive(newValue);
setValue(derivedValue);
}
return [value, customSetValue];
}
I can achieve this with the code above. My problem arises when I use the returned values in an useEffect hook as a dependency, since the returned custom setter is always a new function reference.
const Component = () => {
const [value, setValue] = useCustomState();
useEffect(
() => {
setValue(simpleValue);
},
[setValue],
);
return <p>{ value }</p>;
};
When I don't include the setter as a dependency of the useEffect, the re-rendering stops, since the dependency does not change after a render. I can omit that reference and disable eslint for the line. This is one solution.
I'd like to know whether it is possible to create a custom referentially stable setter function?
I also have tried using useMemo that stores the custom setter function, but the dependencies for the memo still include the internal value and setValue references, since I want to do clean-up and set the new derived value. If the derived value is not the same for the same input, the dependency cycle will result in infinite re-rendering.
You can pass a function to setValue to remove the dependence on value; instead, the current value is passed as an argument. As a simpler alternative to useMemo, useCallback will give you a consistent function:
export function useCustomState() {
const [value, setValue] = useState();
useEffect(() => {
return () => {
// do cleanup
};
}, [value]);
const customSetValue = useCallback((newValue) => {
setValue((oldValue) => {
if (oldValue) {
// do cleanup for the previous value
}
const derivedValue = derive(newValue);
return derivedValue;
});
}, []);
return [value, customSetValue];
}

How to solve this missing dependency in useEffect?

The goal is to reset a value, that is mutated inside of the function, if the props have changed to something that is different from the current value.
I am trying out react hooks and am still not confident how to solve this and if it can be solved.
While this seems to work as expected, I get a ESlint warning:
Warning:(62, 8) ESLint: React Hook useEffect has a missing dependency: 'value'. Either include it or
remove the dependency array. (react-hooks/exhaustive-deps)
function (info) {
const initialValue = info.value;
const [value, set_value] = useState(initialValue);
useEffect(() => {
if (initialValue !== value) {
set_value(initialValue);
}
}, [initialValue]);
...};
I can't add the value variable to the dependency array, because it will prevent the mutation.
I checked the react-hooks documentation and this thread here:
https://github.com/facebook/react/issues/15865
But am still not confident how to apply this knowledge to my case.
You don't need value in useEffect scope, you can use functional updates:
function App({ value: initialValue }) {
const [value, set_value] = useState(initialValue);
useEffect(() => {
set_value((prevValue) =>
initialValue !== prevValue ? initialValue : prevValue
);
}, [initialValue]);
return <></>;
}

How to let eslint react-hooks/exhaustive-deps rule know that the return value of a custom hook is invariant?

When you call one of the "builtin" hook, the react-hooks/exhaustive-deps rule is smart enough to recognize that some return values are guaranteed by React to be invariant. This is the case for state updaters and dispatchers returned by useState and useReducer hooks.
In this first example, the call to useCallback does not need to list the state setter as a dependency:
export const Example1 = () => {
const [date, setDate] = useState(new Date())
const resetDate = useCallback(() => {
setDate(new Date())
}, []) // No need to list any dependency here. No eslint warning.
return (
<>
Date: {date.toISOString()} <button onClick={resetDate}>Reset</button>
</>
)
}
But in the second example, where the setter is returned by a custom hook, it is required.
const useDateState = (initialValue: Date) => {
return useState(initialValue)
}
export const Example2 = () => {
const [date, setDate] = useDateState(new Date())
const resetDate = useCallback(() => {
setDate(new Date())
}, []) // ESLint complains: React Hook useCallback has a missing dependency: 'setDate'. Either include it or remove the dependency array.
return (
<>
Date: {date.toISOString()} <button onClick={resetDate}>Reset</button>
</>
)
}
Is there a way to tell the eslint rule that the setter returned by my custom hook will not change and that it does not need to list it as a dependency? This would help remove some code.
Note: I have read the React FAQ, this StackOverflow question and I understand that there is no performance cost to adding a dependency that will not change.
But since this behavior is available with the built-in hooks, I want to know if there is a way to get the same for a custom hook.

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

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) {
}

How to work around expensive custom hooks?

As we know, the rule is:
Only Call Hooks at the Top Level. Don’t call Hooks inside loops, conditions, or nested functions.
So my questions is how to use and design a custom hook that is expensive?
Given this hook:
const useExpensiveHook = () => {
// some code that uses other built-in hooks...
const value = computeExpensiveValue();
// some more code....
return value;
}
If that rule did not exist my client code would be:
const myComponent = ({isSuperFeatureEnabled}) => {
let value;
if (isSuperFeatureEnabled){
value = useExpensiveHook();
}
return <div>{value}</div>;
}
The solution I came up with is to let the hook know that it should bail out, like this, using a flag:
const useExpensiveHook = ({enabled}) => {
// some code that uses other built-in hooks...
let value;
if(enabled) {
value = computeExpensiveValue();
}
else {
value = undefined;
}
// some more code....
return value;
};
and the client code:
const myComponent = ({isSuperFeatureEnabled}) => {
const value = useExpensiveHook({enabled : isSuperFeatureEnabled});
return <div>{value}</div>;
}
It is passing a flag to expensive hooks the right way to handle conditional hooks? What are the other options?
In original example it is hook initial value that is expensive, not a hook itself, computeExpensiveValue can be conditionally called:
const [value, setValue] = useState(enabled ? computeExpensiveValue() : undefined);
In currently listed example useExpensiveHook is not a hook but some function; it doesn't use React hooks.
The purpose of quoted rule is to make built-in hooks called unconditionally because the state of hooks is determined by the order in which they are called:
if (flipCoin())
var [foo] = useState('foo');
var [bar] = useState('bar');
In case useState('foo') isn't called on next component render, useState('bar') becomes the first useState hook to be called and considered foo state, while second useState is missing, this inconsistency triggers an error in a renderer.
If it were guaranteed that the order of hook calls is preserved, it would be acceptable to use conditions but this is rarely feasible in practice. Even if there's seemingly constant condition like if (process.env.NODE_ENV === 'development'), it could change under some circumstances at runtime and result in said problems that are hard to debug.
Correct:
useEffect(() => {
if (varyingCondition)
computeExpensiveValue();
});
Incorrect:
if (varyingCondition)
useEffect(() => {
computeExpensiveValue();
});
This rule applies only to built-in hooks and functions that call them directly or indirectly (so-called custom hooks). As long as computeExpensiveValue doesn't use built-in hooks internally, it can be conditionally called, as 'correct' example shows.
In case a component needs to conditionally apply third-party hook depending on prop flag, it should be guaranteed that the condition won't change with time by restricting it to be initial prop value:
const Component = ({ expensive, optionalValue }) => {
const isExpensive = useMemo(() => expensive, []);
if (isExpensive)
optionalValue = useExpensiveHook();
return ...
}
This way <Component expensive={flipCoin()} /> won't break the rule but just misuse the component.
Since it should be known if expensive hook is needed at the time when <Component expensive/> is used, a cleaner way is to compose this functionality in higher-order component and use different components depending on which one is needed:
const withExpensive = Comp => props => {
const optionalValue = useExpensiveHook();
return <Comp optionalValue={optionalValue} ...props />;
}
const Component = ({ optionalValue }) => {
return ...
}
const ExpensiveComponent = withExpensive(Component);
The argument to useState is being used only once and hence if you initially pass enabled as false to it, it will not execute the computeExpensiveValue ever. Hence you would need to add a useEffect call too. You could instead design your hook like
const useExpensiveHook = ({enabled}) => {
const [value, setValue] = useState(enabled ? computeExpensiveValue : undefined);
useEffect(()=> {
if(enabled) {
const value = computeExpensiveValue();
setValue(value);
}
}, [enabled]);
// some more code....
return value;
};

Resources