Skip to content
This repository has been archived by the owner on Feb 24, 2020. It is now read-only.

Add useReducer API #34

Merged
merged 3 commits into from
Dec 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 76 additions & 17 deletions examples/dom/WebReconciler.re
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
* This is just an example but you could use this to create interesting
* CLI apps, with a react-like functional API!
*/

exception InvalidNodePrimitiveMatchInUpdateInstance;

let str = string_of_int;

module Reconciler = {
/*
Step 1: Define primitives
*/
type imageProps = {src: string};
type buttonProps = {src: string};
type primitives =
| Div
| Span(string)
| Image(imageProps);
| View
Copy link
Member Author

@jchavarri jchavarri Dec 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exploring the feasibility of making the DOM reconciler primitives more similar to those in Revery 😄

| Text(string)
| Image(string) /* img src */
| Button(unit => unit, string); /* onPress, title */

/*
Step 2: Define node type
Expand All @@ -23,6 +27,7 @@ module Reconciler = {
| Div(Js.t(Dom_html.divElement))
| Span(Js.t(Dom_html.element))
| Image(Js.t(Dom_html.imageElement))
| Button(Js.t(Dom_html.buttonElement))
| Container(Js.t(Dom_html.element));
let document = Dom_html.window##.document;

Expand All @@ -32,15 +37,27 @@ module Reconciler = {
let createInstance: primitives => node =
primitive =>
switch (primitive) {
| Div => Div(Dom_html.createDiv(document))
| Span(s) =>
| View => Div(Dom_html.createDiv(document))
| Text(s) =>
let e = Dom_html.createSpan(document);
e##.innerHTML := Js.string(s);
Span(e);
| Image(p) =>
let img = Dom_html.createImg(document);
img##.src := Js.string(p.src);
img##.src := Js.string(p);
Image(img);
| Button(onPress, title) =>
let button =
Dom_html.createButton(~_type=Js.string("button"), document);
let t = Js.string(title);
button##.title := t;
button##.innerHTML := t;
button##.onclick :=
Dom_html.handler(_e => {
onPress();
Js.bool(false);
});
Button(button);
};

/*
Expand All @@ -52,15 +69,26 @@ module Reconciler = {
| Div(e) => e |> Dom_html.element
| Span(e) => e |> Dom_html.element
| Image(e) => e |> Dom_html.element
| Button(e) => e |> Dom_html.element
| Container(e) => e |> Dom_html.element
};

let updateInstance =
(node: node, _oldPrimitive: primitives, newPrimitive: primitives) =>
switch (newPrimitive, node) {
/* The only update operation we handle today is updating src for an image! */
| (Image({src}), Image(e)) => e##.src := Js.string(src)
| _ => ()
| (View, Div(_e)) => ()
| (Text(s), Span(e)) => e##.innerHTML := Js.string(s)
| (Image(src), Image(e)) => e##.src := Js.string(src)
| (Button(onPress, title), Button(e)) =>
let t = Js.string(title);
e##.title := t;
e##.innerHTML := t;
e##.onclick :=
Dom_html.handler(_e => {
onPress();
Js.bool(false);
});
| _ => raise(InvalidNodePrimitiveMatchInUpdateInstance)
};

let appendChild = (parentNode: node, childNode: node) => {
Expand All @@ -69,6 +97,7 @@ module Reconciler = {
| Div(e) => Dom.appendChild(e, innerNode)
| Span(e) => Dom.appendChild(e, innerNode)
| Image(e) => Dom.appendChild(e, innerNode)
| Button(e) => Dom.appendChild(e, innerNode)
| Container(e) => Dom.appendChild(e, innerNode)
};
};
Expand All @@ -79,6 +108,7 @@ module Reconciler = {
| Div(e) => Dom.removeChild(e, innerNode)
| Span(e) => Dom.removeChild(e, innerNode)
| Image(e) => Dom.removeChild(e, innerNode)
| Button(e) => Dom.removeChild(e, innerNode)
| Container(e) => Dom.removeChild(e, innerNode)
};
};
Expand All @@ -90,23 +120,52 @@ module Reconciler = {
| Div(e) => Dom.replaceChild(e, newInnerNode, oldInnerNode)
| Span(e) => Dom.replaceChild(e, newInnerNode, oldInnerNode)
| Image(e) => Dom.replaceChild(e, newInnerNode, oldInnerNode)
| Button(e) => Dom.replaceChild(e, newInnerNode, oldInnerNode)
| Container(e) => Dom.replaceChild(e, newInnerNode, oldInnerNode)
};
};
};

/* Step 5: Hook it up! */
module JsooReact = Reactify.Make(Reconciler);
open JsooReact;

/* Define our primitive components */
let div = (~children, ()) => JsooReact.primitiveComponent(Div, ~children);

let span = (~text, ~children, ()) =>
JsooReact.primitiveComponent(Span(text), ~children);
let view = (~children, ()) => JsooReact.primitiveComponent(View, ~children);

let image = (~children, ~src, ()) =>
let image = (~children, ~src="", ()) =>
JsooReact.primitiveComponent(Image(src), ~children);

let text = (~children: list(string), ()) =>
JsooReact.primitiveComponent(Text(List.hd(children)), ~children=[]);

let button = (~children, ~onPress, ~title, ()) =>
JsooReact.primitiveComponent(Button(onPress, title), ~children);

type action =
| Increment
| Decrement;

let reducer = (state, action) =>
switch (action) {
| Increment => state + 1
| Decrement => state - 1
};

let renderCounter = () => {
let (count, dispatch) = useReducer(reducer, 0);

<view>
<button title="Decrement" onPress={() => dispatch(Decrement)} />
<text> {"Counter: " ++ str(count)} </text>
<button title="Increment" onPress={() => dispatch(Increment)} />
</view>;
};

module CounterButtons = (
val component((render, ~children, ()) => render(renderCounter, ~children))
);

/* Create a container for our UI */
let container =
JsooReact.createContainer(
Expand All @@ -115,7 +174,7 @@ let container =

/* Let's finally put our UI to use! */
let render = () =>
<span text="Hello World" />;
<view> <text> "Hello World" </text> <CounterButtons /> </view>;

/* First render! */
JsooReact.updateContainer(container, render());
2 changes: 1 addition & 1 deletion examples/dom/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<div id="app"></div>
<script
type="text/javascript"
src="../../_build/default/examples/dom/WebReconciler.bc.js"
src="../../_esy/default/build/default/examples/dom/WebReconciler.bc.js"
></script>
</body>
</html>
40 changes: 24 additions & 16 deletions examples/lambda-term/Lambda_term.re
Original file line number Diff line number Diff line change
Expand Up @@ -115,25 +115,33 @@ let button = (~children, ~text, ~onClick, ()) => {

/* And let's create some custom components! */
/*
IncrementingButton
CounterButtons

This shows how you can use state with a callback from a primitive.
This shows how you can use reducers with a callback from a primitive.
*/

module IncrementingButton = (
val component((render, ~children, ()) =>
render(
() => {
let (count, setCount) = useState(0);
let update = () => setCount(count + 1);
type action =
| Increment
| Decrement;

let text = count > 0 ? "Pressed!" : "Click me";
let reducer = (state, action) =>
switch (action) {
| Increment => state + 1
| Decrement => state - 1
};

<button text onClick=update />;
},
~children,
)
)
let renderCounter = () => {
let (count, dispatch) = useReducer(reducer, 0);

<hbox>
<button text="Decrement" onClick={() => dispatch(Decrement)} />
<label text={"Counter: " ++ string_of_int(count)} />
<button text="Increment" onClick={() => dispatch(Increment)} />
</hbox>;
};

module CounterButtons = (
val component((render, ~children, ()) => render(renderCounter, ~children))
);
/*
Clock
Expand Down Expand Up @@ -175,7 +183,7 @@ let main = () => {
<vbox>
<label text="Hello from Reactify!" />
<Clock />
<IncrementingButton />
<CounterButtons />
<button onClick=quit text="Quit" />
</vbox>;

Expand All @@ -196,4 +204,4 @@ let main = () => {
);
};

let () = Lwt_main.run(main());
let () = Lwt_main.run(main());
42 changes: 27 additions & 15 deletions lib/Reactify.re
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ module Make = (ReconcilerImpl: Reconciler) => {
and t = container
and childInstances = list(instance);

type stateUpdateFunction('t) = 't => unit;
type stateResult('t) = ('t, stateUpdateFunction('t));

type node = ReconcilerImpl.node;
type primitives = ReconcilerImpl.primitives;

Expand Down Expand Up @@ -382,7 +379,7 @@ module Make = (ReconcilerImpl: Reconciler) => {
/* Only both replacing node if the primitives are different */
switch (newInstance.component.element, i.component.element) {
| (Primitive(newPrim), Primitive(oldPrim)) =>
if (oldPrim != newPrim) {
if (oldPrim !== newPrim) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably the most relevant change. Because we were using structural equality before, when the primitives would contain any function in side (like, say, a Button that has a callback onPress) an exception with Invalid_argument equal: functional value would be thrown.

I am not sure about the perf implications. This change will lead to the reconciler rendering much more often but on the other hand the checks should be much faster too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting, thanks for calling it out! Would've been easy to miss 😄

Because we were using structural equality before, when the primitives would contain any function in side (like, say, a Button that has a callback onPress) an exception with Invalid_argument equal: functional value would be thrown.

Good catch! Yes, that makes sense that it is problematic in this case. Comparing 'functions' is not really doable in the OCaml sense like it is in JS.

I am not sure about the perf implications. This change will lead to the reconciler rendering much more often but on the other hand the checks should be much faster too.

Yes, good point, I suspect it will depend on how much work it is to create a new 'node' too. Would really need benchmarks to decide if it is a bottleneck or not.

But I'm OK with this going in since it behaves correctly and all the tests are green. We can focus on addressing the performance once we find bottlenecks / have benchmarks to examine. I created #37 to examine this in more depth.

/* Check if the primitive type is the same - if it is, we can simply update the node */
/* If not, we'll replace the node */
if (Utility.areConstructorsEqual(oldPrim, newPrim)) {
Expand Down Expand Up @@ -479,18 +476,20 @@ module Make = (ReconcilerImpl: Reconciler) => {
newChildInstances^;
};

let useState = (v: 't) => {
let state = __globalState^;
let n = ComponentState.popOldState(state, v);

let updateFunction = ComponentState.pushNewState(state, n);
let useReducer =
(reducer: ('state, 'action) => 'state, initialState: 'state) => {
let globalState = __globalState^;
let componentState =
ComponentState.popOldState(globalState, initialState);

/* let updateFunction = (_n) => { (); }; */
let (getState, updateState) =
ComponentState.pushNewState(globalState, componentState);

let currentContext = ComponentState.getCurrentContext(state);
let currentContext = ComponentState.getCurrentContext(globalState);

let setState = (context: ref(option(instance)), newVal: 't) => {
updateFunction(newVal);
let dispatch = (context: ref(option(instance)), action: 'action) => {
let newVal = reducer(getState(), action);
updateState(newVal);
switch (context^) {
| Some(i) =>
let {rootNode, component, _} = i;
Expand All @@ -503,7 +502,20 @@ module Make = (ReconcilerImpl: Reconciler) => {
};
};

(n, setState(currentContext));
(componentState, dispatch(currentContext));
};

type useStateAction('a) =
| SetState('a);
let useStateReducer = (_state, action) =>
switch (action) {
| SetState(newState) => newState
};
let useState = initialState => {
let (componentState, dispatch) =
useReducer(useStateReducer, initialState);
let setState = newState => dispatch(SetState(newState));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I like how you implemented useState with the useReducer primitive 👍

(componentState, setState);
};

let updateContainer = (container, component) => {
Expand All @@ -520,4 +532,4 @@ module Make = (ReconcilerImpl: Reconciler) => {
module State = State;
module Event = Event;
module Utility = Utility;
module Object = Object;
module Object = Object;
9 changes: 5 additions & 4 deletions lib/Reactify_Types.re
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ module type React = {
let useEffect:
(~condition: Effects.effectCondition=?, Effects.effectFunction) => unit;

type stateUpdateFunction('t) = 't => unit;
type stateResult('t) = ('t, stateUpdateFunction('t));
let useState: 't => stateResult('t);
};
let useState: 'state => ('state, 'state => unit);

let useReducer:
(('state, 'action) => 'state, 'state) => ('state, 'action => unit);
};
12 changes: 7 additions & 5 deletions lib/State.re
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module Make = (StateContextImpl: StateContext) => {
mutable newState: HeterogenousMutableList.t,
};

type updateFunction('a) = 'a => unit;
type getterAndUpdater('a) = (unit => 'a, 'a => unit);

let noneContext = () => ref(None);

Expand All @@ -51,20 +51,22 @@ module Make = (StateContextImpl: StateContext) => {
curr;
};

let pushNewState: (t, 'a) => updateFunction('a) =
let pushNewState: (t, 'a) => getterAndUpdater('a) =
(state: t, currentVal: 'a) => {
let updatedVal: ref(Object.t) = ref(Object.to_object(currentVal));
state.newState = List.append(state.newState, [updatedVal]);
let ret: updateFunction('a) =
let ret: getterAndUpdater('a) = (
() => Object.of_object(updatedVal^),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% sure this is necessary... but I added it just to be sure, for cases when there are two actions being dispatched in the same render.

(newVal: 'a) => {
updatedVal := Object.to_object(newVal);
();
};
},
);
ret;
};

let getCurrentContext = (state: t) => state.context^;

let getNewState: t => HeterogenousMutableList.t =
(state: t) => state.newState;
};
};
Loading