purescript-halogen-store
purescript-halogen-store copied to clipboard
Question: can the Store be used by multiple "apps"?
I have a question: can Halogen Store be used as a global store for multiple application/components? The examples all have a single root component with sub components rendered inside of it, and not using it with multiple root apps and I'm unclear on if this is supported skimming the code.
To give a more concrete example. I have 2 widgets on a server-rendered, non-SPA page, A
and B
. Both A
& B
want to subscribe to connectivity of the browser (e.g. are we online of offline?). A simple store would have type Store = { isOnline ∷ Boolean }
which would be subscribed by A
& B
to know if the browser has gone offline or come online so the event listeners are only set up once and everything is kept in sync. (But I have several similar global state values to share in this manner)
Do I need to a create a "root" application/component that has no render to achieve this and it handles the main Store values, or is there a way to initialize and and subscribe to a store for multiple entries? Or is this library the wrong thing for this approach and just use MonadAsk
, et. al.?
I haven't explored this as a possibility, and I haven't done this with global states elsewhere (for example, in Redux). However, since the StoreT
instance for MonadStore
is implemented just using Ref
s and Effect
-based functions, it should be possible to write something multiple apps can share:
https://github.com/thomashoneyman/purescript-halogen-store/blob/583430dd02da0e8af7636d05083c191fb3cd1f99/src/Halogen/Store/Monad.purs#L57-L83
The runStoreT
function currently takes one component:
https://github.com/thomashoneyman/purescript-halogen-store/blob/583430dd02da0e8af7636d05083c191fb3cd1f99/src/Halogen/Store/Monad.purs#L122-L134
but I believe you could split out the function that creates a HalogenStore
:
https://github.com/thomashoneyman/purescript-halogen-store/blob/583430dd02da0e8af7636d05083c191fb3cd1f99/src/Halogen/Store/Monad.purs#L130-L133
and then reuse the same value for every component that you hoist:
https://github.com/thomashoneyman/purescript-halogen-store/blob/583430dd02da0e8af7636d05083c191fb3cd1f99/src/Halogen/Store/Monad.purs#L134
So the resulting code might be something like
main :: Effect Unit
main = launchAff_ do
store <- liftEffect do
value <- Ref.new initialStore
{ emitter, listener } <- HS.create
pure { value, emitter, listener, reducer }
app1 <- hoist (\(StoreT m) -> runReaderT m hs) App1.component
app2 <- hoist (\(StoreT m) -> runReaderT m hs) App2.component
app3 <- hoist (\(StoreT m) -> runReaderT m hs) App3.component
...
Oh, thank you so much @thomashoneyman! I had started with a dummy root component, but it seemed like a lot of extra machinery to accomplish the task. Maybe at some point I can circle back and contribute an example (especially if you get in an .editorconfig
and .tidyrc.json
so I don't muscle-memory a unicode →
:wink:)
Here you go! https://github.com/thomashoneyman/purescript-halogen-store/blob/main/.tidyrc.json
Does this mean Apps 1-3 should be using the whole Store.connect selectState $ H.mkComponent { ... }
and the same as any other things, or do the need to set up like the App
components in ReduxTodo
? I've never used hoist
and there seems minimal documentation about it in the Halogen docs.
(Above example should be example, runReaderT m store
and not hs
).
They can just connect to the store normally. You’d only use the ReduxTodo approach if you wanted to make multiple stores within the main store.
Hoisting refers to taking a component that runs in some non-Aff monad (like StoreT) and transforming it to run in Aff instead. Ultimately Halogen only runs components in Aff.
Speaking of hoist
and confusion...
Aff.launchAff_ do
...
mbookShowingIO ← do
mdialogElem ← HA.selectElement (QuerySelector "#BookShowingDialog .Dialog-container")
case mdialogElem of
Nothing → pure Nothing
Just dialogElem → do
bookShowing ← H.hoist (\(StoreT m) → runReaderT m store) BookShowing.component
Just <$> runUI bookShowing unit dialogElem
That component:
component ∷
∀ output m.
MonadAff m ⇒
Store.MonadStore Store.Action Store.Store m ⇒
H.Component Query Input output m
component =
Store.connect selectState $ H.mkComponent { ... }
Error
60 bookShowing ← H.hoist (\(StoreT m) → runReaderT m store) BookShowing.component
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Could not match kind
Type
with kind
Type -> Type
while trying to match type Component Query Unit t2
with type t0
while checking that expression (hoist (\$3 ->
case $3 of
...
)
)
component
has type t0 t1
in value declaration main
where t0 is an unknown type
t1 is an unknown type
t2 is an unknown type
Can you make a reproducible example on Try PureScript that I could take a look at? Otherwise, I'd recommend giving a type annotation to each line because sometimes in do
blocks the errors can be a bit obscure.
hoist
isn't an effect ha. It did end up being imperative to help the compiler out with the types for the HalogenIO
with two different apps.
Aff.launchAff_ do
-- BookShowing component
(mbookShowingIO ∷ Maybe (H.HalogenIO BookShowing.Query Void Aff)) ← do
mdialogElem ← HA.selectElement (QuerySelector "#BookShowingDialog .Dialog-container")
case mdialogElem of
Nothing → pure Nothing
Just dialogElem → do
let cmpnt = H.hoist (\(StoreT m) → runReaderT m store) BookShowing.component
Just <$> runUI cmpnt unit dialogElem