Giter Site home page Giter Site logo

hooks.macro's Introduction

Hooks’ Macro ☔

Babel Macros for React Hooks automatic memoization invalidation.

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 memoization keys;

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

  3. By lowering the bar for high correctness, strives to:

    1. make the use of useAutoMemo and useAutoCallback simple and applicable in many more contests;

    2. reduce the overhead of modifying an input’s semantics (for example from a constant to a prop);

    3. reduce to the bare minimum cases of missed inputs — and therefore stale memoizations or effects.

  4. Thoroughly tested: 50+ test cases and 100% code coverage.

Roadmap Help wanted!

  • Create a debug/trace facility to help debugging stale cache, performance issues.
  • Create a escape hatch to signal that a reference should not be part of the inputs array.
  • Identify a rule where we can safely add property accesses to the inputs array. Very important when dealing with refs (ref.current).
  • Bail out on actual constants (such as primitive literals) Update: Done!
  • Warn/error on known non-invariant values (such as literal objects or arrays) — or auto-useAutoMemo them!
  • Create a auto() generic macro to be used with other hooks and APIs with the same signature.

Installation

Requires babel-plugin-macros, which is already configured for you if you are using Create React App v2+.

npm install --dev hooks.macro
yarn add --dev hooks.macro

Usage

Replace:

import { useMemo } from 'react';

function MyComponent({ labels }) {
  const myComputation = useMemo(
    () => labels.map(label => label.toUpperCase()),
    [labels],
  );
}

With:

import { useAutoMemo } from 'hooks.macro';

function MyComponent({ labels }) {
  const myComputation = useAutoMemo(() =>
    labels.map(label => label.toUpperCase()),
  );
}

Or even:

import { useAutoMemo } from 'hooks.macro';

function MyComponent({ labels }) {
  const myComputation = useAutoMemo(labels.map(label => label.toUpperCase()));
}

Full reference

useAutoMemo

Exactly like React’s useMemo but automatically identifies value dependencies.

Can be passed a factory function or directly a value, will convert the latter to a function for you.

import { useAutoMemo } from 'hooks.macro';
useAutoMemo(value);
useAutoMemo(() => value);

Both become:

useMemo(() => value, [value]);

useAutoCallback

Exactly like React’s useCallback but automatically identifies value dependencies.

import { useAutoCallback } from 'hooks.macro';
useAutoCallback(() => {
  doSomethingWith(value);
});

Becomes:

useCallback(() => {
  doSomethingWith(value);
}, [doSomethingWith, value]);

useAutoEffect, useAutoLayoutEffect

They work exactly like their standard React counterpart, but they automatically identify value dependencies.

import { useAutoEffect, useAutoLayoutEffect } from 'hooks.macro';
useAutoEffect(() => {
  doSomethingWith(value);
});

Becomes:

useEffect(() => {
  doSomethingWith(value);
}, [doSomethingWith, value]);

Limitations

To make this work I currently needed to pose some limitations. This could change in the future (PR very welcome).

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

  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.

Inspiration

React documentation about useMemo and use*Effect hooks 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 effect function should also appear in the inputs array. In the future, a sufficiently advanced compiler could create this array automatically.

This project tries to cover exactly that: to create the inputs array automatically.

License

MIT

hooks.macro's People

Contributors

yuchi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hooks.macro's Issues

TypeScript support

This should be fairly easy.

The type of useAutoCallback(T) should just be T.

The type of useAutoMemo((...args: any): S) should be S.

Not adding dynamic components to dependencies

Found an issue with detecting variable usage when the components are stored as variables. We use this quite frequently in our app.

Repro

import { useAutoMemo } from 'hooks.macro'
import React from 'react'

const Foo = () => null
const Bar = () => null

export default function MyComponent({ bar }) {
  const Selected = Foo

  if (foo) Selected = Bar

  return useAutoMemo(<Selected />)
}

Output:

import { useMemo as _useMemo } from "react";
import React from 'react';

const Foo = () => null;

const Bar = () => null;

export default function MyComponent({
  bar
}) {
  const Selected = Foo;
  if (bar) Selected = Bar;
  return _useMemo(() => /*#__PURE__*/React.createElement(Selected, null), []);
}

Expected:

import { useMemo as _useMemo } from "react";
import React from 'react';

const Foo = () => null;

const Bar = () => null;

export default function MyComponent({
  bar
}) {
  const Selected = Foo;
  if (bar) Selected = Bar;
  return _useMemo(() => /*#__PURE__*/React.createElement(Selected, null), [Selected]);
}

Create an escape hatch

Sometimes a single reference should be kept out of the inputs array, we could allow the user to specify it. A proposed syntax is:

import { stale, useAutoMemo } from 'hooks.macro';
import { useRandomValue } from 'minister-of-silly-hooks';

function MyComponent({ prop }) {
  const rnd = useRandomValue();
  const value = useAutoMemo(prop * stale(rnd));
}

Do not inject setState/ref into dependencies

Currently testing this macro, so far with great success!

But I have one minor nitpick.

