Skip to content

Commit

Permalink
Merge pull request #154 from glennsl/feat/ml-hooks
Browse files Browse the repository at this point in the history
Hooks using OCaml idioms and equality semantics
  • Loading branch information
jchavarri authored Mar 29, 2022
2 parents 532c76b + c360776 commit b5f3b7b
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 0 deletions.
60 changes: 60 additions & 0 deletions lib/hooks.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
let use_ref initial =
let state, _ = Core.use_state (fun () -> ref initial) in
state

let use_ref_lazy init =
let state, _ = Core.use_state (fun () -> ref (init ())) in
state

let use_state initial = Core.use_state (fun () -> initial)
let use_state_lazy init = Core.use_state init
let use_reducer = Core.use_reducer

let use_resource ~on:deps ?(equal = ( = )) ?before_render ~release acquire =
let last_deps = use_ref deps in
let resource = use_ref None in

let release () = Option.iter release !resource in
let acquire () = resource := Some (acquire ()) in

let init () =
acquire ();
Some release
in
let update () =
if not (equal deps !last_deps) then begin
last_deps := deps;
release ();
acquire ()
end;
None
in

Core.use_effect_once ?before_render init;
Core.use_effect_always ?before_render update

let use_effect ~on ?equal ?before_render ?(cleanup = fun () -> ()) f =
use_resource ?before_render ~on ?equal ~release:cleanup f
let use_effect_once ?before_render ?cleanup f =
Core.use_effect_once ?before_render (fun () ->
f ();
cleanup)
let use_effect_always ?before_render ?cleanup f =
Core.use_effect_always ?before_render (fun () ->
f ();
cleanup)
let use_memo ~on:deps ?(equal = ( = )) f =
let last_deps = use_ref deps in
let value = use_ref_lazy (fun () -> f ()) in
if not (equal deps !last_deps) then begin
last_deps := deps;
value := f ()
end;
!value
let use_context = Core.use_context
36 changes: 36 additions & 0 deletions lib/hooks.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
val use_ref : 'value -> 'value ref
val use_ref_lazy : (unit -> 'value) -> 'value ref
val use_state : 'state -> 'state * (('state -> 'state) -> unit)
val use_state_lazy : (unit -> 'state) -> 'state * (('state -> 'state) -> unit)

val use_reducer :
init:(unit -> 'state)
-> ('state -> 'action -> 'state)
-> 'state * ('action -> unit)

val use_effect :
on:'deps
-> ?equal:('deps -> 'deps -> bool)
-> ?before_render:bool
-> ?cleanup:(unit -> unit)
-> (unit -> unit)
-> unit

val use_effect_once :
?before_render:bool -> ?cleanup:(unit -> unit) -> (unit -> unit) -> unit

val use_effect_always :
?before_render:bool -> ?cleanup:(unit -> unit) -> (unit -> unit) -> unit

val use_resource :
on:'deps
-> ?equal:('deps -> 'deps -> bool)
-> ?before_render:bool
-> release:('resource -> unit)
-> (unit -> 'resource)
-> unit

val use_memo :
on:'deps -> ?equal:('deps -> 'deps -> bool) -> (unit -> 'value) -> 'value

val use_context : 'value Core.Context.t -> 'value
1 change: 1 addition & 0 deletions lib/react.ml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
include Core
module Hooks = Hooks

module Dom = struct
include Dom
Expand Down
127 changes: 127 additions & 0 deletions test/test_ml.ml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,121 @@ let testContext () =
(Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "bar")))

let test_hooks_use_ref () =
let module C = struct
let%component make ~cb () =
let myRef = React.Hooks.use_ref 1 in
React.use_effect_once (fun () ->
myRef := !myRef + 1;
cb myRef;
None);
div [||] []
end in
withContainer (fun c ->
let myRef = ref None in
let cb reactRef = myRef := Some reactRef in
act (fun () -> React.Dom.render (C.make ~cb ()) (Html.element c));
assert_equal (myRef.contents |> Option.map (fun item -> !item)) (Some 2))

let test_hooks_use_effect () =
let count = ref 0 in
let module C = struct
let%component make ~a ~b =
React.Hooks.use_effect ~on:(a, b) (fun () -> incr count);
div [||] []
end in
withContainer (fun c ->
act (fun () -> React.Dom.render (C.make ~a:1 ~b:2 ()) (Html.element c));
act (fun () -> React.Dom.render (C.make ~a:1 ~b:2 ()) (Html.element c));
act (fun () -> React.Dom.render (C.make ~a:2 ~b:3 ()) (Html.element c));
assert_equal !count 2)

let test_hooks_use_resource_release () =
let acquired = ref [] in
let module C = struct
let%component make ~a ~b =
React.Hooks.use_resource ~on:(a, b)
~release:(fun resource ->
if resource = List.hd !acquired then acquired := List.tl !acquired)
(fun () ->
acquired := (a, b) :: !acquired;
(a, b));
div [||] []
end in
withContainer (fun c ->
act (fun () -> React.Dom.render (C.make ~a:1 ~b:2 ()) (Html.element c));
act (fun () -> React.Dom.render (C.make ~a:1 ~b:2 ()) (Html.element c));
act (fun () -> React.Dom.render (C.make ~a:2 ~b:3 ()) (Html.element c));
act (fun () -> React.Dom.render (div [||] []) (Html.element c));
assert_equal !acquired [])

let test_hooks_use_effect_custom_equal () =
let count = ref 0 in
let module C = struct
let%component make ~a ~b =
let equal a b = fst a = fst b in
React.Hooks.use_effect ~equal ~on:(a, b) (fun () -> incr count);
div [||] []
end in
withContainer (fun c ->
act (fun () -> React.Dom.render (C.make ~a:1 ~b:2 ()) (Html.element c));
act (fun () -> React.Dom.render (C.make ~a:1 ~b:3 ()) (Html.element c));
act (fun () -> React.Dom.render (C.make ~a:2 ~b:3 ()) (Html.element c));
assert_equal !count 2)

let test_hooks_use_memo () =
let count = ref 0 in
let module UseMemo = struct
let%component make ~a =
let result =
React.Hooks.use_memo ~on:a (fun () ->
incr count;
a ^ Int.to_string !count)
in
div [||] [ string result ]
end in
withContainer (fun c ->
act (fun () ->
React.Dom.render (UseMemo.make ~a:"foo" ()) (Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "foo1"));

act (fun () ->
React.Dom.render (UseMemo.make ~a:"foo" ()) (Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "foo1"));

act (fun () ->
React.Dom.render (UseMemo.make ~a:"bar" ()) (Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "bar2"));

act (fun () ->
React.Dom.render (UseMemo.make ~a:"foo" ()) (Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "foo3")))

let test_hooks_use_memo_custom_equal () =
let count = ref 0 in
let module UseMemo = struct
let%component make ~a ~b =
let equal a b = fst a = fst b in
let result =
React.Hooks.use_memo ~on:(a, b) ~equal (fun () ->
incr count;
a ^ b ^ Int.to_string !count)
in
div [||] [ string result ]
end in
withContainer (fun c ->
act (fun () ->
React.Dom.render (UseMemo.make ~a:"foo" ~b:"x" ()) (Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "foox1"));

act (fun () ->
React.Dom.render (UseMemo.make ~a:"foo" ~b:"y" ()) (Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "foox1"));

act (fun () ->
React.Dom.render (UseMemo.make ~a:"bar" ~b:"y" ()) (Html.element c));
assert_equal c##.textContent (Js.Opt.return (Js.string "bary2")))

let test_use_effect_always () =
let count = ref 0 in
let module C = struct
Expand Down Expand Up @@ -803,6 +918,17 @@ let basic =

let context = "context" >::: [ "testContext" >:: testContext ]

let hooks =
"hooks"
>::: [ "use_ref" >::: [ "basic" >:: test_hooks_use_ref ]
; "use_effect" >::: [ "basic" >:: test_hooks_use_effect ]
; "use_effect"
>::: [ "custom equal" >:: test_hooks_use_effect_custom_equal ]
; "use_resource" >::: [ "release" >:: test_hooks_use_resource_release ]
; "use_memo" >::: [ "basic" >:: test_hooks_use_memo ]
; "use_memo" >::: [ "custom equal" >:: test_hooks_use_memo_custom_equal ]
]

let use_effect =
"use_effect"
>::: [ "use_effect_always" >:: test_use_effect_always
Expand Down Expand Up @@ -881,6 +1007,7 @@ let suite =
"ocaml"
>::: [ basic
; context
; hooks
; use_effect
; use_callback
; use_state
Expand Down

0 comments on commit b5f3b7b

Please sign in to comment.