Last active
August 7, 2019 17:53
-
-
Save donut/1ca82a78b8c0ba196db21d2d6a297054 to your computer and use it in GitHub Desktop.
An alternative to `React.useReducer` that supports middleware with side effects.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type ('action, 'state) middleware | |
= dispatch:('action -> unit) | |
-> 'state | |
-> 'action | |
-> [ `Rerun of 'action | `Next of 'action | `Stop of 'action ] | |
let apply_middleware middleware dispatch state action = | |
let rec apply action = function | |
| [] -> action | |
| hd :: tl -> | |
match hd ~dispatch state action with | |
| `Rerun a -> apply a middleware | |
| `Next a -> apply a tl | |
| `Stop a -> a | |
in | |
apply action middleware | |
let use_reducer ?(middleware=[]) reducer initial = | |
let open React in | |
(* Since the middleware may have side effects, they must be run within | |
[useEffect]. And since middleware can change the final action, the | |
reducer must run after the middleware, within the same [useEffect]. *) | |
(* Only use [useReducer] to figure out what action should be acted on. *) | |
let action_state, dispatch = useReducer (fun _ a -> a) None in | |
(* Since handling actions lives in an effect, we need to make sure the | |
effect runs whenever an action is dispatched, regardless of whether or | |
not it is the same action. The counter is used to tell the effect that | |
the state it depends on has changed even when the action is the same as | |
before. *) | |
let counter, set_counter = useState (fun () -> 0) in | |
let dispatch a = | |
let new_action = Some a in | |
if new_action <> action_state | |
then dispatch new_action | |
else set_counter ((+) 1) | |
in | |
let state, set_state = useState (fun () -> initial) in | |
(* An effect is used instead of just putting this all in a custom dispatch | |
function so that asynchronous effects wont change state if component was | |
updated or unmounted. This is accomplished by wrapping the [dispatch] | |
function with a check for whether or not the effect's cleanup function | |
has run. *) | |
let () = useEffect2 begin fun () -> | |
match action_state with | |
| None -> None | |
| Some action -> | |
let canceled = ref false in | |
let dispatch a = if !canceled then () else dispatch a in | |
let () = | |
action | |
|> apply_middleware middleware dispatch state | |
|> reducer state | |
|> (fun s -> set_state (fun s' -> if s <> s' then s else s')) | |
in | |
Some (fun () -> canceled := true) | |
end (action_state, counter) in | |
(state, dispatch) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(** [middleware] is a function that acts on the passed action, returning the | |
same or new action wrapped in a command. [`Next] will pass its action to | |
the next middleware if any. [`Rerun] will re-run all middleware with its | |
action. [`Stop] will stop any remaining middleware from running. *) | |
type ('action, 'state) middleware | |
= dispatch:('action -> unit) | |
-> 'state | |
-> 'action | |
-> [ `Next of 'action | `Rerun of 'action | `Stop of 'action ] | |
(** [use_reducer ?middleware reducer initial_state] is an alternative to | |
[React.useReducer] with support for middleware that has side effects. *) | |
val use_reducer | |
: ?middleware:('action, 'state) middleware list | |
-> ('state -> 'action -> 'state) | |
-> 'state | |
-> 'state * ('action -> unit) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment