purescript-halogen-store icon indicating copy to clipboard operation
purescript-halogen-store copied to clipboard

Question: can the Store be used by multiple "apps"?

Open toastal opened this issue 2 years ago • 8 comments

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.?

toastal avatar Aug 06 '21 07:08 toastal

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 Refs 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
  
  ...

thomashoneyman avatar Aug 11 '21 13:08 thomashoneyman

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:)

toastal avatar Aug 11 '21 13:08 toastal

Here you go! https://github.com/thomashoneyman/purescript-halogen-store/blob/main/.tidyrc.json

thomashoneyman avatar Aug 11 '21 14:08 thomashoneyman

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).

toastal avatar Aug 11 '21 14:08 toastal

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.

thomashoneyman avatar Aug 11 '21 14:08 thomashoneyman

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

toastal avatar Aug 11 '21 16:08 toastal

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.

thomashoneyman avatar Aug 11 '21 16:08 thomashoneyman

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

toastal avatar Aug 13 '21 10:08 toastal