{"id":13422403,"url":"https://github.com/lukasbach/synergies","last_synced_at":"2025-04-05T22:31:58.757Z","repository":{"id":37895926,"uuid":"473412970","full_name":"lukasbach/synergies","owner":"lukasbach","description":"Create a performant distributed context state for React by composing reusable state logic.","archived":false,"fork":false,"pushed_at":"2023-03-07T20:09:03.000Z","size":4417,"stargazers_count":9,"open_issues_count":16,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-05-29T08:51:18.158Z","etag":null,"topics":["async","atoms","composable","context","fast","global","hierarchy","local","nested","performance","react","reuse","state","state-management","synergy","typed","typescript"],"latest_commit_sha":null,"homepage":"https://synergies.js.org/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lukasbach.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2022-03-24T01:14:28.000Z","updated_at":"2023-05-02T15:41:38.000Z","dependencies_parsed_at":"2023-02-18T15:30:41.573Z","dependency_job_id":"a89f3cca-2fa7-4307-8bc1-e04bb5feb853","html_url":"https://github.com/lukasbach/synergies","commit_stats":{"total_commits":86,"total_committers":3,"mean_commits":"28.666666666666668","dds":"0.19767441860465118","last_synced_commit":"71ebb9a8a25b66ba484b142f1dc10ee1f9f09240"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukasbach%2Fsynergies","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukasbach%2Fsynergies/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukasbach%2Fsynergies/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukasbach%2Fsynergies/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lukasbach","download_url":"https://codeload.github.com/lukasbach/synergies/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247411238,"owners_count":20934650,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","atoms","composable","context","fast","global","hierarchy","local","nested","performance","react","reuse","state","state-management","synergy","typed","typescript"],"created_at":"2024-07-30T23:00:43.995Z","updated_at":"2025-04-05T22:31:58.369Z","avatar_url":"https://github.com/lukasbach.png","language":"TypeScript","funding_links":[],"categories":["Code Design"],"sub_categories":["Data Store"],"readme":"# Synergies\n\n\u003e Create a performant distributed context state for React and compose reusable state logic.\n\n![Testing](https://github.com/lukasbach/synergies/workflows/Testing/badge.svg)\n![Pretty](https://github.com/lukasbach/synergies/workflows/Pretty/badge.svg)\n![Storybook Deployment](https://github.com/lukasbach/synergies/workflows/Storybook%20Deployment/badge.svg)\n![NPM Version](https://badgen.net/npm/v/synergies)\n![NPM Typings](https://badgen.net/npm/types/synergies)\n![Minzipped size](https://badgen.net/bundlephobia/minzip/synergies)\n![Treeshaking Report](https://badgen.net/bundlephobia/tree-shaking/synergies)\n![Packagephobia Report](https://badgen.net/packagephobia/install/synergies)\n\nFind out more at [synergies.js.org](https://synergies.js.org/)!\n\n`synergies` is a tiny (~3kB), yet powerful state management library for React. It allows you to\nspecify small state atoms, that you can combine into Synergies of multiple atoms that define shared state logic. \nFeatures include\n\n- __Distributed state__: You can inject individual atoms at multiple arbitrary points in your component hierarchy.\n  This allows you to both define unique global state logic in your application root, and smaller reusable components \n  that have their own context-based state. Your state logic can not only read and write to the atoms\n  provided by the closest provider, but to all atoms provided upwards in the component hierarchy.\n- __Immutable update logic__: `synergies` uses `immer` to provide drafts of your state in your update handlers,\n  so you can more easily update state.\n- __Performant update-triggers__: Even though a synergy provider can provide several atoms and can access any atoms\n  from other providers upwards the hierarchy, calling update logic will only trigger updates on components\n  that read from atoms that were actually changed. Again `immer` is used to let you update drafts of your state,\n  and smartly detects which atoms were actually changed and which ones were only read from during the update.\n- __Asynchronous update logic__: No more thunk plugins! Update handlers can be asynchronous, and you can even \n  manually trigger updates on certain atoms while still being in the middle of the update handler.\n- __Reusable state logic__: Since you can provide atom state rather low down in the hierarchy, you can reuse small\n  pieces of state between components, while still maintaining more global parts of your app state farther up\n  in the component hierarchy.\n- __Typed__: Full type safety for everything.\n- __Tiny package__: 3kB + immer. Use it for your global app state, or just replace small context providers with\n  `synergies` to simplify your codebase and speed up your state.\n\n[![Product Hunt](https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=339769\u0026theme=light)](https://www.producthunt.com/posts/synergies?utm_source=badge-featured\u0026utm_medium=badge\u0026utm_souce=badge-synergies)\n\n## Usage\n\nGet started by installing the library:\n\n```bash\nyarn add synergies immer\n```\n\nCreate your first state atoms:\n\n```typescript jsx\nconst valueAtom = createAtom(\"\");\nconst isInitialStateAtom = createAtom(true);\n```\n\nSynergyze your atoms to create React Hooks:\n\n```typescript jsx\nconst useSetValue = createSynergy(valueAtom, isInitialStateAtom).createAction(\n  (newValue: string) =\u003e (valueDraft, isInitialStateDraft) =\u003e {\n    // Components that read from the value atom will be updated.\n    valueDraft.current = newValue;\n    \n    if (isInitialStateDraft.current) {\n      // If isInitialState is already false, then the draft will not be updated,\n      // and components that read from it will not trigger a rerender.\n      isInitialStateDraft.current = false;\n    }\n  }\n);\n\n// Every atom is also a synergy of itself, so we can call `createSelector` also on atoms.\nconst useValue = valueAtom.createSelector(value =\u003e value);\nconst useIsInitialState = isInitialStateAtom.useValue; // shortcut for directly reading atom state\n```\n\nProvide your atoms:\n\n```typescript jsx\n\u003cSynergyProvider atoms={[valueAtom, isInitialStateAtom]}\u003e\n  {/* Components that consume value and isInitialState... */}\n\n  {/* We can also nest other synergy providers */}\n  \u003cSynergyProvider atoms={[moreLocalizedAtom]}\u003e\n    {/* Can read from and write to all three atoms. */}\n  \u003c/SynergyProvider\u003e\n  \n  {/* Reuse providers with more localized state */}\n  \u003cSynergyProvider atoms={[moreLocalizedAtom]}\u003e\n    {/* ... */}\n  \u003c/SynergyProvider\u003e\n\u003c/SynergyProvider\u003e\n```\n\nUse your hooks:\n\n```typescript jsx\nconst Component = () =\u003e {\n  const setValue = useSetValue();\n  const value = useValue();\n  return (\n    \u003cinput \n      value={value}\n      onChange={e =\u003e setValue(e.target.value)}\n    /\u003e\n  )\n}\n```\n\nYou can find more examples and details at [synergies.js.org](https://synergies.js.org/)!\n\n## More advanced examples\n\n### Async update logic\n\n`synergies` supports asynchronous update actions. You can also trigger atoms \nin the middle of an update handler, so that their subscribers get rerendered before\nthe action has completed.\n\nThis is shown in the following example, where an API fetch call is dispatched in a \naction handler. The `isLoading` atom is updated to true immediately when the fetch\nis dispatched, while the update call continues to load the data from the server.\nComponents reading the `isLoading` atom will be rerendered immediately. Once the data\nhas loaded, we update the `data` atom with the fetched result, and update the `isLoading`\natom is updated to false, triggering rerenders of all components that read from either\nthe `data` or the `isLoading` atom.\n\nNote that, if the `isLoading` atom would not rerender a second time at the end, only\ncomponents subscribing to the `data` atom would be rerendered.\n\n```typescript jsx\nconst isLoadingAtom = createAtom(false);\nconst dataAtom = createAtom(null);\n\n// Async update handler\nconst useFetchData = createSynergy(dataAtom, isLoadingAtom).createAction(\n  () =\u003e async (data, isLoading) =\u003e {\n    isLoading.current = true;\n    \n    // Trigger rerenders of all components that read from the `isLoading` atom.\n    // The `isLoading` draft will be discarded, so we need to use the new one\n    // that the `trigger` method returns.\n    isLoading = isLoading.trigger();\n    \n    const res = await fetch(\"https://pokeapi.co/api/v2/pokemon/pikachu\");\n    \n    // Update the `data` and `isLoading` atoms\n    data.current = await res.json();\n    isLoading.current = false;\n  }\n);\n\n// For simplicity, read from both atoms at once\nconst usePokemonData = createSynergy(dataAtom, isLoadingAtom).createSelector(\n  (data, isLoading) =\u003e ({ data, isLoading })\n);\n\nexport const Example = () =\u003e {\n  const { data, isLoading } = usePokemonData();\n  const fetchData = useFetchData();\n  const resetData = useReset();\n  return !data \u0026\u0026 !isLoading ? (\n    \u003cButton onClick={fetchData}\u003eLoad Pokemon\u003c/Button\u003e\n  ) : isLoading ? (\n    \u003cdiv\u003eLoading...\u003c/div\u003e\n  ) : (\n    \u003cdiv\u003e\n      {data.name} has a height of {data.height} and the abilities{\" \"}\n      {data.abilities.map(({ ability }) =\u003e ability.name).join(\", \")}\n    \u003c/div\u003e\n  );\n};\n```\n\n### Nested synergy providers\n\n```typescript jsx\n// ---------------------\n// Atoms\n// ---------------------\nconst filterAtom = createAtom(false);\nconst itemsAtom = createAtom([\n  { todo: \"First Todo\", checked: true },\n  { todo: \"Second Todo\", checked: false },\n  { todo: \"Third Todo\", checked: false },\n]);\nconst inputValueAtom = createAtom(\"\");\n\n// ---------------------\n// Selectors and state actions\n// ---------------------\nconst useFilteredItems = createSynergy(itemsAtom, filterAtom).createSelector(\n  (items, filter) =\u003e items.filter(({ checked }) =\u003e !filter || checked)\n);\n\nconst useAddTodo = createSynergy(itemsAtom, inputValueAtom).createAction(\n  () =\u003e (items, input) =\u003e {\n    items.current.push({ todo: input.current, checked: false });\n    input.current = \"\";\n  }\n);\n\nconst useToggleTodo = itemsAtom.createAction((id: number) =\u003e items =\u003e {\n  items.current[id].checked = !items.current[id].checked;\n});\n\nconst useToggleFilter = filterAtom.createAction(() =\u003e filter =\u003e {\n  filter.current = !filter.current;\n});\n\n// ---------------------\n// Components that use the hooks\n// ---------------------\nconst List = () =\u003e {\n  const items = useFilteredItems();\n  const toggle = useToggleTodo();\n\n  return (\n    \u003c\u003e\n      {items.map((item, index) =\u003e (\n        \u003cCheckbox\n          key={index}\n          checked={item.checked}\n          label={item.todo}\n          onChange={() =\u003e toggle(index)}\n        /\u003e\n      ))}\n    \u003c/\u003e\n  );\n};\n\nconst TodoInput = () =\u003e {\n  const [value] = inputValueAtom.useValue();\n  const setValue = inputValueAtom.useSet();\n  const addTodo = useAddTodo();\n\n  return (\n    \u003cControlGroup\u003e\n      \u003cInputGroup\n        placeholder=\"Add a todo\"\n        value={value}\n        onChange={e =\u003e setValue(e.target.value)}\n      /\u003e\n      \u003cButton onClick={addTodo}\u003eAdd\u003c/Button\u003e\n    \u003c/ControlGroup\u003e\n  );\n};\n\nconst FilterButton = () =\u003e {\n  const toggle = useToggleFilter();\n  const [isToggled] = filterAtom.useValue();\n  return (\n    \u003cButton onClick={toggle} active={isToggled}\u003e\n      Only show completed todos\n    \u003c/Button\u003e\n  );\n};\n\n// ---------------------\n// App container\n// ---------------------\nexport const App = () =\u003e (\n  // We don't have to nest the providers so extremely, but this demonstrates how you can inject\n  // atoms at any place in the hierarchy and they can still communicate upwards with other\n  // atoms.\n  \u003cSynergyProvider atoms={[filterAtom]}\u003e\n    \u003cFilterButton /\u003e\n    \u003cSynergyProvider atoms={[itemsAtom]}\u003e\n      \u003cList /\u003e\n      \u003cSynergyProvider atoms={[inputValueAtom]}\u003e\n        \u003cTodoInput /\u003e\n      \u003c/SynergyProvider\u003e\n    \u003c/SynergyProvider\u003e\n  \u003c/SynergyProvider\u003e\n);\n```\n\n## Maintenance\n\nWhen developing locally, run in the root directory...\n\n- `yarn` to install dependencies\n- `yarn test` to run tests in all packages\n- `yarn build` to build distributables and typings in `packages/{package}/out`\n- `yarn storybook` to run a local storybook server\n- `yarn build-storybook` to build the storybook\n- [`npx lerna version`](https://github.com/lerna/lerna/tree/main/commands/version#readme) to interactively bump the\n  packages versions. This automatically commits the version, tags the commit and pushes to git remote.\n- [`npx lerna publish`](https://github.com/lerna/lerna/tree/main/commands/publish#readme) to publish all packages\n  to NPM that have changed since the last release. This automatically bumps the versions interactively.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukasbach%2Fsynergies","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukasbach%2Fsynergies","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukasbach%2Fsynergies/lists"}