@dbeining/react-atom
v4.1.21
Published
State management made simple for React. Built on React Hooks. Inspired by `atom`s in `reagent.cljs`.
Downloads
13,186
Maintainers
Readme
- Description
- Why use
react-atom
? - Installation
- Documentation
- Code Example:
react-atom
in action - 🕹️ Play with
react-atom
in CodeSandbox 🎮️ - Contributing / Feedback
Description
react-atom
provides a very simple way to manage state in React, for both global app state and for local component state: ✨Atom
s✨.
Put your state in an Atom
:
import { Atom } from "@dbeining/react-atom";
const appState = Atom.of({
color: "blue",
userId: 1
});
Read state with deref
You can't inspect Atom
state directly, you have to deref
erence it, like this:
import { deref } from "@dbeining/react-atom";
const { color } = deref(appState);
Update state with swap
You can't modify an Atom
directly. The main way to update state is with swap
. Here's its call signature:
function swap<S>(atom: Atom<S>, updateFn: (state: S) => S): void;
updateFn
is applied to atom
's state and the return value is set as atom
's new state. There are just two simple rules for updateFn
:
- it must return a value of the same type/interface as the previous state
- it must not mutate the previous state
To illustrate, here is how we might update appState
's color:
import { swap } from "@dbeining/react-atom";
const setColor = color =>
swap(appState, state => ({
...state,
color: color
}));
Take notice that our updateFn
is spreading the old state onto a new object before overriding color
. This is an easy way to obey the rules of updateFn
.
Side-Effects? Just use swap
You don't need to do anything special for managing side-effects. Just write your IO-related logic as per usual, and call swap
when you've got what you need. For example:
const saveColor = async color => {
const { userId } = deref(appState);
const theme = await post(`/api/user/${userId}/theme`, { color });
swap(appState, state => ({ ...state, color: theme.color }));
};
Re-render components on state change with the ✨useAtom
✨ custom React hook
useAtom
is a custom React Hook. It does two things:
- returns the current state of an atom (like
deref
), and - subscribes your component to the atom so that it re-renders every time its state changes
It looks like this:
export function ColorReporter(props) {
const { color, userId } = useAtom(appState);
return (
<div>
<p>
User {userId} has selected {color}
</p>
{/* `useAtom` hook will trigger a re-render on `swap` */}
<button onClick={() => swap(appState, setRandomColor)}>Change Color</button>
</div>
);
}
Nota Bene: You can also use a selector to subscribe to computed state by using the
options.select
argument. Read the docs for details.
Why use react-atom
?
function Awkwardddd(props) {
const [name, setName] = useState("");
const [bigState, setBigState] = useState({ ...useYourImagination });
const updateName = evt => setName(evt.target.value);
const handleDidComplete = val => setBigState({ ...bigState, inner: val });
return (
<>
<input type="text" value={name} onChange={updateName} />
<ExpensiveButMemoized data={bigState} onComplete={handleDidComplete} />
</>
);
}
Every time input
fires onChange
, ExpensiveButMemoized
has to re-render because handleDidComplete
is not strictly equal (===) to the last instance passed down.
The React docs admit this is awkward and suggest using Context to work around it, because the alternative is super convoluted.
With react-atom
, this problem doesn't even exist. You can define your update functions outside the component so they are referentially stable across renders.
const state = Atom.of({ name, bigState: { ...useYourImagination } });
const updateName = ({ target }) => swap(state, prev => ({ ...prev, name: target.value }));
const handleDidComplete = val =>
swap(state, prev => ({
...prev,
bigState: { ...prev.bigState, inner: val }
}));
function SoSmoooooth(props) {
const { name, bigState } = useAtom(state);
return (
<>
<input type="text" value={name} onChange={updateName} />
<ExpensiveButMemoized data={bigState} onComplete={handleDidComplete} />
</>
);
}
Installation
npm i -S @dbeining/react-atom
Dependencies
react-atom
has one bundled dependency, @libre/atom, which provides the Atom data type. It is re-exported in its entirety from @dbeining/atom
. You may want to reference the docs here.
react-atom
also has two peerDependencies
, namely, react@^16.8.0
and react-dom@^16.8.0
, which contain the Hooks API.
Documentation
Code Example: react-atom
in action
import React from "react";
import ReactDOM from "react-dom";
import { Atom, useAtom, swap } from "@dbeining/react-atom";
//------------------------ APP STATE ------------------------------//
const stateAtom = Atom.of({
count: 0,
text: "",
data: {
// ...just imagine
}
});
//------------------------ EFFECTS ------------------------------//
const increment = () =>
swap(stateAtom, state => ({
...state,
count: state.count + 1
}));
const decrement = () =>
swap(stateAtom, state => ({
...state,
count: state.count - 1
}));
const updateText = evt =>
swap(stateAtom, state => ({
...state,
text: evt.target.value
}));
const loadSomething = () =>
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then(res => res.json())
.then(data => swap(stateAtom, state => ({ ...state, data })))
.catch(console.error);
//------------------------ COMPONENT ------------------------------//
export const App = () => {
const { count, data, text } = useAtom(stateAtom);
return (
<div>
<p>Count: {count}</p>
<p>Text: {text}</p>
<button onClick={increment}>Moar</button>
<button onClick={decrement}>Less</button>
<button onClick={loadSomething}>Load Data</button>
<input type="text" onChange={updateText} value={text} />
<p>{JSON.stringify(data, null, " ")}</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
🕹️ Play with react-atom
in CodeSandbox 🎮️
You can play with react-atom
live right away with no setup at the following links:
| JavaScript Sandbox | TypeScript Sandbox | | ------------------------------- | ------------------------------- | | | |
Contributing / Feedback
Please open an issue if you have any questions, suggestions for improvements/features, or want to submit a PR for a bug-fix (please include tests if applicable).