Currently this:

const [state, setState] = useState(0)
useAutoEffect(() => {
   setState(1)
})

Is converted to:

const [state, setState] = useState(0)
useEffect(() => {
   setState(1)
}, [setState])

But adding setState to the dependency array is redundant, since react guarantees it to be referentially stable. The same is true for useRef.

The react-hooks/exhaustive-deps eslint takes both of them into consideration.

Support React 17?

Peer dependencies mention only React 16.8.
This makes it impossible to install in Node 16+ without legacy-peer-deps flag.. 😢

Create a generic form

If someone writes a useCoolMemo hook which has a similar signature of useMemo, there should be a generic macro which gives access to the inputs array. A proposed syntax is:

import { auto } from 'hooks.macro';

useCoolMemo(someOtherArg, ...auto(() => x * y));

Which could become:

useCoolMemo(someOtherArg, ...[
  () => x * y,
  [x, y]
]);

Special cases

We could also treat some special cases in a different way for performance reasons.

Array destructuring

const [ impl, inputs ] = auto(() => x * y);

Becomes:

const impl = () => x * y;
const inputs = [x, y];

Arguments spread

useSomething(...auto(() => x * y));

Becomes:

useSomething(() => x * y, [x, y]);

Rules for property accesses

Because accessing a property in a callback could reference different values that the ones are available at function definition, we currently do not add property accesses to the inputs array. For example:

function MyComponent({ config }) {
  return useAutoMemo(config.value * 2);
}

Gives this output:

function MyComponent({ config }) {
  return useMemo(() => config.value * 2, [config]);
}

This is a big issue when dealing with refs, which are a { current } object. Since they are mutable, this makes the problem non-trivial.

Note: the props reference is a special case of this. Since is easier to identify, we should at least treat that some love.

Bail out on constants

An example can be clearer:

function MyComponent({ prop }) {
  const value = 12;
  return useAutoMemo(value * prop);
}

Since value is invariant, we can transform to:

function MyComponent({ prop }) {
  const value = 12;
  return useMemo(value * prop, [prop]);
}

Extract indirect dependencies

Simple example provided by @threepointone on Twitter:

function App(){
  let [val, setVal] = useState(0);

  let memoed = useAutoMemo(() => someLocallyScopedFunc());

  function someLocallyScopedFunc() {
    return val * 2;
  }
}

We currently get:

function App(){
  let [val, setVal] = useState(0);

  let memoed = useMemo(
    () => someLocallyScopedFunc(),
    [someLocallyScopedFunc]
    // we would want [val] here!
  );

  function someLocallyScopedFunc()() {
    return val * 2;
  }
}

Feature request: debugChangedDependencies

I've been using hooks.macro daily for quite some time now and love it. There is however one pain point I'm struggling a bit with. When effects get hairy it can be difficult to pinpoint what caused the effect to be re-evaluated.

This would be a good solution for this problem. What do you think?

import { logChangedDependencies } from 'hooks.macro'

useAutoEffect(() => {
  logChangedDependencies()
  // way too much code...
})

could log out this message:

[hooks.macro] Changed dependencies: someVar, someOtherVar

Create a debug/trace facility

Since this is a macro, there’s little to no way to trace which references are placed in the inputs array.

An initial proposal syntax could be this:

function MyComponent() {
  const { magnitude } = props;
  const [counter, setCounter] = useState();
  const handleClick = useAutoCallback(event => {
    setCounter(counter * magnitude);
  }, trace);
  // ↑↑↑↑↑
}

Which would output (on every render):

MyComponent › handleClick › useAutoCallback inputs: { counter: 0, magnitude: 4 }
  ┬             ┬             ┬                       ─┬──────────────────────
  │             │             │                        │
  │             │             └─ name of the macro     └─ identifier name and
  │             │                                         value for each input
  │             │
  │             └─ eventual name of the resulting variable
  └─ name of the outer significant scope

Error/warn on known non-invariants

In the following example the current implementation outputs very useless code for useAutoMemo and useAutoCallback, and potentially un-performant code for effects:

function MyComponent({ prop }) {
  const config = { a: prop };
  return useAutoMemo(JSON.stringify(config));
}

The obvious reason is that config will always be different from the previous render. To “solve” it we need to wrap the object literal in useAutoMemo:

function MyComponent({ prop }) {
  const config = useAutoMemo({ a: prop });
  return useAutoMemo(JSON.stringify(config));
}

The macros should at least warn about this borderline situation.

Known static return value from react-redux hook

I'm pretty sure the useDispatch hook in the react-redux package returns a referentially equal value every time, so it should be safe to omit it from the dependency arrays.

Here is the source for this hook.

Memoized values with empty inputs array are known to be static

In the following example the value of values should never change, and we should be able to treat it as static:

const values = React.useMemo(() => new Array(12), []);

I say “should” because the official React documentation states:

You may rely on useMemo as a performance optimization, not as a semantic guarantee.
In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

We will skip this optimization since we cannot guarantee the semantic correctness in the long run.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.