Skip to content

Instantly share code, notes, and snippets.

@donut
Last active August 7, 2019 17:53
Show Gist options
  • Save donut/1ca82a78b8c0ba196db21d2d6a297054 to your computer and use it in GitHub Desktop.
Save donut/1ca82a78b8c0ba196db21d2d6a297054 to your computer and use it in GitHub Desktop.
An alternative to `React.useReducer` that supports middleware with side effects.
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)
(** [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