-
Notifications
You must be signed in to change notification settings - Fork 47.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inference of Hooks’ inputs #14406
Comments
Note that ref updates never trigger a rerender. Because of that, having I find myself typically using refs specifically when you need something that has referential identity while being able to manipulate its const realCallback = useCallback(() => {/* ... */}, [/* real inputs */])
const callbackRef = useRef(realCallback)
// useLayoutEffect for updating this could be better
// but it'd break synchronous invocations
// right now it breaks concurrent updates instead 🤷♀️
callbackRef.current = realCallback
// note how memoCallback is only ever supposed to be generated once
const memoCallback = useCallback<typeof realCallback>(() => callbackRef.current(), [callbackRef])
// memoCallback is returned as the 2nd item of the pair Although all that this is is a horrible workaround for the problem in #14099 TypeScript hints can't really help with keeping the inputs array consistent; at best they can just tell when you tried to use something in an input while it's in TDZ (because you are following rules of hooks, right?). What do you mean by auto-callback feature? You mean it's possible to pass a non-function to Personally my problem with |
Yes and no, see how it is used in That specific case should support a pass-through inputs array that somehow encodes that the ref refers to something else (a responsibility completely in the hands of the users of the hook). Having Should the second argument of effects support an inputs array creator? Looks silly IMHO. const [element, setElement] = useState(null);
const onScreen = useOnScreen(useAutoMemo({ current: element })); // <-- auto-callback in use here
return <div ref={setElement}> … </div> These complexities root on a still incomplete grasp of hooks from me (but I hope/fear I’m not alone) and made me wait on adding object properties access in inputs. Lets wait the core team to shed some light.
We can do more aggressive optimizations. If I could know that a value is a ReadOnlyArray then I could spread the values in the inputs array too, and I could remove the need for a useMemo call: const { values: ReadOnlyArray<string> } = props;
return useAutoMemo(values.join('-'));
// ↓ ↓ ↓
return useMemo(() => values.join('-'), [values.length, ...values]);
Note that
|
I already filed an issue on const [something, setSomething] = useState();
const handleChange = useStableCallback(...auto(event => {
setSomethingElse(something + parseInt(event.currentTarget.value, 10));
}));
// ↓ ↓ ↓
const handleChange = useStableCallback(event => {
setSomethingElse(something + parseInt(event.currentTarget.value, 10));
}, [something]); (Thanks for the feedback!) |
@yuchi your
|
Can you elaborate a little bit? I’m not sure I understood what you mean. |
It just occurred to me that the term “auto-callback” (to refer to the fact that with |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution. |
This is a thorough suggestion and I would appreciate hearing an opinion and guidance from the core team. Please, stalebot. |
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment! |
Bump |
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment! |
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you! |
I open this issue (as suggested by @gaearon on Twitter) to discuss how Hook’s inputs argument should be inferred.
Background
The current Hooks API reference cites (emphasis mine):
Captivated by this sentence (and the idea of bringing “the future” closer) I built
hooks.macro
, a Babel Macro which tries to do exactly this. That is, to infer the second argument touseMemo
,useCallback
anduse*Effect
hooks.The tool trasform this:
To this:
In the process of designing the constraints of that tool, I realized there are more than a few open questions, whose answers can be found only with guidance from the core React team and a broader discussion with the community.
Rationale
Why should we bother with this topic? The reasons that motivated me in implementing
hooks.macro
were the following.Failing to populate the inputs array correctly has sad consequences:
too wide, and it will invalidate the use of memoization itself, and/or bring potential perf issues;
too narrow, and bugs can be tricky to identify (IMHO unit tests are not viable to avoid this: you need to test changes, not just different possible values;)
bugs caused by having non local values are potentially non deterministic.
Keeping the inputs array updated is important, but it’s a matter of discipline:
when I change the “body” of the hook I need to check it;
when I change the semantics of a used valued I need to verify if I need to include it (e.g. when it was a constant and now a prop);
not introducing bugs during merge conflicts is matter of even higher discipline (since usually inputs will fit in a single line, you probably need to edit the line to solve the conflict).
By removing the burden of adding the inputs array, we reduce the mental overhead of using the memoizing hooks (namely
useMemo
anduseCallback
). This let us:make following the best practices easier,
encourage the use of memoization in unexpected places (such as directly inside the final returning JSX of a component).
Nothing is stopping me from shooting my own feet with obvious errors (such as passing the result of a non-memoized array literal ad an input). If a stable enough approach is found, this could be even solved automatically by memoizing all necessary inputs too.
It’s an error-prone and boring practice that can be automated away.
Current implementation
(Straight from
hooks.macro
’s README)Features
Extracts all references used, and adds them to the inputs array.
Favors strict correctness over performance, but uses safe optimizations:
skips constants and useless inputs;
traverses all functions called or referenced, and appends their dependencies too, removing the need for unnecessary
useCallback
hooks.Constraints
Only variables created in the scope of the component body are automatically trapped as inputs.
Only variables, and not properties’ access, are trapped. This means that if you use
obj.prop
only[obj]
will become part of the memoization invalidation keys. This is a problem for refs, and will be addressed specifically in a future release.You can work around this limitation by creating a variable which holds the current value, such as
const { current } = ref
.Currently there’s no way to add additional keys for more fine grained cache invalidation. Could be an important escape hatch when you do nasty things, but in that case I’d prefer to use
useMemo
/useCallback
directly.Only locally defined functions declarations and explicit function expressions (
let x = () => {}
) are traversed for indirect dependencies — all other function calls (such asxxx()
) are treated as normal input dependencies and appended too. This is unnecessary (but not harmful) for setters coming fromuseState
, and not an issue at all if the function is the result ofuseCallback
oruseAutoCallback
.Questions
As I said before I have few open questions for the core React team, but I’m sure the whole community will be impactful in the discussion.
What were/are the requirements of the «sufficiently advanced compiler» you envision? Is the current approach of
hooks.macro
aligned with those?Using a broader approach, Flow/TypeScript hints could give an incredible amount of precision not available on the pure Babel pipeline. I feel pre-pack can help here too (we could help pre-pack do its thing actually) but I’m not sure how.
Are users potentially scared by the amount of black magic involved? I tried to follow the Hooks lead by having very clear, very understandable rules. Is this enough?
The auto-closure feature of
useMemo
(you don’t need an arrow function) has received the most negative feedback in the small discussions I had online. Yet I personally find the most interesting hook of this library, since adds so little overhead I can throw it any time I need… potentially abusing it eventually (so, points against it?)I personally prefer to have a different API that makes it clear that those are not standard hooks without "inputs". A Babel plugin that transform all hooks without an explicit inputs array seems silly and scary — you get used to not passing them and you don't have the API signaling you that you need to.
Since this is a specialized form of lambda-lifting, are you willing to add some advanced hooks (with one of your lovely silly prefixes such as
DONT_USE_
) that pass the inputs as arguments to creator/effect function? This open some interesting perf optimization opportunities by hoisting functions higher up.The text was updated successfully, but these errors were encountered: