Find out more at synergies.js.org!
synergies
is a tiny (~3kB), yet powerful state management library for React. It allows you to
specify small state atoms, that you can combine into Synergies of multiple atoms that define shared state logic.
Features include
synergies
uses immer
to provide drafts of your state in your update handlers,
so you can more easily update state.immer
is used to let you update drafts of your state,
and smartly detects which atoms were actually changed and which ones were only read from during the update.synergies
to simplify your codebase and speed up your state.Get started by installing the library:
yarn add synergies immer
Create your first state atoms:
const valueAtom = createAtom("");
const isInitialStateAtom = createAtom(true);
Synergyze your atoms to create React Hooks:
const useSetValue = createSynergy(valueAtom, isInitialStateAtom).createAction(
(newValue: string) => (valueDraft, isInitialStateDraft) => {
// Components that read from the value atom will be updated.
valueDraft.current = newValue;
if (isInitialStateDraft.current) {
// If isInitialState is already false, then the draft will not be updated,
// and components that read from it will not trigger a rerender.
isInitialStateDraft.current = false;
}
}
);
// Every atom is also a synergy of itself, so we can call `createSelector` also on atoms.
const useValue = valueAtom.createSelector(value => value);
const useIsInitialState = isInitialStateAtom.useValue; // shortcut for directly reading atom state
Provide your atoms:
<SynergyProvider atoms={[valueAtom, isInitialStateAtom]}>
{/* Components that consume value and isInitialState... */}
{/* We can also nest other synergy providers */}
<SynergyProvider atoms={[moreLocalizedAtom]}>
{/* Can read from and write to all three atoms. */}
</SynergyProvider>
{/* Reuse providers with more localized state */}
<SynergyProvider atoms={[moreLocalizedAtom]}>
{/* ... */}
</SynergyProvider>
</SynergyProvider>
Use your hooks:
const Component = () => {
const setValue = useSetValue();
const value = useValue();
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
)
}
You can find more examples and details at synergies.js.org!
synergies
supports asynchronous update actions. You can also trigger atoms
in the middle of an update handler, so that their subscribers get rerendered before
the action has completed.
This is shown in the following example, where an API fetch call is dispatched in a
action handler. The isLoading
atom is updated to true immediately when the fetch
is dispatched, while the update call continues to load the data from the server.
Components reading the isLoading
atom will be rerendered immediately. Once the data
has loaded, we update the data
atom with the fetched result, and update the isLoading
atom is updated to false, triggering rerenders of all components that read from either
the data
or the isLoading
atom.
Note that, if the isLoading
atom would not rerender a second time at the end, only
components subscribing to the data
atom would be rerendered.
const isLoadingAtom = createAtom(false);
const dataAtom = createAtom(null);
// Async update handler
const useFetchData = createSynergy(dataAtom, isLoadingAtom).createAction(
() => async (data, isLoading) => {
isLoading.current = true;
// Trigger rerenders of all components that read from the `isLoading` atom.
// The `isLoading` draft will be discarded, so we need to use the new one
// that the `trigger` method returns.
isLoading = isLoading.trigger();
const res = await fetch("https://pokeapi.co/api/v2/pokemon/pikachu");
// Update the `data` and `isLoading` atoms
data.current = await res.json();
isLoading.current = false;
}
);
// For simplicity, read from both atoms at once
const usePokemonData = createSynergy(dataAtom, isLoadingAtom).createSelector(
(data, isLoading) => ({ data, isLoading })
);
export const Example = () => {
const { data, isLoading } = usePokemonData();
const fetchData = useFetchData();
const resetData = useReset();
return !data && !isLoading ? (
<Button onClick={fetchData}>Load Pokemon</Button>
) : isLoading ? (
<div>Loading...</div>
) : (
<div>
{data.name} has a height of {data.height} and the abilities{" "}
{data.abilities.map(({ ability }) => ability.name).join(", ")}
</div>
);
};
// ---------------------
// Atoms
// ---------------------
const filterAtom = createAtom(false);
const itemsAtom = createAtom([
{ todo: "First Todo", checked: true },
{ todo: "Second Todo", checked: false },
{ todo: "Third Todo", checked: false },
]);
const inputValueAtom = createAtom("");
// ---------------------
// Selectors and state actions
// ---------------------
const useFilteredItems = createSynergy(itemsAtom, filterAtom).createSelector(
(items, filter) => items.filter(({ checked }) => !filter || checked)
);
const useAddTodo = createSynergy(itemsAtom, inputValueAtom).createAction(
() => (items, input) => {
items.current.push({ todo: input.current, checked: false });
input.current = "";
}
);
const useToggleTodo = itemsAtom.createAction((id: number) => items => {
items.current[id].checked = !items.current[id].checked;
});
const useToggleFilter = filterAtom.createAction(() => filter => {
filter.current = !filter.current;
});
// ---------------------
// Components that use the hooks
// ---------------------
const List = () => {
const items = useFilteredItems();
const toggle = useToggleTodo();
return (
<>
{items.map((item, index) => (
<Checkbox
key={index}
checked={item.checked}
label={item.todo}
onChange={() => toggle(index)}
/>
))}
</>
);
};
const TodoInput = () => {
const [value] = inputValueAtom.useValue();
const setValue = inputValueAtom.useSet();
const addTodo = useAddTodo();
return (
<ControlGroup>
<InputGroup
placeholder="Add a todo"
value={value}
onChange={e => setValue(e.target.value)}
/>
<Button onClick={addTodo}>Add</Button>
</ControlGroup>
);
};
const FilterButton = () => {
const toggle = useToggleFilter();
const [isToggled] = filterAtom.useValue();
return (
<Button onClick={toggle} active={isToggled}>
Only show completed todos
</Button>
);
};
// ---------------------
// App container
// ---------------------
export const App = () => (
// We don't have to nest the providers so extremely, but this demonstrates how you can inject
// atoms at any place in the hierarchy and they can still communicate upwards with other
// atoms.
<SynergyProvider atoms={[filterAtom]}>
<FilterButton />
<SynergyProvider atoms={[itemsAtom]}>
<List />
<SynergyProvider atoms={[inputValueAtom]}>
<TodoInput />
</SynergyProvider>
</SynergyProvider>
</SynergyProvider>
);
When developing locally, run in the root directory...
yarn
to install dependenciesyarn test
to run tests in all packagesyarn build
to build distributables and typings in packages/{package}/out
yarn storybook
to run a local storybook serveryarn build-storybook
to build the storybooknpx lerna version
to interactively bump the
packages versions. This automatically commits the version, tags the commit and pushes to git remote.npx lerna publish
to publish all packages
to NPM that have changed since the last release. This automatically bumps the versions interactively.