Description
Feature
A new overloading for useEffect
(Typescript syntax):
interface useEffect {
/**
* @param what - what this side effect does?
* @param whatDeps - which variables modify “what” the side effect does?
* These dependencies must match all live variables explicitly referenced
* in the body of the “what” callback.
* @param whenDeps - which variables modify “when” the side effect takes place?
* When and only when at least one of those dependencies change, the “what”
* callback should be executed.
*/
(what: (...args: any[]) => any, whatDeps: any[], whenDeps: any[]): void;
/**
* @param what - what this side effect does?
* @param deps - an array of values that the effect depends on.
*
*/
(what: (...args: any[]) => any, deps?: any[]): void;
}
Motivations
In the current implementation, the second argument of useEffect
, “deps”, is described as such:
The array of values that the effect depends on.
This definition does not account for an important nuance between two kind of dependencies:
- what dependencies, those which require the effect callback to be
recomputedupdated; - when dependencies, those which require the effect callback to be
rerunexecuted.
The community seems to be in need of a solution, see https://stackoverflow.com/q/55724642/2779871.
Use case
I want to scroll to top of a component when the content changes (first dependency), but this effect also depends on a variable padding top (second dependency).
With the current implementation of useEffect
, this is what I would do:
function MyComponent(props) {
const { paddingTop, content } = props;
const ref = React.useRef();
React.useEffect(() => {
// scroll to paddingTop when content changes?
ref.current.scrollTo(0, paddingTop);
}, [paddingTop, content]);
return <div ref={ref}>...</div>
}
There is an undesired behavior: the hook is executed on paddingTop
changes. Moreover, content
is not, semantically, a dependency of the callback, but rather a dependency of when this side effect should take place. So I could use a ref, store the previous value of paddingTop
, and compare the two. But that is cumbersome.
What I would like to do, is express the when this side-effect should take place dependencies declaratively:
function MyComponent(props) {
const { paddingTop, content } = props;
const ref = React.useRef();
React.useEffect(() => {
// scroll to paddingTop when content changes.
ref.current.scrollTo(0, paddingTop);
}, [paddingTop], [content]);
return <div ref={ref}>...</div>
}
Detailed behavior
My understanding is that this proposal would not be a breaking change and is 100% retrocompatible with current implementation.
One argument
useEffect(what);
The behavior is identical to current implementation. The effect is executed after each render cycle.
Two arguments
useEffect(what, deps);
The behavior is identical to current implementation. The second argument conflates whatDeps and whenDeps.
Empty second argument
useEffect(what, []);
The behavior is identical to current implementation. The callback is executed only once.
Empty third argument
useEffect(what, whatDeps, []);
The callback is executed only once, regardless of the changes in whatDeps.
Three arguments
useEffect(what, whatDeps, whenDeps);
The callback is executed when and only when at least one variable in whenDeps array changes, regardless of the changes in whatDeps.