{"id":23171656,"url":"https://github.com/orestis/reseda","last_synced_at":"2026-04-04T06:33:40.739Z","repository":{"id":42126638,"uuid":"262497733","full_name":"orestis/reseda","owner":"orestis","description":"A Clojure-y state management library for modern React, from the future 🚀","archived":false,"fork":false,"pushed_at":"2023-01-27T11:50:17.000Z","size":420,"stargazers_count":75,"open_issues_count":6,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-16T06:02:48.092Z","etag":null,"topics":["clojurescript","react","reactivity"],"latest_commit_sha":null,"homepage":"https://reseda.orestis.gr","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/orestis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-09T05:40:53.000Z","updated_at":"2024-04-29T18:11:34.000Z","dependencies_parsed_at":"2023-02-15T08:46:37.823Z","dependency_job_id":null,"html_url":"https://github.com/orestis/reseda","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/orestis/reseda","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orestis%2Freseda","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orestis%2Freseda/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orestis%2Freseda/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orestis%2Freseda/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/orestis","download_url":"https://codeload.github.com/orestis/reseda/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orestis%2Freseda/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270961682,"owners_count":24675914,"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","status":"online","status_checked_at":"2025-08-18T02:00:08.743Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["clojurescript","react","reactivity"],"created_at":"2024-12-18T04:19:09.295Z","updated_at":"2026-04-04T06:33:40.704Z","avatar_url":"https://github.com/orestis.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Reseda\n\n![tests](https://github.com/orestis/reseda/workflows/tests/badge.svg)\n\nA Clojure-y state management library for modern React, from the future 🚀\n\n## Rationale\n\nFor a long time, React applications in ClojureScript would rely on comprehensive libraries such as [Om](https://github.com/omcljs/om), [Reagent](https://github.com/reagent-project/reagent) and [re-frame](https://github.com/day8/re-frame), [Fulcro](https://fulcro.fulcrologic.com), etc. for a big chunk of web-application concerns, while treating React as purely a view layer. In particular, on top of state management, these libraries would also deal with reactivity: making sure components re-render when a piece of state changes.\n\nWith the introduction of [Context](https://reactjs.org/docs/context.html), [Hooks](https://reactjs.org/docs/hooks-intro.html), and the (currently work-in-progress) [Concurrent Mode](https://reactjs.org/docs/concurrent-mode-intro.html), React is getting more and more opinionated about state management, while at the same time exposing lower-level primitives that can allow fine-grained control over reactivity.\n\nReseda explores the space of using Clojure's philosophy of [Identity, State, and Values](https://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey/) for state management, while leaning on React for reactivity.\n\nThe result is a state managment library that works with plain React components, is REPL friendly, uses plain Clojure atoms as the underlying storage mechanism, can be used both for global and local state, and can be used whenever `useState` is inadequate.\n\nIn addition, by fully embracing [Suspense for Data Fetching](https://reactjs.org/docs/concurrent-mode-suspense.html), Reseda allows you to build User Interfaces without asynchronous data fetching concerns, resulting in less moving parts in your application. And by being compatible with React Stable, it gives you a glimpse of the future today 😎\n\n## Status\n\nUsed in production at [Nosco](https://nos.co), and evolves as we explore various use cases.\n\nThe API might change (but probably not), and there's some unit test coverage but still – use at your own risk.\n\nHappily accepting issues to discuss use cases, bugs, new features etc.\n\nSee [Vision](./VISION.md) for the original vision and potential future additions.\n\n## React version compatibility\n\nReseda is forwards compatible with React 18, and backwards compatible with React 17. \nIt should also work fine with React 16.8 (with hooks), but that's not a goal anymore.\n\n## Usage\n\nUse `deps.edn` to get a reference to the latest commit, e.g.:\n\n    {orestis/reseda {:git/url \"https://github.com/orestis/reseda.git\"\n                     :sha \"\u003clatest commit\u003e\"}}\n\nInstall [use-sync-external-store](https://www.npmjs.com/package/use-sync-external-store) from npm. \nThis is a compatibility shim that makes Reseda compatible with React 17, \nbut also takes advantage of React 18 native APIs if running under React 18.\n\n## Getting Started\n\n### Setting up the store\n\nAt the core, Reseda exposes a `Store` that wraps an `IWatchable` (most often a Clojure atom).\n\n```clojure\n(:require [reseda.state])\n\n;; create the atom\n(defonce backing-store (atom {}))\n\n;; create a new store\n(defonce store (reseda.state/new-store backing-store))\n```\n\nHaving the store at hand, you can subscribe to be notified whenever something in the backing store changes:\n\n```clojure\n;; whenever the value under :fred changes, print it\n(subscribe store :fred println)\n\n;; you can pass in any function as the selector, not just keywords:\n(subscribe store identity tap\u003e)\n(subscribe store (fn [m] (select-keys m [:fred :wilma])) tap\u003e)\n\n;; as a convenience, you can also pass a vector of keywords as the selector, like get-in\n(subscribe store [:fred :name] #(log %))\n```\n\nNote that the value you get is whatever the selector returns, and Clojure's equality via `=` is used to determine if a change was made. The `on-change` function receives a single argument, the new value.\n\nObviously, you can put whatever you want inside the backing store - most usually it will be a map, but it might be a vector or anything else that you can put in an atom.\n\nYou can implement the `IWatchable` protocl (CLJS) or `clojure.lang.IRef` interface (Clojure) for something fancier -- e.g. to trigger subscriptions based on a websocket connection etc.\n\n### React integration\n\nSo far the code is cross-platform, but the main use of Reseda is for building UIs with React.\n\nThe basic setup is exactly the same:\n\n```clojure\n(ns reseda.readme\n  (:require [reseda.state]\n            [reseda.react]\n            [hx.react :refer [defnc]]))\n\n;; be sure to use defonce or your state will be gone if you use hot-reloading on save\n(defonce backing-store (atom {:user {:name \"Fred\"\n                                     :email \"fred@example.com\"}}))\n(defonce store (reseda.state/new-store backing-store))\n```\n\nYou can then re-render a component whenever something changes in the store via the `useStore` hook:\n\n```clojure\n;; The first render will give you whatever is in the store, and\n;; from then on, your component will re-render whenever the value changes\n(defnc Name []\n  (let [name (reseda.react/useStore store [:user :name])]\n   [:div \"The user's name is: \" name]))\n```\n\nNote that [hx](https://github.com/lilactown/hx) is used for the examples, but any library that can\nmake use of React Hooks can be used.\n\nTo make changes, simply change the underlying backing store however you see fit, e.g.:\n\n```clojure\n(defnc EditName []\n  (let [name (reseda.react/useStore store [:user :name])]\n    [:input {:value name\n             :on-change #(swap! backing-store assoc-in\n                                              [:user :name]\n                                              (-\u003e % .-target .-value))}]))\n```\n\nYou have the entire Clojure toolbox at your disposal to make changes. Use plain maps, a statechart library, a Datascript database, whatever fits your use case.\n\n**Note:** `useStore` uses the new `use-sync-external-store` shim by React. This is backwards compatible with React \u003c18 (any version with hooks), but uses the native functionality in React 18.\n\n**Note:** The selector function passed to `useStore` has to be stable, that is,\nnot recreated on every render - otherwise you'll end up in an infinite render loop. Be sure to wrap these selectors in a `useCallback`, or define them as top-level functions. Plain keywords and vectors are automatically wrapped so most of the time you can just forget about this.\n\n### Suspense Integration\n\nReseda supports [Suspense for Data Fetching](https://reactjs.org/docs/concurrent-mode-suspense.html) even in React Stable (16.13), even though it's technically not supported yet, even in React 18.\n\nThis allows you to avoid a whole bunch of asynchronous code by allowing React to suspend rendering if some remote value hasn't arrived yet.\n\nAt the core of this support is the `Suspending` type, which you can construct by giving it a Promise:\n\n```clojure\n(ns reseda.readme\n (:require [reseda.state]\n           [reseda.react]\n           [\"react\" :as React]))\n\n(defn fetch-api []\n ;; fetch a remote resource and return a Javascript Promise\n ,,,)\n\n(defonce backing-store (atom {:data (-\u003e (fetch-api)\n                                        (reseda.react/suspending-value))}))\n(defonce store (reseda.state/new-store backing-store))\n\n;; :data now contains a Suspending that wraps the Promise\n(realized? (:data @backing-store))\n;;=\u003e false\n\n;; after some time passes, the Promise resolves:\n(realized? (:data @backing-store))\n;;=\u003e true\n\n;; you can now deref the Suspending to get the actual data:\n(deref (:data @backing-store))\n;;=\u003e \u003cthe remote data\u003e\n\n;; If the value might be nil, use deref* to get back nil\n(reseda.react/deref* (:missing-data @backing-store))\n```\n\nThe magic happens you combine a Suspending with a React Suspense Boundary:\n\n```clojure\n\n(defnc RemoteName []\n ;; using a trailing * for reader clarity --\n ;; this is a Suspending and you need to deref it\n (let [data* (reseda.react/useStore store :data)]\n   ;; notice the @ that derefs the Suspending\n   [:div \"The remote data is: \" @data*]))\n\n(defnc Root []\n ;; see note about Suspense boundaries\n ;; -- you cannot have them in the same component that suspends\n [React/Suspense {:fallback (hx/f [:div \"Loading...\"])}\n  [RemoteName]])\n```\n\nWhen React tries to render `RemoteName`, and the data hasn't fetched yet, the `deref` of the Suspending will cause React to \"suspend\". This means that the closest `Suspense` component will show its fallback element instead of its children. When the promise resolves, React will try to re-render, in which case the data will have been loaded and rendering proceeds normally (or until a child component suspends).\n\nNote that the result of the `Promise` is not used by React to render anything. The Promise resolving only signals React to re-render the component.\n\nRead more about [Suspense in the official React Docs](https://reactjs.org/docs/react-api.html#reactsuspense).\n\n#### useCachedSuspending\n\nThe final trick that Reseda provides, is the ability to show *previous versions* of a Suspending value. This is useful in \"refreshing\" contexts, where some content is already visible to the user, and replacing that will a fallback will make for a jarring user experience.\n\n\n```clojure\n(defnc SearchResults [{:keys [results*]}]\n [:div (for [v @results*])\n         [Row {:value v}]])\n\n(defnc SearchList []\n (let [results* (reseda.react/useStore store :results)]\n   [SearchBox {:on-change\n               (fn [text]\n                 (swap! backing-store :results (fetch-new-results text)))}]\n   [React/Suspense\n    [SearchResults {:results* results*}]]))\n```\n\nThe moment you change the `:results` value of the backing store to a new Suspending, Reseda will make your component re-render, which in turn will make React suspend, meaning the previous results will be gone from the screen. Not cool.\n\nTo avoid this, wrap the `Suspending` value with a `useCachedSuspending` hook like so:\n\n```clojure\n(defnc SearchList []\n (let [[results* loading?] (-\u003e (reseda.react/useStore store :results)\n                               (reseda.react/useCachedSuspending))]\n   [SearchBox {:show-spinner loading?\n               :on-change\n               (fn [text]\n                (swap! backing-store :results (fetch-new-results text)))}]\n   [React/Suspense\n    [SearchResults {:results* results*\n                    :loading? loading?}]]))\n```\n\n`useCachedSuspending` will return a vector of the suspending plus a boolean that indicates if a new value is on the way.\n\nIn React 18, this is simply a wrapper over `useDeferredValue`. In React 17, it's keeping track of the last resolved `Suspending` and returning that until the next one resolves.\n\n**Note:** In React \u003c18, `useCachedSuspending` will add a callback to the underlying Promise of the `Suspending`. This should be harmless and only does side-effects related to React. The actual value is passed-through unchanged.\n\n\n### Local state\n\nWhile all the examples so far were dealing with global atoms and stores, you can also use Reseda for local state. You just need to make sure that React doesn't throw away your local state. You can do that with a `useRef`:\n\n```clojure\n(defnc ComplexComponent []\n (let [backing-ref (React/useRef (atom {})\n       backing (.-current backing-ref)\n       store-ref (React/useRef (reseda.state/new-store backing)))\n       store (.-current store-ref)]\n    [ReadOnlyComponent {:store store}\n    [WriteOnlyComponent {:backing backing}\n    [ReadWriteComponent {:store store :backing backing}]]]))\n```\n\nReact will make sure that the atom and the wrapping store will stay the same during the lifecycle of the component (ie from mount to unmount), so you can pass the \"current\" value around as props to any child components that may need them.\n\nThe separation of store and backing also makes it clear if a component is just reading values from the store or also writing values into it.\n\n### Gotchas and advanced topics\n\nDue to the way React works, you need to keep in mind a few things:\n\n#### Selector functions\n\nSelector functions should have a consistent identity, (i.e. not re-created every render), otherwise you may go into a render-loop.\n\n* Keywords and vectors of keywords \"just work\"\n* Functions defined via `defn` also work, since their identity doesn't change\n* Anything else has to be wrapped inside a `useCallback`\n\n\nReseda doesn't try to do any batching or asynchronicity of subscriptions. This means that one change to the underlying atomwill trigger one subscription (assuming of course the selected value *does* change).\n\nThis is fine in practice, since often React will batch the DOM updates, but if you care to avoid multiple renders, make sure you `swap!` just once.\n\n#### Caching, derivative values and extra logic\n\nReseda doesn't do any caching and will naively re-run all your subscriptions every time the underlying atom changes.\n\nIf some selector functions are expensive, you would probably want to either pre-calculate their values and store them in a different place. You can do this in numerous ways, e.g.:\n\n* By doing all the work during a simple `swap!`\n* By adding an extra watch (via plain `add-watch`) *before* you create the Reseda `Store`. This way you can catch changes to the store before the Reseda subscriptions run.\n\n#### Suspense Boundaries\n\nYou cannot have a Suspense boundary inside the component that does the Suspending. This is because React walks the component tree upwards to find the next Suspense boundary, and will throw away the results of the current render (that put the inline Suspense boundary in place).\n\n\n## Non-goals\n\n* Creating React elements via hiccup or other means. There is already a lot of exploration happening in the space, with libraries such as [hx](https://github.com/Lokeh/hx) and [helix](https://github.com/Lokeh/helix), and Hiccup parsers such as [Hicada](https://github.com/rauhs/hicada) and [Sablono](https://github.com/r0man/sablono).\n\n* Server-side rendering (Node or JVM). I'm not personally interested in this for the kind of applications I develop. However once the progressive hydration story of React stabilises, it might be interesting to revisit.\n\n## Demo\n\nThere's various demos at [src/reseda/demo](src/reseda/demo). You can follow along at https://reseda.orestis.gr or run `clojure -M:demo:shadow-cljs watch demo`\n\n## License\n\n```\nCopyright (c) Orestis Markou. All rights reserved. The use and\ndistribution terms for this software are covered by the Eclipse\nPublic License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)\nwhich can be found in the file epl-v10.html at the root of this\ndistribution. By using this software in any fashion, you are\nagreeing to be bound by the terms of this license. You must\nnot remove this notice, or any other, from this software.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forestis%2Freseda","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Forestis%2Freseda","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forestis%2Freseda/lists"}