2 ms·
I haven't used this pattern very extensively yet myself, so definitely take this with a grain of salt, but I think a good way to avoid the pitfalls around accid
by lewisl9029 8y ago
I haven't used this pattern very extensively yet myself, so definitely take this with a grain of salt, but I think a good way to avoid the pitfalls around accidentally omitting depencencies consistently might be to define our effect functions as pure functions _outside the scope of the render function_, that takes a list of dependencies from our render function, and then returns an effectful function to pass into useEffect.
Since these functions don't even have access to the render function's closure to begin with, we can eliminate the possibility of our effect functions ever lying about their dependencies, as they have no choice but to receive their dependencies explicitly as arguments.
Probably easier to demonstrate with a code sample:
```
const effectFn = (dependency1, dependency2) => () => console.log(dependency1, dependency2)
const Component = ({prop1, prop2}) => {
const dependencies = [prop1, prop2]
useEffect(effectFn(...dependencies), dependencies)
// rest of render function...
}
```
We can even define a separate hook to solidify this pattern and make it less cumbersome to write. I whipped up this quick example a while ago to show coworkers and tentatively named it `useEffectFromArgs`:
https://codesandbox.io/s/4wwqnqp3zx https://codesandbox.io/s/4wwqnqp3zx
For those who don't want to click the link, it's just a really thin wrapper around useEffect:
```
const useEffectFromArgs = (effectFromArgs, args) =>
useEffect(effectFromArgs(...args), args)
const effectFn = (dependency1, dependency2) => () => console.log(dependency1, dependency2)
const Component = ({prop1, prop2}) => {
useEffectFromArgs(effectFn, [prop1, prop2])
// rest of render function...
}
```
I actually came up with this originally because I wanted a way to be able to test effect functions in isolation as pure functions rather than involve the entire React rendering pipeline just to test something that could have been tested as a pure function, which are by far the simplest tests to write and reason about, and the fastest tests to run. So for me, avoiding the accidental scope capture was really just a nice side effect.
The pattern is equally applicable to other hooks like useCallback, which was actually the original motivation for this. I also have another version of this that use named arguments to specify dependencies rather than a list, that I prefer using over the other one because positional semantics don't scale: https://codesandbox.io/s/4x86wy3r60 https://codesandbox.io/s/4x86wy3r60
It does make me wonder why the hooks API doesn't just work like this to begin with though. I could definitely be missing some downsides to this approach, so if anyone wants to chime in with any critiques/insights, I'd love to hear them.