Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hooks using OCaml idioms and equality semantics #154

Merged
merged 12 commits into from
Mar 29, 2022
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
jchavarri marked this conversation as resolved.
Show resolved Hide resolved
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