Global state management tool for react hooks inspired by RecoilJS and Jotai using proxies.
- No need for context
- Zero dependencies
- Super lightweight: ~ 1kb gzipped
Inspired by RecoilJS with its 3D state management where the state does not live with the virtual dom tree, I wanted to create a simpler and much more lightweight version for modern browsers (IE is dead!!). The current state management solutions in the react ecosystem work really well (mobx, redux, etc), but I think a recoil like library that allows for granular updates without context and without having to rerender the whole DOM tree is the future. Thus Entangle was born. The name Entangle comes from quantum entanglement where two Atoms are linked event across great distances and can affect each other.
This library is written in TS and has typings shipped with it.
This library should work with all browsers that support proxies (aka all modern browsers). However if you need to support other browsers there is a polyfill available, though that wont be officially supported by this library.
Please try this library out and let me know if you encounter any bugs and suggestions on improvements. Its still very much in the experimental and testing phase so try at your own risk.
Super simple example with makeAtom
import { makeAtom, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const Component1 = () => {
const [atomState, setAtomState] = useEntangle(atomValue);
return (
<div>
<button onClick={() => setAtomState("Hello, 世界")}>Update atomState</button>
<h1>{atomState}</h1>
</div>
);
}
const Component2 = () => {
const [atomState, setAtomState] = useEntangle(atomValue);
return (
<div>
<button onClick={() => setAtomState("Hello World")}>Update atomState</button>
<h1>{atomState}</h1>
</div>
);
}
In the above example, a global atomValue
is created with the initial value passed in. Then the components that need to access that value will pass in the atomValue
to a useEntangle
hook inside the component.
The useEntangle
hook works the same way as a useState
hook, the first value is the value, while the second is an updater function. If either of the buttons are clicked and they update the atomState
, then both components (and only those components and their children) will rerender, staying in sync. Most importantly the parents will not rerender.
import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const moleculeValue = makeMolecule((get) => get(atomValue) + " world");
const Component = () => {
const [atomState] = useEntangle(moleculeValue);
return (
<div>
<h1>{atomState}</h1>
</div>
);
}
Entangle also supports composition using atoms as well. You can pass a function to makeMolecule
that takes a get method and composes the composed value using get
to get the atom's current value and subscribe to those changes.
import { makeAtom, makeAsyncMolecule, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const asyncMoleculeValue = makeAsyncMolecule(async (get) => {
const response = await fetch(`API/${get(atomValue)}`);
const value = await response.json(); // { value: "Hello World" }
return value;
}, {
value: "Default"
}});
const Component = () => {
const [atomState] = useEntangle(asyncMoleculeValue);
return (
<div>
<h1>{atomState}</h1>
</div>
);
}
Entangle also supports async molecules as well with the makeAsyncMolecule
method. You can do API calls using atom values here, and they will automatically update and subscribe to those atom changes. The value of the second parameter must match the return value of the async generator function passed in.
For example the below example wont work since you passed in a string for a default value but the async function returns an object.
import { makeAtom, makeAsyncMolecule, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const asyncMoleculeValue = makeAsyncMolecule(async (get) => {
const response = await fetch(`API/${get(atomValue)}`);
const {value} = await response.json(); // { value: "Hello World" }
return { response: value };
}, "HELLO WORLD");
const Component = () => {
const [atomState] = useEntangle(asyncMoleculeValue);
return (
<div>
<h1>{atomState}</h1>
</div>
);
}
For this reason it is better to add explicit types (if you are using TS) to the make methods:
import { makeAtom, makeMolecule, makeAsyncMolecule, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom<string>("1");
const moleculeValue = makeMolecule<number>((get) => parseInt(get(atomValue)));
const atomValue = makeAsyncMolecule<{value: string}>(async (get) => ({value: get(atomValue)}));
## API
makeAtom creates an atom value to be used inside the useEntangle hook. All components using this value will be synced and updated when any of the components update the atom. It does not matter how deeply nested.
import { makeAtom, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const Component = () => {
const [atomState, setAtomState] = useEntangle(atomValue);
return (
<div>
<button onClick={() => setAtomState("Hello, 世界")}>Update atomState</button>
<h1>{atomState}</h1>
</div>
);
}
makeMolecule allows for subscriptions to an atoms changes for composing values based off other atoms.
import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const atomValue2 = makeAtom("world");
// In the below example, you can pass in am optional boolean as a second argument to the getter, this will un subscribe the molecule from that atoms changes
const moleculeValue = makeMolecule((get) => get(atomValue) + get(atomValue2, false));
const Component = () => {
const [atomState, setAtomState] = useEntangle(atomValue);
const [moleculeState] = useEntangle(moleculeValue);
return (
<div>
<button onClick={() => setAtomState("Hello, 世界")}>Update atomState</button>
<h1>{atomState}</h1>
<h1>{moleculeState}</h1>
</div>
);
}
It is important to note that since molecules are dependent on atoms. They are read only, thus while they can be used with useEntangle
, calling the set function will throw an error. As a result they should be used with useReadEntangle
import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const moleculeValue = makeMolecule((get) => get(atomValue) + " world");
const Component = () => {
const [atomState, setAtomState] = useEntangle(atomValue);
const [moleculeState, setMoleculeState] = useEntangle(moleculeValue); // not recommended
const readOnlyMoleculeState = useReadEntangle(moleculeValue);
return (
<div>
<button onClick={() => setAtomState("Hello, 世界")}>Update atomState</button>
<button onClick={() => setMoleculeState("Hello, 世界")}>Throws an error</button>
<h1>{atomState}</h1>
<h1>{moleculeState}</h1>
<h1>{readOnlyMoleculeState}</h1>
</div>
);
}
Same usage as makeMolecule
except you pass in an async function and a default value as the second argument.
import { makeAtom, makeMolecule, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const moleculeValue = makeMolecule(async (get) => get(atomValue) + " world", "defaultValue");
const Component = () => {
const [atomState, setAtomState] = useEntangle(atomValue);
const [moleculeState] = useEntangle(moleculeValue);
return (
<div>
<button onClick={() => setAtomState("Hello, 世界")}>Update atomState</button>
<h1>{atomState}</h1>
<h1>{moleculeState}</h1>
</div>
);
}
Sometimes we want to do side effects that update other atoms outside of a component, thats where makeAtomEffect
comes in handy.
You pass it a function that has a getter and setter passed to it and in it you can get and set atoms, be aware of infinite loops though as the
makeAtomEffect
subscribes to all the getters it uses/calls
import { makeAtom, makeAtomEffect } from "@bennyg_123/entangle";
const atomValue1 = makeAtom("Hello");
const atomValue2 = makeAtom(" World");
const combinedValue = makeAtom("");
makeAtomEffect((get, set) => {
const value1 = get(atomValue);
// Similar to get molecule, for the getter function, if you pass in a false boolean as the second parameter, it will not subscribe to the atoms changes
const value2 = get(atomValue, false);
set(combinedValue, value1 + value2);
})
useEntangle
entangles the atoms together with the components and syncs them. The API is the same as useState
and whenever an atom is updated, all other components that has useEntangle
with that atom value or has useEntangle
with a molecule that is composed with that atom value will get updated.
***if a molecule is passed in, calling the set function will throw an error. Thus it is advised to use molecules with useReadEntangle
instead. ***
import { makeAtom, useEntangle } from "@bennyg_123/entangle";
const atomValue = makeAtom("Hello");
const Component = () => {
const [atomState, setAtomState] = useEntangle(atomValue);
return (
<div>
<button onClick={() => setAtomState("Hello, 世界")}>Update atomState</button>
<h1>{atomState}</h1>
</div>
);
}
For reading and setting multiple atoms, you can use useMultiEntangle
with multiple atoms. You pass in a list of atoms as arguments and it'll return getters and setters in that order.
Unfortunately due to my own limited knowledge with advanced ts, I was unable to make typing work with the useMulti
hooks. I am actively looking for a way for TS to infer the types of atoms passed in and evaluate typings, but any help would be greatly appreciated.
import { makeAtom, useMultiEntangle } from "@bennyg_123/entangle";
const atom1 = makeAtom("Hello");
const atom2 = makeAtom("World");
const atom3 = makeAtom1("!!!");
const Component = () => {
const [
[atom1Value, atom2Value, atom3Value],
[setAtom1, setAtom2, setAtom3]
] = useMultiEntangle(atom1, atom2, atom3);
...
}
Sometimes in our components we don't want to allow for updates to an atom and only want to consume the values, thats where useReadEntangle comes in handy. It only returns a read only value and lets the component subscribe to the atom changes.
import { makeAtom, useReadEntangle } from "@bennyg_123/entangle";
const atom1 = makeAtom("Hello");
const Component = () => {
const value = useReadEntangle(atom1);
return (
<div>{value}</div>
)
}
When one needs to read from multiple atoms and stay subscribed use useMultiReadEntangle
. Pass in multiple atoms and get the values back in an array.
import { makeAtom, useMultiReadEntangle } from "@bennyg_123/entangle";
const atom1 = makeAtom("Hello");
const atom2 = makeAtom("World");
const atom3 = makeAtom1("!!!");
const Component = () => {
const [atom1Value, atom2Value, atom3Value] = useMultiReadEntangle(atom1, atom2, atom3);
...
}
Sometimes a component only needs to set an atom's value and not subscribe to those changes, as a result useSetEntangle will only return a function that'll set an atoms value and update other components subscribed but not the current component. useSetEntangle will not take in a molecule
import { makeAtom, useSetEntangle } from "@bennyg_123/entangle";
const atom1 = makeAtom("Hello");
const Component = () => {
const setValue = useSetEntangle(atom1);
// Is not subscribed to ATOM changes
return (
<button onClick={() => setValue("World")}>Update Atom</button>
)
}
When one needs to set multiple atoms use useMultiSetEntangle
. Pass in multiple atoms and get the setters back in an array.
import { makeAtom, useMultiSetEntangle } from "@bennyg_123/entangle";
const atom1 = makeAtom("Hello");
const atom2 = makeAtom("World");
const atom3 = makeAtom1("!!!");
const Component = () => {
const [setAtom1, setAtom2, setAtom3] = useMultiSetEntangle(atom1, atom2, atom3);
...
}
For certain situations it might advantageous to manually call a side effect function without having it subscribe to atom changes. For this makeAtomEffectSnapshot
can be used.
makeAtomEffectSnapshot
takes in a function or async function exactly like makeAtomEffect, with a getter and setter parameter and returns a function that can be called with arguments when the developer wants the side effect function to be run.
import { makeAtom, makeAtomEffectSnapshot } from "@bennyg_123/entangle";
const atom1 = makeAtom("Hello");
const snapshotFN = makeAtomEffectSnapshot(async (get, arg1) => {
writeToDB(get(atom1) + arg1);
});
const Component = () => {
useEffect(() => {
snapshotFN("ARG")
}, [])
// Is not subscribed to ATOM changes
return (<></>)
}
When we need to have a array or set of atoms, makeAtomFamily can help. It is an atom generator that takes either an initial value or function that returns an initial value, and outputs a helper function to generate atoms on the fly.
You can pass in values as arguments for initialization, and then use it the exact same as a regular atom. The first argument must be a string as this acts as a key to differentiate an atom from each other, thus if one component updates an atom, then the other components using an atomFamily wont get updated. This also allows atoms in families to be shared if they use the same key.
import { makeAtomFamily } from "@bennyg_123/entangle";
const atomFamily = makeAtomFamily("Hello");
const Component1 = () => {
const setValue = useEntangle(atomFamily("A"));
// Component1 will not update Component2
return (
<button onClick={() => setValue("World")}>Update Atom</button>
)
}
const Component2 = () => {
const setValue = useEntangle(atomFamily("B"));
// Component2 will not update Component1
return (
<button onClick={() => setValue("World")}>Update Atom</button>
)
}
const Component3 = () => {
const setValue = useEntangle(atomFamily("A"));
// Component1 will update Component3
return (
<button onClick={() => setValue("World")}>Update Atom</button>
)
}
import { makeAtomFamily } from "@bennyg_123/entangle";
// First argument is always a string that acts as a key to differentiate atoms
const atomFamily = makeAtomFamily((arg1: string, arg2) => parseInt(arg1) + arg2);
const Component = () => {
const setValue = useEntangle(atomFamily("1", 2));
return (
// All subsequent sets to the atom should be set like a regular atom and not via the function
<button onClick={() => setValue(32)}>Update Atom</button>
)
}
const Component2 = () => {
const setValue = useEntangle(atomFamily("3", 4));
return (
<button onClick={() => setValue(24)}>Update Atom</button>
)
}
Same as makeAtomFamily but instead of instantiating atoms, it instantiates molecules. The initializer function has a getter function (same as makeMolecule), a key, and arguments passed in. The return function subsequently takes a unique string key and any additional arguments that need to be passed to the molecule function.
import { makeAtom, makeMoleculeFamily } from "@bennyg_123/entangle";
const atom = makeAtom("Hello");
const moleculeFamily = makeMoleculeFamily((get, key, arg1) => `${get(atom)} ${key} ${arg1}`);
const Component1 = () => {
const value = useReadEntangle(moleculeFamily("A", 123));
// will render `Hello A 123`
return (
<div>{value}</div>
)
}
Same as makeMoleculeFamily except this takes in an async function and also takes either an initial value or a synchronous function to generate an initial value for the async molecules.
import { makeAtom, makeAsyncMoleculeFamily } from "@bennyg_123/entangle";
const atom = makeAtom("Hello");
const asyncMoleculeFamily = makeAsyncMoleculeFamily(async (get, key, arg1) => {
value = await db.get(); // returns ABCD
`${get(atom)} ${key} ${arg1} ${value}`
}, "Loading");
const Component1 = () => {
const value = useReadEntangle(asyncMoleculeFamily("A", 123));
// will render `Loading` at first then `Hello A 123 ABCD` when the db call is done
return (
<div>{value}</div>
)
}
import { makeAtom, makeAsyncMoleculeFamily } from "@bennyg_123/entangle";
const atom = makeAtom("Hello");
const asyncMoleculeFamily = makeAsyncMoleculeFamily(async (get, key, arg1) => {
value = await db.get(); // returns ABCD
`${get(atom)} ${key} ${arg1} ${value}`
}, (get, key, arg1) => `Loading ${key} ${arg1}`);
const Component1 = () => {
const value = useReadEntangle(asyncMoleculeFamily("A", 123));
// will render `Loading A 123` at first then `Hello A 123 ABCD` when the db call is done
return (
<div>{value}</div>
)
}
Since the makeAtomEffect
, makeAsyncMolecule
and makeMolecule
functions run automatically, sometimes when a large amount of changes are made at the same, this could result in running expensive functions multiple times. Thus there is the option to debounce these functions by waiting a set time before running them. This can be achieved by passing a time in ms as the last argument when initializing an Atom Effect or a Molecule.
// This effect will debounce and be run 500 ms after receiving the last atom update. If an atom is updated before 500ms then the debounce timer is reset and the effect wont run.
makeAtomEffect((get, set) => { ... }, 500);
// Equivalent debounce usage for molecules and async molecules
makeMolecule((get, set) => { ... }, 500);
makeAsyncMolecule((get, set) => { ... }, {}, 500)
To develop, you can fork this repo.
To build:
yarn && yarn run build
To run test:
yarn && yarn run test
To run lint:
yarn && yarn run lint
To run example page:
yarn && yarn run example
Thank you so much for trying this library out. Please leave feedback in the issues section. Have fun.