Skip to content

Inference of Hooks’ inputs #14406

Closed
Closed
@yuchi

Description

@yuchi

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):

The array of inputs is not passed as arguments to the function. Conceptually, though, that’s what they represent: every value referenced inside the function should also appear in the inputs array. In the future, a sufficiently advanced compiler could create this array automatically.

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 to useMemo, useCallback and use*Effect hooks.

The tool trasform this:

import { useAutoMemo } from 'hooks.macro';
function MyComponent({ propValue }) {
  return useAutoMemo(() => propValue.split('').join('-'));
  //     ---^^^^----                                    ^ no inputs
}

To this:

function MyComponent({ propValue }) {
  return React.useMemo(() => propValue.split('').join('-'), [propValue]);
  //     ^---^ ^-----^                                       ^-------^
}

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.

  1. Failing to populate the inputs array correctly has sad consequences:

    1. too wide, and it will invalidate the use of memoization itself, and/or bring potential perf issues;

    2. 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;)

    3. bugs caused by having non local values are potentially non deterministic.

  2. Keeping the inputs array updated is important, but it’s a matter of discipline:

    1. when I change the “body” of the hook I need to check it;

    2. 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);

    3. 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).

  3. By removing the burden of adding the inputs array, we reduce the mental overhead of using the memoizing hooks (namely useMemo and useCallback). This let us:

    1. make following the best practices easier,

    2. encourage the use of memoization in unexpected places (such as directly inside the final returning JSX of a component).

  4. 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.

  5. It’s an error-prone and boring practice that can be automated away.

Current implementation

(Straight from hooks.macro’s README)

Features

  1. Extracts all references used, and adds them to the inputs array.

  2. Favors strict correctness over performance, but uses safe optimizations:

    1. skips constants and useless inputs;

    2. traverses all functions called or referenced, and appends their dependencies too, removing the need for unnecessary useCallback hooks.

Constraints

  1. Only variables created in the scope of the component body are automatically trapped as inputs.

  2. 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.

  3. 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.

  4. Only locally defined functions declarations and explicit function expressions (let x = () => {}) are traversed for indirect dependencies — all other function calls (such as xxx()) are treated as normal input dependencies and appended too. This is unnecessary (but not harmful) for setters coming from useState, and not an issue at all if the function is the result of useCallback or useAutoCallback.

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.

  1. What were/are the requirements of the «sufficiently advanced compiler» you envision? Is the current approach of hooks.macro aligned with those?

  2. 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.

  3. 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?

  4. 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?)

  5. 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.

  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions