-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathAff.purs
More file actions
270 lines (250 loc) · 8.97 KB
/
Aff.purs
File metadata and controls
270 lines (250 loc) · 8.97 KB
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
module React.Basic.Hooks.Aff
( useAff
, useSteppingAff
, UseAff(..)
, useAffReducer
, AffReducer
, mkAffReducer
, runAffReducer
, noEffects
, UseAffReducer(..)
, useAffActionState
, useAffActionStateWithPermalink
, UseAffActionState(..)
) where
import Prelude
import Control.Promise (Promise, fromAff)
import Data.Either (Either(..))
import Data.Foldable (for_)
import Data.Function.Uncurried (Fn2, Fn3, mkFn2, mkFn3, runFn2)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Nullable (Nullable, toNullable)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Aff (Aff, Error, error, killFiber, launchAff, launchAff_, throwError, try)
import Effect.Class (liftEffect)
import Effect.Uncurried (EffectFn3, EffectFn4, runEffectFn3, runEffectFn4)
import Effect.Unsafe (unsafePerformEffect)
import React.Basic.Hooks (type (&), type (/\), Hook, Reducer, UnsafeReference(..), UseActionState, UseEffect, UseMemo, UseReducer, UseState, coerceHook, mkReducer, unsafeHook, unsafeRenderEffect, useEffect, useMemo, useReducer, useState, (/\))
import React.Basic.Hooks as React
--| `useAff` is used for asynchronous effects or `Aff`. The asynchronous effect
--| is re-run whenever the deps change. If another `Aff` runs when the deps
--| change before the previous async resolves, it will cancel the previous
--| in-flight effect.
--|
--| *Note: This hook requires parent components to handle error states! Don't
--| forget to implement a React error boundary or avoid `Aff` errors entirely
--| by incorporating them into your result type!*
useAff ::
forall deps a.
Eq deps =>
deps ->
Aff a ->
Hook (UseAff deps a) (Maybe a)
useAff = useAff' (const Nothing)
--| A variant of `useAff` where the asynchronous effect's result is preserved up
--| until the next run of the asynchronous effect _completes_.
--|
--| Contrast this with `useAff`, where the asynchronous effect's result is
--| preserved only up until the next run of the asynchronous effect _starts_.
useSteppingAff ::
forall deps a.
Eq deps =>
deps ->
Aff a ->
Hook (UseAff deps a) (Maybe a)
useSteppingAff = useAff' identity
useAff' ::
forall deps a.
Eq deps =>
(Maybe (Either Error a) -> Maybe (Either Error a)) ->
deps ->
Aff a ->
Hook (UseAff deps a) (Maybe a)
useAff' initUpdater deps aff =
coerceHook React.do
result /\ setResult <- useState Nothing
useEffect deps do
setResult initUpdater
fiber <-
launchAff do
r <- try aff
liftEffect do
setResult \_ -> Just r
pure do
launchAff_ do
killFiber (error "Stale request cancelled") fiber
unsafeRenderEffect case result of
Just (Left err) -> throwError err
Just (Right a) -> pure (Just a)
Nothing -> pure Nothing
newtype UseAff deps a hooks
= UseAff
( hooks
& UseState (Maybe (Either Error a))
& UseEffect deps
)
derive instance ntUseAff :: Newtype (UseAff deps a hooks) _
--| Provide an initial state and a reducer function. This is a more powerful
--| version of `useReducer`, where a state change can additionally queue
--| asynchronous operations. The results of those operations must be mapped
--| into the reducer's `action` type. This is essentially the Elm architecture.
--|
--| Generally, I recommend `useAff` paired with tools like `useResetToken` over
--| `useAffReducer` as there are many ways `useAffReducer` can result in race
--| conditions. `useAff` with proper dependency management will handle previous
--| request cancellation and ensure your `Aff` result is always in sync with
--| the provided `deps`, for example. To accomplish the same thing with
--| `useAffReducer` would require tracking `Fiber`s manually in your state
--| somehow.. :c
--|
--| That said, `useAffReducer` can still be helpful when converting from the
--| current `React.Basic` (non-hooks) API or for those used to Elm.
--|
--| *Note: Aff failures are thrown. If you need to capture an error state, be
--| sure to capture it in your action type!*
useAffReducer ::
forall state action.
state ->
AffReducer state action ->
Hook (UseAffReducer state action) (state /\ (action -> Effect Unit))
useAffReducer initialState affReducer =
coerceHook React.do
reducer' <-
useMemo (UnsafeReference affReducer) \_ ->
unsafePerformEffect do
mkReducer (\{ state } -> runAffReducer affReducer state)
{ state, effects } /\ dispatch <-
useReducer { state: initialState, effects: [] } reducer'
useEffect (UnsafeReference effects) do
for_ effects \aff ->
launchAff_ do
actions <- aff
liftEffect do for_ actions dispatch
mempty
pure (state /\ dispatch)
newtype UseAffReducer state action hooks
= UseAffReducer
( hooks
& UseMemo (UnsafeReference (AffReducer state action))
( Reducer
{ effects :: Array (Aff (Array action))
, state :: state
}
action
)
& UseReducer { state :: state, effects :: Array (Aff (Array action)) } action
& UseEffect (UnsafeReference (Array (Aff (Array action))))
)
derive instance ntUseAffReducer :: Newtype (UseAffReducer state action hooks) _
newtype AffReducer state action
= AffReducer
( Fn2
state
action
{ state :: state, effects :: Array (Aff (Array action)) }
)
mkAffReducer ::
forall state action.
(state -> action -> { state :: state, effects :: Array (Aff (Array action)) }) ->
Effect (AffReducer state action)
mkAffReducer = pure <<< AffReducer <<< mkFn2
--| Run a wrapped `Reducer` function as a normal function (like `runFn2`).
--| Useful for testing, simulating actions, or building more complicated
--| hooks on top of `useReducer`
runAffReducer ::
forall state action.
AffReducer state action ->
state ->
action ->
{ state :: state, effects :: Array (Aff (Array action)) }
runAffReducer (AffReducer reducer) = runFn2 reducer
noEffects ::
forall state action.
state ->
{ state :: state
, effects :: Array (Aff (Array action))
}
noEffects state = { state, effects: [] }
--| Aff version of `useActionState` for managing async form actions.
--| The action function receives the previous state and form data, and returns
--| an `Aff` that resolves to the new state. React will automatically handle
--| the pending state whilst the Aff is running.
--|
--| *Note: Aff failures are thrown as React errors. If you need to capture an
--| error state, incorporate it into your state type (e.g., `Either Error MyState`)!*
--|
--| ```purs
--| state /\ formAction /\ isPending <- useAffActionState initialState \prevState formData -> do
--| result <- submitToServer formData
--| pure (processResult prevState result)
--|
--| pure $ R.button
--| { disabled: isPending
--| , onClick: handler_ (formAction myFormData)
--| }
--| ```
useAffActionState ::
forall state formData.
state ->
(state -> formData -> Aff state) ->
Hook (UseAffActionState state formData) (state /\ ((formData -> Effect Unit) /\ Boolean))
useAffActionState initialState affFn =
coerceHook React.do
unsafeHook do
let affFnAsPromise prevState formData = fromAff (affFn prevState formData)
runEffectFn3 useAffActionState_
mkTuple3
affFnAsPromise
initialState
where
mkTuple3 :: forall a b c. Fn3 a b c (a /\ (b /\ c))
mkTuple3 = mkFn3 \a b c -> Tuple a (Tuple b c)
--| Like `useAffActionState` but with a permalink for progressive enhancement.
--| The form will submit to this URL if JavaScript is disabled.
--|
--| ```purs
--| state /\ formAction /\ isPending <- useAffActionStateWithPermalink initialState affFn "/api/submit"
--|
--| pure $ R.form
--| { action: formAction
--| , children: [ ... ]
--| }
--| ```
useAffActionStateWithPermalink ::
forall state formData.
state ->
(state -> formData -> Aff state) ->
String ->
Hook (UseAffActionState state formData) (state /\ ((formData -> Effect Unit) /\ Boolean))
useAffActionStateWithPermalink initialState affFn permalink =
coerceHook React.do
unsafeHook do
let affFnAsPromise prevState formData = fromAff (affFn prevState formData)
runEffectFn4 useAffActionStateWithPermalink_
mkTuple3
affFnAsPromise
initialState
permalink
where
mkTuple3 :: forall a b c. Fn3 a b c (a /\ (b /\ c))
mkTuple3 = mkFn3 \a b c -> Tuple a (Tuple b c)
foreign import useAffActionState_ ::
forall state formData.
EffectFn3
(forall a b c. Fn3 a b c (a /\ (b /\ c)))
(state -> formData -> Effect (Promise state))
state
(state /\ ((formData -> Effect Unit) /\ Boolean))
foreign import useAffActionStateWithPermalink_ ::
forall state formData.
EffectFn4
(forall a b c. Fn3 a b c (a /\ (b /\ c)))
(state -> formData -> Effect (Promise state))
state
String
(state /\ ((formData -> Effect Unit) /\ Boolean))
newtype UseAffActionState state formData hooks
= UseAffActionState (hooks & UseActionState state formData)
derive instance ntUseAffActionState :: Newtype (UseAffActionState state formData hooks) _