{"id":17297722,"url":"https://github.com/jarohen/oak","last_synced_at":"2025-10-13T14:06:52.568Z","repository":{"id":62433086,"uuid":"67684608","full_name":"jarohen/oak","owner":"jarohen","description":"A ClojureScript library to structure single-page apps - taking inspiration from the Elm Architecture","archived":false,"fork":false,"pushed_at":"2018-08-18T22:15:52.000Z","size":115,"stargazers_count":13,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-10-13T14:06:04.639Z","etag":null,"topics":["clojurescript","elm","reagent","spa"],"latest_commit_sha":null,"homepage":"","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/jarohen.png","metadata":{"files":{"readme":"README.org","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":"2016-09-08T08:36:00.000Z","updated_at":"2021-09-29T01:49:48.000Z","dependencies_parsed_at":"2022-11-01T20:47:18.576Z","dependency_job_id":null,"html_url":"https://github.com/jarohen/oak","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/jarohen/oak","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarohen%2Foak","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarohen%2Foak/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarohen%2Foak/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarohen%2Foak/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jarohen","download_url":"https://codeload.github.com/jarohen/oak/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarohen%2Foak/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279015699,"owners_count":26085747,"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-10-13T02:00:06.723Z","response_time":61,"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","elm","reagent","spa"],"created_at":"2024-10-15T11:16:54.242Z","updated_at":"2025-10-13T14:06:52.531Z","avatar_url":"https://github.com/jarohen.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"* Oak\nA ClojureScript library to structure single-page [[https://github.com/reagent-project/reagent][Reagent]] apps - taking\ninspiration from the Elm Architecture.\n\n** Concepts\n\nOak components are supersets of Reagent components - most of what you already\nknow from Reagent applies here too.\n\n*** State management\n\nState management, in CLJS/Reagent applications, has traditionally been handled\nin one of two ways:\n\n- We /could/ store it in local Reagent 'ratoms', but over time, this has proven\n  to be problematic - if we want to serialise/re-construct the state of the\n  whole application, we have to go to each component and extract its individual\n  state. We'd rather this was available centrally.\n- We /could/ store it all in one global ratom. This has the advantage of being\n  able to serialise the entire app state easily. However, it's not ideal for\n  state that's logically scoped to a given component - should the component know\n  where in the global atom to store it? Particularly, if we have multiple\n  instances of the same component on screen, we don't want every individual\n  instance to know /exactly/ where to store its state.\n\nIt seems that neither storing all our state in one atom, nor individually in\neach component, is a good solution to this problem.\n\nIn Oak, therefore, we explicitly distinguish between these areas of state -\nstate that's relevant to the whole application (usually business entities, known\nin Oak as the 'db'), and state that's only relevant to a single component (known\nas 'component local').\n\nIn any given component, Oak exposes both the 'db' and the component's local\nstate - but, overall, it stores the state of the application in one global\nratom. Best of both worlds, hopefully!\n\n*** Events + commands\n\nWe update the state by raising events in our DOM elements. Event handlers are\ndefined separately from components - they are functions that accept the current\nstate of the world and an event, and return the new state of the world. In Elm,\nthese are implemented as a top-level function which delegates as necessary; in\nOak, we use multimethods.\n\nEvent handlers are synchronous - they are expected to return the new state\nimmediately. Fortunately, they can also call 'commands' - functions that accept\nsome command data and a callback. When the asynchronous part finishes, we call\nthe callback, passing it an event value, and the cycle continues.\n\n** Creating components\n\nIn Oak, we wrap component functions using the ~oak/defc~ macro:\n\n#+BEGIN_SRC clojure\n  (oak/defc simple-counter []\n    (let [counter-value (oak/*db* get ::counter 0)]\n      [:div {:class #{:counter}}\n       [:div\n        \"The current value of the counter is \"\n        counter-value]\n\n       [:button {:oak/on {:click [::counter-incremented {}]}}\n        \"Increment!\"]]))\n#+END_SRC\n\nA couple of points to note here:\n- We accessed the DB using ~oak/*db*~, which accepts ~f \u0026 args~ - if we didn't\n  want the default, we could simply use ~(oak/*db* ::counter)~. When this data\n  changes, the appropriate components get re-rendered.\n- To access component-local state, we'd use ~oak/*local*~ - more on that later.\n- When the button's clicked, we raise an Oak event of type\n  ~::counter-incremented~. We can optionally pass a map of parameters to the\n  event handler.\n\nTo define an event handler, we implement the ~oak/handle~ defmulti:\n\n#+BEGIN_SRC clojure\n  (defmethod oak/handle ::counter-incremented [state ev]\n    (-\u003e state\n        (oak/update-db update ::counter (fnil inc 0))))\n#+END_SRC\n\nIn addition to ~update-db~, we also have ~get-db~, ~get-local~ and\n~update-local~.\n\nThe original React event is available in the ~ev~ map, under `:oak/react-ev`.\n\n** Introducing commands - HTTP requests\n\nEvent handlers have to return the next state of the world /synchronously/. To\nspawn an asynchronous action (for example, an HTTP request) we have to instruct\nOak to run a command. Let's say we want to relay the counter action to our server:\n\n#+BEGIN_SRC clojure\n    ;; (:require [oak.http :as http])\n\n    (defmethod oak/handle ::counter-incremented [state _]\n      (-\u003e state\n          (oak/update-db update ::counter (fnil inc 0))\n          (oak/with-cmds [::http/request! {:method :post,\n                                           :url \"/api/increment-counter\"\n                                           :ev [::increment-resp-received]}])))\n#+END_SRC\n\nThe command data structure is similar to an event - a command type and some\nparams. Here, the HTTP command is helpfully allowing us to specify another event\nthat we'd like to be fired when the response is received.\n\nTo make our own commands, we implement the ~oak/cmd!~ multimethod. For example,\na simplified version of the above HTTP request command could be:\n\n#+BEGIN_SRC clojure\n  (ns oak.http\n    (:require [oak.core :as oak]\n              [cljs-http.client :as http]\n              [cljs.core.async :as a])\n    (:require-macros [cljs.core.async.macros :refer [go]]))\n\n  (defmethod oak/cmd! ::request! [{:keys [method url] :as opts} cb]\n    (go\n      (let [resp (a/\u003c! (http/request opts))]\n        (cb [::response-received {::resp resp}]))))\n#+END_SRC\n\nHere, we're using core.async to wait for the result of the cljs-http request,\nand then calling through to the callback, generating a ~::response-received~\nevent (to be handled by another ~oak/handle~ defmethod).\n\n** Within components\n*** Component local state\nIn the counter example, above, we used the 'DB' area of Oak's state to store\ndata that was relevant to the whole application. Frequently, though, we have to\nstore data that's scoped to a given component - which we then access with\n~oak/*local*~.\n\nThe 'component-local' part of the Oak state typically has a tree structure that\nloosely mirrors the component tree - each parent component stores the state of\nits child components. Having said that, we'd like each component to be able to\naccess its state without knowing the tree structure above, so we borrow an idea\nfrom 'lenses' - specifying how to 'focus' from the larger data structure to the\ncomponent's individual state.\n\n#+BEGIN_SRC clojure\n  (oak/defc todo-item [{:keys [todo-id]}]\n    (let [{:keys [...]} (oak/*local* ...)\n          {:keys [todo-id label status] :as todo} (oak/*db* get-in [:todos todo-id])]\n      [:li\n       ...]))\n\n  (oak/defc todo-list [{:keys [todo-filter]}]\n    (let [{:keys [...]} (oak/*local* ...)]\n      [:ul.todo-list\n       (doall\n        (for [{:keys [todo-id]} ...]\n          ^{:key (str todo-id)\n            :oak/focus [:items todo-id]}\n          [todo-item {:todo-id todo-id}]))]))\n#+END_SRC\n\nPoints to note:\n- We specify the focus as meta-data on the call to the child component, in the\n  same way as we specify the React key on a collection of child elements - in\n  fact, the two are often found together.\n- ~oak/*local*~ returns the state of the component that it's called from - it'll\n  return a different state in each individual item than it will in the parent.\n\nWe often store the current value of form inputs in local state, and then copy it\nto the DB when the user chooses to save it. If we were to follow the React\npattern of storing the current value in a prop, specifying the value of the\ninput box, and registering a change handler, it'd look something like this:\n\n#+BEGIN_SRC clojure\n  (defmethod oak/handle ::input-updated [state ev]\n    (-\u003e state\n        (oak/update-local assoc :input-value (-\u003e ev :oak/react-ev .-target .-value))))\n\n  (oak/defc my-form []\n    (let [{:keys [input-value]} (oak/*local* select-keys [:input-value])]\n      [:form\n       [:input {:type :text\n                :value input-value\n                :oak/on {:change [::input-updated]}}]]))\n#+END_SRC\n\nThis gets quite boring quite quickly - so, for the simple case, Oak provides\n'binds':\n\n#+BEGIN_SRC clojure\n  (oak/defc my-form []\n    [:form\n     [:input {:type :text, :oak/bind [:input-value]}]])\n#+END_SRC\n\nThe 'bind', in this case, is the path into the local state that stores the\ncurrent value of that input field.\n\n(TODO: implement binds for non-text fields)\n\n*** Component lifecycle\n\nWe can attach events to React/Reagent's usual component lifecycle. For example,\nto raise an event when a component's about to be mounted, we would write:\n\n#+BEGIN_SRC clojure\n  (defmethod oak/handle ::my-component-will-mount [state _]\n    ...)\n\n  (oak/defc my-component [...]\n    {:oak/on {:component-will-mount [::my-component-will-mount {...}]}}\n\n    [:div.my-component\n     ...])\n#+END_SRC\n\nA common use-case here is to set up some state when the component mounts, and\ntear it down when the component un-mounts. Fortunately, given this is such a\ncommon use-case, we provide an ~:oak/transients~ option, where you can set up\ntransient component state:\n\n#+BEGIN_SRC clojure\n  (oak/defc my-component [...]\n    {:oak/transients [{:keys [selected-filter]} {:selected-filter :all}]}\n\n    [:div\n     (case selected-filter\n       :all \"you selected all\"\n       :some \"you selected some\")])\n#+END_SRC\n\nIn the 'transients' option, we're specifying a binding for our transient state,\nand the initial value. Transient state is stored in the local component state,\nand is updated in the same way - likewise, it can be used in 'binds'.\n\n*** Child → parent communication\n\nOften, child components need to relay something the user's done to their parent\ncomponent - let's say, the user's finished with the child component and wants it\nto go away. The close button, and hence the responsibility for initially\nhandling the user action, is on the child component - but the decision for what\nto do next (and the state to make it happen) rests with the parent.\n\nIn Oak, parent components can specify a 'listener event' when calling through to\na child component. When the child component wants to raise an event to their\nparent, they call 'notify' within one of their event handlers:\n\n#+BEGIN_SRC clojure\n  (defmethod oak/handle ::child-form-submitted [state ev]\n    (-\u003e state\n        (cond-\u003e form-valid? (oak/notify [::notify-child-form-submitted {...}]))))\n\n  (oak/defc child-component [...]\n    [:form {:oak/on {:submit [::child-form-submitted {...}]}}\n     ...\n     [:button {:type :submit}\n      \"Save\"]])\n\n  (defmethod oak/handle [::child-form-listener ::notify-child-form-submitted] [state ev]\n    (-\u003e state\n        (update-local assoc :child-visible? false)\n        ...))\n\n  (oak/defc parent-component [...]\n    [:div\n     ^{:oak/listener-ev [::child-form-listener {...}]}\n     [child-component ...]])\n#+END_SRC\n\nThis allows the child component to be re-used in different contexts - the notify\nevent becomes part of the child's API, for each parent to handle.\n\n(I'm particularly interested in feedback on this, both the concept and the\nimplementation - there are many, many different ways to handle it!)\n\n** Navigation - HTML5 history\n\nOak provides basic navigation support, backed by [[https://github.com/juxt/bidi][Bidi]]. To set this up, you first\nneed to initialise it on app startup:\n\n#+BEGIN_SRC clojure\n  (:require [oak.nav :as nav]\n            [oak.nav.bidi :as nav.bidi])\n\n  (def bidi-routes\n    [\"\" {\"/home\" :home\n         \"/page2\" :page-2}])\n\n  (defmethod oak/handle ::app-mounted [state _]\n    (-\u003e state\n        (oak/with-cmd [::nav/init-nav {::nav/router (nav.bidi/-\u003eRouter bidi-routes)}])))\n\n  (oak/defc app-root [...]\n    {:oak/on {:component-will-mount [::app-mounted]}}\n\n    [:div \"Welcome!\"])\n#+END_SRC\n\nYou can then:\n- access the current location (in the form of a map containing ~:handler~,\n  ~:route-params~, ~:query-params~ and ~:history-state~) by calling ~(oak/*db*\n  ::nav/location)~.\n- change the location in your event handlers, using the ~[::nav/push-location\n  {:location {...}}]~ and ~[::nav/replace-location {:location {...}}]~ commands.\n- generate links - ~[:a (nav/link location) \"Link text\"]~\n\nYou can also react to changes in the location using three multimethods -\n~nav/handle-mount~, ~nav/handle-change~ and ~nav/handle-unmount~, which have\nsimilar signatures to normal event handlers:\n\n#+BEGIN_SRC clojure\n  (defmethod nav/handle-mount :home [state {:keys [location]}]\n    (-\u003e state\n        (oak/with-cmd [::http/request! {...}])))\n\n  (defmethod nav/handle-change :home [state {:keys [old-location new-location]}]\n    (-\u003e state\n        ...))\n\n  (defmethod nav/handle-unmount :home [state {:keys [location]}]\n    (-\u003e state\n        ;; tear down, if required\n        ...))\n#+END_SRC\n\nA view is considered to be re-mounted (from a nav point-of-view, even if the\ncomponents aren't necessarily re-mounted) if either the handler or the\nroute-params change - at which point, the old handler is un-mounted and the\nnew handler mounted. If the query-params or the history-state changes, only\n~handle-change~ will be called.\n\n* Feedback? Want to contribute?\n\nYes please! Please submit issues/PRs in the usual Github way. I'm also\ncontactable through Twitter, or email.\n\nIf you do want to contribute a larger feature, that's great - but\nplease let's discuss it before you spend a lot of time implementing\nit. If nothing else, I'll likely have thoughts, design ideas, or\nhelpful pointers :)\n\n* Thanks!\n\nThanks to [[https://github.com/olical][Oliver Caldwell]] and [[https://github.com/krisajenkins][Kris Jenkins]] who have, over the years, contributed\na awful lot to Oak, in the form of thoroughly fruitful discussions and debates!\n\n* LICENCE\n\nCopyright © 2018 James Henderson\n\nOak is distributed under the Eclipse Public License - either version 1.0 or (at\nyour option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjarohen%2Foak","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjarohen%2Foak","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjarohen%2Foak/lists"}