/
UseEvent.purs
178 lines (168 loc) · 6.03 KB
/
UseEvent.purs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
module Halogen.Hooks.Extra.Hooks.UseEvent
( useEvent
, UseEvent
, UseEventApi
)
where
import Prelude
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Traversable (for_)
import Data.Tuple.Nested ((/\))
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Ref as Ref
import Halogen.Hooks (HookM, Hooked, UseRef, useRef)
import Halogen.Hooks as Hooks
newtype UseEvent m a hooks =
UseEvent
(UseRef
{ valueCB
:: Maybe ( ((HookM m (HookM m Unit)) -> HookM m Unit)
-> a -- pushed value
-> HookM m Unit -- code that gets run
)
, unsubscribeCB
:: Maybe (HookM m Unit)
}
hooks)
derive instance newtypeUseEvent :: Newtype (UseEvent m a hooks) _
-- | For proper usage, see the docs for `useEvent`.
type UseEventApi m a =
{ push :: a -> HookM m Unit
, setCallback
:: Maybe
( ((HookM m (HookM m Unit)) -> HookM m Unit)
-> a -- pushed value
-> HookM m Unit -- code that gets run
)
-> HookM m (HookM m Unit)
}
-- | Allows you to "push" events that occur inside a hook
-- | to a single handler outside of the hook. This allows the end user
-- | to use the full API returned by the hook when handling the event.
-- | Moreover, the end-user can set up resources on the first time
-- | the handler is run and unsubscribe when the finalizer is run.
-- |
-- | For example...
-- | ```
-- | -- let's say this is the end-user's Hook code
-- | onEvent <- useEvent
-- |
-- | -- Here, we'll inline the code for a hypothetical hook we found.
-- | -- This could be a hook provided by a library or something.
-- | { foo } <- Hooks.do
-- |
-- | -- somewhere in the hypothetical hook, an event occurs
-- | onEvent.push "user clicked foo"
-- |
-- | -- return the value of the hook provided by the library
-- | pure { foo: "foo" }
-- |
-- | Hooks.useLifecycleEffect do
-- | unsubscribe <- onEvent.setCallback $ Just \setupSubscription str -> do
-- | -- handle the event
-- | Hooks.raise ("Event occurred: " <> str)
-- |
-- | setupSubscription do
-- | -- Then, set up some resources in this code block
-- | -- that need to be cleaned up later
-- | liftEffect $ log $ "Setting up resources."
-- |
-- | pure do
-- | -- now define the code that will run when we call
-- | -- 'unsubscribe' later
-- | liftEffect $ log $ "Cleaning up resources."
-- |
-- | pure $ Just do
-- | -- unsubscribe to clean up resources
-- | unsubscribe
-- | ```
-- | If you don't need to unsubscribe, just ignore the first argument
-- | in the function passed to `onEvent`:
-- | ```
-- | state /\ stateId <- useState 0
-- |
-- | Hooks.captures { state } Hooks.useTickEffect do
-- | -- Notice two things here:
-- | -- 1. we're ignoring the 'unsubscribeCallback' argument
-- | -- by using the underscore (i.e. _).
-- | -- 2. we're ignoring the returned 'unsubscribe' code by using `void`.
-- | void $ onEvent \_ string -> do
-- | -- handle the event
-- | Hooks.raise ("Event occurred: " <> string)
-- |
-- | pure Nothing -- no need to unsubscribe here
-- | ```
-- |
-- | ## Beware Infinite Loops
-- |
-- | If you use this hook, it's possible for you to create an infinite loop.
-- | This will occur if a handler runs code that causes another event to
-- | be emitted. Consider this workflow:
-- |
-- | 1. Event A is emitted
-- | 2. During A's handler, Action X is called
-- | 3. During Action X's computation, Event A is emitted.
-- | 4. An infinite loop occurs (go to Step 2)
-- |
-- | Here's an example in code:
-- | ```
-- | library <- useLibrary
-- | useLifecycleEffect do
-- | library.onNewInteger \newInt -> do
-- | library.setIntValue (newInt + 1)
-- | ```
-- | Consider also cases where the chain is longer and
-- | some computations run only when certain conditions are true:
-- |
-- | 1. Event A is emitted
-- | 2. During A's handler, Action X is called
-- | 3. During Action X's computation, Event B is emitted.
-- | 4. During B's handler, Action Y is called
-- | 5. During Action Y's computation, Event C is emitted but only if State M is equal to 4.
-- | 6. During C's handler, Action Z is called
-- | 7. During Action Z's computation, Event A is emitted.
-- | 8. Infinite loop may occur (depends on State M)
useEvent :: forall m a hooks
. MonadEffect m
=> Hooked m hooks (UseEvent m a hooks)
(UseEventApi m a)
useEvent = Hooks.wrap Hooks.do
-- valueCB = the callback to run when a new event is pushed
-- unsubscribeCB = callback to run when unsubscribing
_ /\ ref <- useRef { valueCB: Nothing, unsubscribeCB: Nothing }
let
push :: a -> HookM m Unit
push value = do
mbCallback <- liftEffect $ map (_.valueCB) $ Ref.read ref
for_ mbCallback \callback -> do
callback setupUnsubscribeCallback value
setupUnsubscribeCallback :: (HookM m (HookM m Unit)) -> HookM m Unit
setupUnsubscribeCallback subscribeAndReturnUnsubscribeCallback = do
mbUnsubscribe <- liftEffect $ map (_.unsubscribeCB) $ Ref.read ref
case mbUnsubscribe of
Nothing -> do
unsubscribeCode <- subscribeAndReturnUnsubscribeCallback
liftEffect $ Ref.modify_ (_ { unsubscribeCB = Just unsubscribeCode}) ref
_ -> do
-- no need to store unsubscriber because
-- 1. it's already been stored
-- 2. no one has subscribed to this yet
pure unit
setCallback :: Maybe
( ((HookM m (HookM m Unit)) -> HookM m Unit)
-> a -- pushed value
-> HookM m Unit -- code that gets run
)
-> HookM m (HookM m Unit)
setCallback callback = do
liftEffect $ Ref.modify_ (_ { valueCB = callback }) ref
pure do
mbUnsubscribe <- liftEffect $ map (_.unsubscribeCB) $ Ref.read ref
case mbUnsubscribe of
Just unsubscribeCode -> do
unsubscribeCode
liftEffect $ Ref.modify_ (_ { unsubscribeCB = Nothing }) ref
_ -> do
pure unit
Hooks.pure { push, setCallback }