As said previously, these files are very similar from existing abstractions such as Vuex, Redux, Flux, etc. Basically the overall structure can be comprehend as a global Record RootStore
containing every existing stores of the project. Individual stores module are structured with implicit implementation:
-
determined filepath — filename representation: (note: automation still on work in progress) for example by creating a
Hello.Stores.Foo.Bar
store module, we create arootStore { "foo/bar": store | rootStore }
, ie. a newRow
within theRootStore
type - determined variable and type names: each module has to export a certain amount of attended variables
-
Store
&State
types: refering to bothRecord
: a "boxed" Toestand representation, and its vanilla values -
state
variable: thunk hydrating default values of the store module
module Hello.Stores.Public.Authentication
-- mandatory exports
( state
, State
, Store
-- custom exports
, login
, LoginData
) where
import Prelude
import Data.Either (Either)
import Effect.Aff (Aff, Error, Milliseconds(..), attempt, delay)
import Effect.Class (liftEffect)
import Toestand as T
type Store =
( onPending :: T.Box (Boolean)
)
type State =
( onPending :: Boolean
)
state :: Unit -> Record State
state = \_ ->
{ onPending: false
}
type LoginData =
( email :: String
, password :: String
)
-- Below is an example of "actions" (from the predictable state container
-- jargon). They are asynchroneous computations made on the current store
login :: Record Store -> Record LoginData -> Aff (Either Error Unit)
login { onPending } _ = do
liftEffect $ T.write_ true onPending
result <- attempt $ simulateAPIRequest
liftEffect $ T.write_ false onPending
pure $ result
simulateAPIRequest :: Aff Unit
simulateAPIRequest = delay $ Milliseconds 2000.0
Actions and Mutations
In the above module, we have an example of logic written within the store. It is a common thing with predictable state container library to provide abstracted behaviors of such kinds:
-
"mutations": ~ synchronous computations, mostly individual setters for each stored field
- as we rely on Toestand, the most valuable solution for this kind of computations is to stick with Toestand API
- so every setter calls can be executed anywhere in the code, just by reusing
T.write
,T.modify
, etc.
-
"actions": ~ asynchronous computations, two differents types
-
scoped: as illustrated with the
login
method just above, take theRecord Store
as an argument and return anAff *
-
unscoped: take the
Record RootStore
in first argument,Record Store
of the module in second, return anAff *
-
scoped: as illustrated with the
Heads up. These definitions regarding "actions" are pragmatic and opinionated. There is no strict implementation to follow here, we just think that this would be the best abstraction possible.