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

subscribing to external events and cleaning them when component is removed #59

Open
srghma opened this issue Jan 30, 2021 · 7 comments
Open

Comments

@srghma
Copy link

srghma commented Jan 30, 2021

I am rewriting keyboard example with FRP.Event

srghma@1204045

and don't know how to bracket the widget to cleanup with stopListeningKeydownEvent

could someone help me


P.S.

in future I'll have to also uncomment the toggleEvents button and NOT ONLY subscribe on Initialize event, but also being able to unsubscribe/resubscribe with this toggle button

in halogen this is similar to this example https://github.com/purescript-halogen/purescript-halogen/blob/master/examples/keyboard-input/src/Main.purs AND would be implemented with

which allows user to finalize manually, BUT also finalizes when component is destroyed

  • storing Maybe H.SubscriptionId in State

@Mateiadrielrafael, you are writing Halogen makes subscribing to external events (eg: window resizing) a lot easier

do You know how to accomplish this? could you help, please

@srghma srghma changed the title subscribing to external events and cleaning them when parent node is removed subscribing to external events and cleaning them when component is removed Jan 30, 2021
@Mateiadrielrafael
Copy link

Mateiadrielrafael commented Jan 30, 2021

@srghma

I started by creating a helper for generating a bus:

-- | Create an async bus for dom events
eventBus :: EventType -> Boolean -> EventTarget -> Effect (Bus.BusR Event)
eventBus eventType capture target = do
 Tuple readBus writeBus <- Bus.make <#> Bus.split
 listener <- eventListener \event -> launchAff_ $ Bus.write event writeBus
 addEventListener eventType listener capture target  
 pure readBus

then my I used a wrapper component which looks like this:

editor :: forall a. Widget HTML a
editor = do
  window <- liftEffect HTML.window
  -- create the event bus here
  resizeBus <- liftEffect $ eventBus (EventType "resize") true (Window.toEventTarget window)
  -- put the bus into the state, so the inner component can listen to events
  tea (defaultState resizeBus) editor' $ handleAction >>> runEditorM -- This uses a custom version of `tea` I made
  -- unsubscribing logic would probably have to sit here, haven't implemented that yet.
  where
  defaultState :: Bus.BusR Event -> EditorState
  defaultState =
    { ... -- more state here
    , resizeBus: _
    }

Then the inner component is trivial:

editor' :: EditorState -> Widget HTML EditorAction
editor' state = do
  -- This will either return a dom event from within htmlSutff, or a resize event, whichever finishes first.
  htmlStuff -- The rendering stuff is put here
    <|> resizeEvent
  where
  resizeEvent = liftAff $ Bus.read state.resizeBus $> Resize

This feels like a hack, and wouldn't handle unsubscribing too well (eg: a change of route won't trigger it), but it's the first thing I could come up with and works fine for now

@srghma
Copy link
Author

srghma commented Jan 30, 2021

unsubscribing logic would probably have to sit here, haven't implemented that yet.

doesnt tea makes never ending widget? so unsubscribing logic wont ever be reached, not even when editor :: forall a. Widget HTML a vdom node is destroyed

wouldn't handle unsubscribing too well (eg: a change of route won't trigger it)

yes

@Mateiadrielrafael
Copy link

yep, I think that's the biggest problem rn

@ajbarber
Copy link
Contributor

ajbarber commented Jun 29, 2021

I am using routing to drive this sort of set up/tear down logic when I need it in my Concur app.

So listen to routes using purescript-routing and when they are hit or navigated away from, one can hook up componentDidMount, componentWillUnmount or in Hooks parlance useEffect type logic. It's not precisely the same as listening to vdom events of course. Note that this is not a new idea. I know of at least one other state management framework in JS which uses this approach, that being mobx-router.

@ajbarber
Copy link
Contributor

ajbarber commented Jul 9, 2021

Here is some more info on my approach. I have quite a simple API, not unlike purescript-react-hooks useEffectOnce. The major difference of course being route changes are the trigger.

It looks like this:

myWidget :: forall a b. StateT (MyState b) (Widget HTML) a
myWidget = do
  void $ runOnce (log "subscribe home") (log "cancel home")
  D.div' [ D.h1' [D.text " You are on the Homepage"]]
  ...

runOnce will do what it says, run on the first time myWidget is entered, when a user navigates to its route. It takes a "subscriber" effect for the first argument, and a "canceler" effect for the second argument. The subscriber runs once on the first execution of the Widget, on subsequent recursions, it will not run. This ability to turn itself off is reflected in its type signature, returning Just a when the effect ran or Nothing if we are on a subsequent render:

runOnce ::
  forall a b.
  Effect a ->
  Effect Unit ->
  StateT (MyState b) (Widget HTML) (Maybe a)

And I haven't forgotten about cleaning events up. When you are done with myWidget and route off somewhere else, the canceler is run. So "cancel home" gets printed in the console. So note here that you can run a canceler on a never ending widget.

It's a nice API, which has sufficed for my rather large commercial web app. I think it could be expanded on quite a bit.

To see it in action I have put together an example. You can just drop this into the examples folder of puresript-concur-react.

https://gist.github.com/ajbarber/2fd3bc86be0e8cd1acc9e3602bf5be82

@ajbarber
Copy link
Contributor

ajbarber commented Jul 12, 2021

I looked into this a little further. So the above works okay, I suppose, if you have routing set up for your application. But perhaps you are integrating concur into an existing React JS application where you don't have control of routing, or perhaps you simply would prefer to leverage the React lifecycle events.

In this vein I had a little look at it and providing an API, something very basic like

-- Widget v a is a never ending widget, as there is no concept of a
-- component which returns something in React!
runAsComponent :: forall v a. Effect Unit -> Effect Unit -> Widget v a -> Widget v a

should be really quite simple.

As a first draft API, really the first thing that came into my head, you would provide onMount and willUnmount effects, and a never-ending widget. runAsComponent then would return a never-ending widget with the lifecycle events hooked. Is this something that anyone would find useful?

The important thing is such an API would only make sense to be used on never ending widgets. Which I believe is what I believe the OP was trying to achieve. I haven't looked at the OP's provided code or links, but as a simple example, I just scratched around with hooking an unMount effect to the greeting widget which prints "HELLO!", in the Login.purs widget, located in the examples directory. When you log out, this greeting is removed from the vdom, and the unMount effect is run, as expected. Something like:

loginWidget :: forall a. Widget HTML a
loginWidget = runTask do
  u <- currentUser
  runAsComponent (log "hello") (log "goodbye") (D.div'[ D.text "HELLO!" ])

@ajbarber
Copy link
Contributor

ajbarber commented Jul 12, 2021

Pushed something to react-component-lifecycle-api-idea .

To see a minimal example:

Make the below change to Login.purs in examples:

-  D.div'
-    [ D.text "HELLO!"
-    ]
+  runAsComponent (log "hello") (log "goodbye") (D.div'[ D.text "HELLO!" ])

This hooks log "hello" to componentDidMount, and log "goodbye" to componentWillUnmount

you'll need this import too

+import Concur.React (runAsComponent)

then watch the console as you log out, as described above.

Ps: to incorporate this branch in your project you would need something like

 in  upstream
+  with concur-react.version = "component-lifecycle-api-idea"
+  with concur-react.repo = "https://github.com/ajbarber/purescript-concur-react.git"

in your packages.dhall

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

3 participants