A ClojureScript microlib for state management in a Helix-based React app
deepstate provides simple hooks-based state management primitives which probably aren't very performant as the single source of truth in a large app (there is nothing like Reagent's reactions'), but are simple, flexible, and straightforward to use in an async world.
(require '[deepstate.action :as a])
(require '[deepstate.action.async :as a.a])
(require '[deepstate.action.axios :as a.ax]')
deepstate reduces a stream of events (called action
s) onto a state value.
There are a few core concepts:
action-context
- a React context for some deepstate state. Hooks need to
be passed such a contextaction-context
to a component treestate
value and a dispatch
functiondispatch
- a fn returned by the [[deepstate.action/use-action]] hook which
sends an action to be handledA quite simple example, showing a synchronous state-only action and an asynchronous action. Clicks will result in consistent data however they are interleaved:
(a/def-state-action ::inc-counter
[state _action]
(update state ::counter inc))
(a.a/def-async-action ::inc-delay
[state
{data ::a/data
:as _async-action-state}
_action]
;; a promise of the action data
(promesa.core/delay 2000 5)
;; effects which can use the destructured action data
or other state
{::a/state (update state ::counter + data)})
(def action-ctx (a/create-action-context))
(defnc App
[]
(let [[counter dispatch] (a/use-action action-ctx [::counter])]
(d/div
(d/p "Counter: " counter)
(d/button {:on-click (fn [_] (dispatch ::inc-counter))} "inc")
(d/button {:on-click (fn [_] (dispatch ::inc-delay))} "+5 delay"))))
Another example showing how the result of an async action can be destructured to conditionally create effects:
(a.ax/def-axios-action ::fetch-apod
[state
{status ::a/status
:as async-action-state}
action]
;; a promise of the action data
(axios/get "https://api.nasa.gov/planetary/apod\?api_key\=DEMO_KEY")
;; only create a navigate effect when successful
(when (= ::a/success status)
{::a/navigate "/show-pic"}))
(def action-ctx (a/create-action-context))
(defnc App
[]
(let [[{status ::a/status
result ::a/result
:as apod}
dispatch] (a/use-action action-ctx [::fetch-apod])]
(d/div
(d/p "Status:" (str status))
(d/p "APOD:" (pr-str result))
(d/button {:on-click (fn [_] (dispatch ::fetch-apod))} "Fetch!"))))
An action-context
is created with a/create-action-context
, and
passed through a component tree by an a/action-context-provider
element.
Components then consume and update state through the a/use-action
hook, which
returns [state dispatch]
- a state value and a dispatch fn. The
dispatch
fn is called with an action
map, which will be handled to
generate effects.
Actions are maps which describe an operation to change state (or perform
some other effect). They have
an ::a/action
key which selects a handler, and may have any other keys
the particular handler requires.
An action is dispatch
ed causing a handler to be invoked according
to the ::a/action
key in the action. The handler
returns a function of state
, and when that is invoked it returns
a map of (all optional) action-effects
(returning no effects is
not very useful, but perfectly fine). The available effects are:
::a/state
- a new state value::a/navigate
- a url to navigate to::a/dispatch
- an action-map
| [action-map
] to be dispatched::a/later
- a promise of a fn (fn [state] ...)
-> action-effects
It is possible to define an action handler directly, with
(defmethod a/handle <key> [action] (fn [state] ...))
, but it's
easier to use one of the sugar macros, which allow for some
destructuring:
action-effects
There are currently 4 effects available:
::a/state
- a new state value::a/navigate
- a url to navigate to::a/dispatch
- an action-map
| [action-map
] to be dispatched::a/later
- a promise of a fn (fn [state] ...)
-> action-effects
to provide more effects laterDefines a generic action handler. It takes
::a/action
key used in an action mapstate
and action-map
(a/def-action ::change-query
[state
{q :q
:as _action}]
{::a/state (assoc state ::query q)
::a/navigate "/show-state"})
Defines an action handler which only modifies state - the body form evaluates
to the updated state (i.e. not an action-effects
map)
(a/def-state-action ::change-query
[state
{q :q
:as _action}]
(assoc state ::query q))
Defines a promise-based async action handler. The action is specified as
a form returning a promise of the result. The global state
, the
async-action-state
and the action
map are all available for
destructuring
(a.a/def-async-action ::run-query
[__state
{status ::a/status
{id :id} ::a/data
:as __action-state}
{q :q
:as __action}]
(run-query action)
(when (= ::a/success status)
{::a/navigate (str "/item/" id)}))
This def-async-action
will assoc an async-action-state
map in the
global state
at path [::run-query]
, with the shape
{::a/status ::a/inflight|::a/success|::a/error
::a/data ...
::a/error ...}
Exactly like [[deepstate.action.async/def-async-action]], but the
action-data-promise
is expected to be an axios promise, and the response
or error will be parsed into the async-action-state
Copyright © 2023 mccraigmccraig of the clan mccraig
Distributed under the MIT License.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close