Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
toastal opened this issue Aug 6, 2021 · 8 comments
Open

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

toastal opened this issue Aug 6, 2021 · 8 comments

Comments

@toastal
Copy link
Contributor

toastal commented Aug 6, 2021

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

@thomashoneyman
Copy link
Owner

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:

instance monadStoreStoreT :: MonadAff m => MonadStore a s (StoreT a s m) where
getStore = StoreT do
store <- ask
liftEffect do
Ref.read store.value
updateStore action = StoreT do
store <- ask
liftEffect do
current <- Ref.read store.value
let newStore = store.reducer current action
Ref.write newStore store.value
HS.notify store.listener newStore
emitSelected (Selector selector) = StoreT do
store <- ask
liftEffect do
init <- Ref.read store.value
prevRef <- Ref.new (selector.select init)
pure $ filterEmitter store.emitter \new -> do
prevDerived <- Ref.read prevRef
let newDerived = selector.select new
if selector.eq prevDerived newDerived then
pure Nothing
else do
liftEffect $ Ref.write newDerived prevRef
pure (Just newDerived)

The runStoreT function currently takes one component:

runStoreT
:: forall a s q i o m
. Monad m
=> s
-> (s -> a -> s)
-> H.Component q i o (StoreT a s m)
-> Aff (H.Component q i o m)
runStoreT initialStore reducer component = do
hs <- liftEffect do
value <- Ref.new initialStore
{ emitter, listener } <- HS.create
pure { value, emitter, listener, reducer }
pure $ hoist (\(StoreT m) -> runReaderT m hs) component

but I believe you could split out the function that creates a HalogenStore:

hs <- liftEffect do
value <- Ref.new initialStore
{ emitter, listener } <- HS.create
pure { value, emitter, listener, reducer }

and then reuse the same value for every component that you hoist:

pure $ hoist (\(StoreT m) -> runReaderT m hs) component

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

@toastal
Copy link
Contributor Author

toastal commented Aug 11, 2021

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

@thomashoneyman
Copy link
Owner

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

@toastal
Copy link
Contributor Author

toastal commented Aug 11, 2021

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

@thomashoneyman
Copy link
Owner

thomashoneyman commented Aug 11, 2021

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.

@toastal
Copy link
Contributor Author

toastal commented Aug 11, 2021

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

@thomashoneyman
Copy link
Owner

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.

@toastal
Copy link
Contributor Author

toastal commented Aug 13, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants