diff --git a/lib/hooks.ml b/lib/hooks.ml new file mode 100644 index 00000000..b8f1b4d6 --- /dev/null +++ b/lib/hooks.ml @@ -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 diff --git a/lib/hooks.mli b/lib/hooks.mli new file mode 100644 index 00000000..04c57486 --- /dev/null +++ b/lib/hooks.mli @@ -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 diff --git a/lib/react.ml b/lib/react.ml index 95ca3ef0..87bed258 100644 --- a/lib/react.ml +++ b/lib/react.ml @@ -1,4 +1,5 @@ include Core +module Hooks = Hooks module Dom = struct include Dom diff --git a/test/test_ml.ml b/test/test_ml.ml index fb1051fd..6e248be3 100644 --- a/test/test_ml.ml +++ b/test/test_ml.ml @@ -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 @@ -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 @@ -881,6 +1007,7 @@ let suite = "ocaml" >::: [ basic ; context + ; hooks ; use_effect ; use_callback ; use_state