Solve your hook spaghetti (with more spaghetti). Inspired by React Forget.
npm i forgetti
yarn add forgetti
pnpm add forgetti
Forgetti is an auto-memoization Babel plugin I made for a hook-based flow like React hooks. This plugin was inspired by React Forget.
Note Please watch the video. Video explains about the problems it solves and benefits it delivers
Forgetti only optimizes component functions (if it is valid based on the component name regex), component functions in HOC (based on HOC imports) and hook functions.
There's four kinds of memoizations that Forgetti produces, and relies on the fundamental concept between a value and its relation to its dependencies:
If an expression has zero dependencies, it is memoized as a constant value. This behavior is similar to a lazy useRef.
function Example() {
const value = [1, 2, 3, 4];
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
function Example() {
let _c = _$$cache(_useMemo, 1);
const value = 0 in _c ? _c[0] : _c[0] = [1, 2, 3, 4];
}
Complex expressions that are entirely constant are cached as one.
function Example() {
const message = `${getGreeting()}, ${getReceiver()}`;
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
function Example() {
let _c = _$$cache(_useMemo, 1);
const message = 0 in _c ? _c[0] : _c[0] = `${getGreeting()}, ${getReceiver()}`;
}
A dependency is a memoized form of reference. Forgetti memoizes these values to mark if the dependency value has changed. Aside the memoization step, a variable for checking the condition is also yielded.
If an expression has one or more dependencies, the condition yielded by the dependencies is combined, which makes the dependent value yield the combined condition as its own conditon. If the condition is falsy, the dependent value produces a new value for its own. This process is similar to useMemo
but without having to rely on dependency lists.
function Example(props) {
const message = `${props.greeting}, ${props.message}`;
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
function Example(props) {
let _c = _$$cache(_useMemo, 6),
_eq = _$$equals(_c, 0, props),
_v = _eq ? _c[0] : _c[0] = props,
_v2 = _eq ? _c[1] : _c[1] = _v.greeting,
_eq2 = _$$equals(_c, 2, _v2),
_v3 = _eq2 ? _c[2] : _c[2] = _v2,
_v4 = _eq ? _c[3] : _c[3] = _v.message,
_eq3 = _$$equals(_c, 4, _v4),
_v5 = _eq3 ? _c[4] : _c[4] = _v4;
const message = _eq2 && _eq3 ? _c[5] : _c[5] = `${_v3}, ${_v5}`;
}
Hooks have their own optimization step so that the expression would be consistent between function calls.
A hook defined as a ref
must resemble React's useRef
import { useRef } from 'react';
function Example(props) {
const example = useRef(initialValue);
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { useRef } from 'react';
function Example(props) {
let _c = _$$cache(_useMemo, 1);
const example = 0 in _c ? _c[0] : _c[0] = {
current: initialValue
};
}
A hook defined as a memo
must resemble React's useMemo
. memo
optimization might be similar to the optimization step for dependent values except that memo
allows you to handpick which value it needs to be dependent on.
import { useMemo } from 'react';
function Example(props) {
const dependencyA = getA(props);
const dependencyB = getB(props);
const value = useMemo(() => getValue(dependencyA, dependencyB), [dependencyB]);
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
import { useMemo } from 'react';
function Example(props) {
let _c = _$$cache(_useMemo, 6),
_eq = _$$equals(_c, 0, props),
_v = _eq ? _c[0] : _c[0] = props;
const dependencyA = _eq ? _c[1] : _c[1] = getA(_v);
const dependencyB = _eq ? _c[2] : _c[2] = getB(_v);
let _eq2 = _$$equals(_c, 3, dependencyB),
_v4 = _eq2 ? _c[3] : _c[3] = dependencyB,
_v5 = _eq2 ? _c[4] : _c[4] = [_v4];
const value = _eq2 ? _c[5] : _c[5] = (() => getValue(dependencyA, dependencyB))();
}
A hook defined as a callback
must resemble React's callback
. callback
optimization might be similar to the function expression optimization step except that callback
allows you to handpick which value it needs to be dependent on.
import { useCallback } from 'react';
function Example(props) {
const dependencyA = getA(props);
const dependencyB = getB(props);
const value = useCallback(() => getValue(dependencyA, dependencyB), [dependencyB]);
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
import { useCallback } from 'react';
function Example(props) {
let _c = _$$cache(_useMemo, 6),
_eq = _$$equals(_c, 0, props),
_v = _eq ? _c[0] : _c[0] = props;
const dependencyA = _eq ? _c[1] : _c[1] = getA(_v);
const dependencyB = _eq ? _c[2] : _c[2] = getB(_v);
let _eq2 = _$$equals(_c, 3, dependencyB),
_v4 = _eq2 ? _c[3] : _c[3] = dependencyB,
_v5 = _eq2 ? _c[4] : _c[4] = [_v4];
const value = _eq2 ? _c[5] : _c[5] = () => getValue(dependencyA, dependencyB);
}
A hook defined as an effect
must resemble React's useEffect
. This optimization step only memoizes the dependency list so that React (or the target library) only has to compare one item instead of N items.
import { useEffect } from 'react';
function Example(props) {
useEffect(() => {
getValue(props.a, props.b, props.c);
}, [props.a, props.b, props.c]);
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
import { useEffect } from 'react';
function Example(props) {
let _c = _$$cache(_useMemo, 9),
_eq = _$$equals(_c, 0, props),
_v = _eq ? _c[0] : _c[0] = props,
_v2 = _eq ? _c[1] : _c[1] = _v.a,
_eq2 = _$$equals(_c, 2, _v2),
_v3 = _eq2 ? _c[2] : _c[2] = _v2,
_v4 = _eq ? _c[3] : _c[3] = _v.b,
_eq3 = _$$equals(_c, 4, _v4),
_v5 = _eq3 ? _c[4] : _c[4] = _v4,
_v6 = _eq ? _c[5] : _c[5] = _v.c,
_eq4 = _$$equals(_c, 6, _v6),
_v7 = _eq4 ? _c[6] : _c[6] = _v6,
_hoisted = useEffect(() => {
getValue(props.a, props.b, props.c);
}, [_eq2 && _eq3 && _eq4 ? _c[7] : _c[7] = [_v3, _v5, _v7]]);
_$$equals(_c, 8, _hoisted) ? _c[8] : _c[8] = _hoisted;
}
All expressions are covered with the exception of the following:
this
super
import
import.meta
- class expressions
yield
await
++
and--
- function bind
- do expressions
- pipeline operators
- module expressions
The optimization step is basically an auto-memoized callback. It will only change if the values it is using has changed.
function Example(props) {
const callback = () => {
console.log(props.message);
};
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
function Example(props) {
let _c = _$$cache(_useMemo, 2),
_eq = _$$equals(_c, 0, props),
_v = _eq ? _c[0] : _c[0] = props;
const callback = _eq ? _c[1] : _c[1] = () => {
console.log(props.message);
};
}
Literals are skipped, with the exception of undefined
. Guaranteed literals (complex expressions that are comprised only of literals) are cached as a constant.
Note In the future, guaranteed literals might be skipped so that bundlers and minifiers can optimize the expressions.
Forgetti expands conditional expressions into an if-statement.
function Example(props) {
const value = props.condition ? props.left : props.right;
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$branch as _$$branch } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
function Example(props) {
let _c = _$$cache(_useMemo, 4),
_eq = _$$equals(_c, 0, props),
_v2 = _eq ? _c[0] : _c[0] = props,
_v;
if (_eq ? _c[1] : _c[1] = _v2.condition) {
let _c2 = _$$branch(_c, 2, 1);
_v = _eq ? _c2[0] : _c2[0] = _v2.left;
} else {
let _c3 = _$$branch(_c, 3, 1);
_v = _eq ? _c3[0] : _c3[0] = _v2.right;
}
const value = _v;
}
Almost same process as ternaries, this is to allow short-circuiting while also generating branched caching.
function Example(props) {
const value = props.condition ?? props.right;
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$branch as _$$branch } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
function Example(props) {
let _c = _$$cache(_useMemo, 3),
_eq = _$$equals(_c, 0, props),
_v2 = _eq ? _c[0] : _c[0] = props,
_v3 = _eq ? _c[1] : _c[1] = _v2.condition,
_v;
if (_v3 == null) {
let _c2 = _$$branch(_c, 2, 1);
_v = _eq ? _c2[0] : _c2[0] = _v2.right;
} else _v = _v3;
const value = _v;
}
The following statements are optimized:
- expression statements
- variable declaration
return
statementsthrow
statements- block statements
if-else
statementsfor
statementswhile
anddo-while
statementsswitch-case
statementstry-catch-finally
statements- labeled statements
When a branched statement (a statement with multiple paths) is detected, forgetti
generates a branched cache from a parent cache (by default, the root cache).
Example with an if-statement
:
function Example(props) {
if (props.type === 'a') {
return examples.a(props.value);
} else {
return examples.b(props.value);
}
}
// Compiles intoimport { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$branch as _$$branch } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
function Example(props) {
let _c = _$$cache(_useMemo, 6),
_eq = _$$equals(_c, 0, props),
_v = _eq ? _c[0] : _c[0] = props,
_v2 = _eq ? _c[1] : _c[1] = _v.type,
_eq2 = _$$equals(_c, 2, _v2),
_v3 = _eq2 ? _c[2] : _c[2] = _v2;
if (_eq2 ? _c[3] : _c[3] = _v3 === 'a') {
let _c2 = _$$branch(_c, 4, 3),
_v5 = _eq ? _c2[0] : _c2[0] = _v.value,
_eq3 = _$$equals(_c2, 1, _v5),
_v6 = _eq3 ? _c2[1] : _c2[1] = _v5;
return _eq3 ? _c2[2] : _c2[2] = examples.a(_v6);
} else {
let _c3 = _$$branch(_c, 5, 3),
_v8 = _eq ? _c3[0] : _c3[0] = _v.value,
_eq4 = _$$equals(_c3, 1, _v8),
_v9 = _eq4 ? _c3[1] : _c3[1] = _v8;
return _eq4 ? _c3[2] : _c3[2] = examples.b(_v9);
}
}
Note Nested branches are also supported.
List of supported branch statements:
if-else
try-catch-finally
switch-case
- block statements
- labeled statements
Note Branched statements are usually illegal in a hook-based system (like React), but a
memo
orcallback
call inside the branches are allowed.
Loops are a bit different than normal branches because normal branches are static in size: the amount of branches does not change. Loops are dynamic in branches, it can re-evaluate as many times as the condition is met, so there's an extra step being done. forgetti
generates the branch cache outside of the loop, and also another variable to track its size. This variable is used to index the inner branch cache that is generated inside the loop.
function Example(props) {
for (const x in props.arr) {
console.log(x);
}
}
// Compiles into
import { useMemo as _useMemo } from "react";
import { $$cache as _$$cache } from "forgetti/runtime";
import { $$branch as _$$branch } from "forgetti/runtime";
import { $$equals as _$$equals } from "forgetti/runtime";
function Example(props) {
let _c = _$$cache(_useMemo, 3),
_eq = _$$equals(_c, 0, props),
_v = _eq ? _c[0] : _c[0] = props,
_v2 = _eq ? _c[1] : _c[1] = _v.arr,
_c2 = _$$branch(_c, 2, 0),
_id = 0;
for (const x in _v2) {
let _l = _$$branch(_c2, _id++, 2),
_eq2 = _$$equals(_l, 0, x),
_v3 = _eq2 ? _l[0] : _l[0] = x;
_eq2 ? _l[1] : _l[1] = console.log(_v3);
}
}
List of supported loop statements:
for
for-of
for-in
while
do-while
To disable optimization for an specific component, you can include the comment /* @forgetti skip */
before the component.
/* @forgetti skip */
function UnoptimizedComponent(props) {
return <h1 className={props.className}>{props.children}</h1>;
}
Configuration only has one property: preset
. This property can either be "react"
, "preact"
or your custom preset.
Here's an example preset:
{
// This boolean is to choose if the compiler
// should optimize JSX or not. Defaults to `true`
"shouldOptimizeJSX": false,
// Required. This is used to construct a RegExp
// to match "components". This is done by testing
// either the component's function name or its
// variable name (if declared as a variable)
"componentFilter": {
"source": "^[A-Z]",
"flags": "",
},
// Optional, this has two purposes:
// 1. To match potential function that are hooks
// so that the compiler can also transform it.
// 2. Forces the hook call to follow hook rules,
// since normal function calls are memoized.
"hookFilter": {
"source": "^use[A-Z]",
"flags": "",
},
// This is used to construct the cache
// the information provided is going to be used
// as to where the hook is going to be imported.
memo: {
// Name of the hook
name: 'useMemo',
// What module source does it come from
source: 'react',
// Kind of import (named or default)
kind: 'named',
},
// This defines which hooks are special
// Take note that by default, hooks that match
// the hook filter already follows the hook rules,
// however this configuration is to optimize
// known hooks.
// There's 3 types of hooks it tracks:
// - memo: a hook that resembles `useMemo`
// - callback: a hook that resembles `useCallback`
// - effect: a hook that resembles `useEffect`
// - ref: a hook that resembles `useRef`
//
// The definition is almost the same as the `memo` config
// but you need to define the kind of the hook
hooks: [
{
type: 'ref',
name: 'useRef',
source: 'react',
kind: 'named',
},
{
type: 'memo',
name: 'useMemo',
source: 'react',
kind: 'named',
},
{
type: 'callback',
name: 'useCallback',
source: 'react',
kind: 'named',
},
{
type: 'effect',
name: 'useEffect',
source: 'react',
kind: 'named',
},
],
// This is used to match higher-order components.
// Same configuration as `memo` config.
hocs: [
{
name: 'forwardRef',
source: 'react',
kind: 'named',
},
{
name: 'memo',
source: 'react',
kind: 'named',
},
],
}
MIT © lxsmnsyc