From c1e6f431fded5c000ab767d2932624d89b67252e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Sun, 6 Jan 2019 01:49:27 +0100 Subject: [PATCH 1/2] Getting started --- lib/Reactify.re | 4 +- lib/ReactifyExperimental.re | 588 ++++++++++++++++++++++++++++++ lib/ReactifyExperimental.rei | 13 + lib/Reactify_Types.re | 4 +- lib/Reactify_TypesExperimental.re | 139 +++++++ lib/Slots.re | 12 + lib/Slots.rei | 9 + test/StateTest.ts | 28 -- 8 files changed, 763 insertions(+), 34 deletions(-) create mode 100644 lib/ReactifyExperimental.re create mode 100644 lib/ReactifyExperimental.rei create mode 100644 lib/Reactify_TypesExperimental.re create mode 100644 lib/Slots.re create mode 100644 lib/Slots.rei delete mode 100644 test/StateTest.ts diff --git a/lib/Reactify.re b/lib/Reactify.re index 0bb1cb1..f1b289b 100644 --- a/lib/Reactify.re +++ b/lib/Reactify.re @@ -2,9 +2,7 @@ open Reactify_Types; module Make = (ReconcilerImpl: Reconciler) => { /* Module to give us unique IDs for components */ - type renderedElement = - | RenderedPrimitive(ReconcilerImpl.node) - and elementWithChildren = (list(element), Effects.effects, Context.t) + type elementWithChildren = (list(element), Effects.effects, Context.t) and render = unit => elementWithChildren and element = | Primitive(ReconcilerImpl.primitives, render) diff --git a/lib/ReactifyExperimental.re b/lib/ReactifyExperimental.re new file mode 100644 index 0000000..52cc539 --- /dev/null +++ b/lib/ReactifyExperimental.re @@ -0,0 +1,588 @@ +open Reactify_Types; + +module Make = (ReconcilerImpl: Reconciler) => { + /* Module to give us unique IDs for components */ + type elementWithChildren = (list(element), Effects.effects, Context.t) + and render = opaqueSlots => elementWithChildren + and element = + | Primitive(ReconcilerImpl.primitives, render) + | Component(ComponentId.t, render) + | Provider(render) + | Empty(render) + /* + An instance is a component that has been rendered. + We store some additional context for it, like the state, + effects that need to be run, and corresponding nodes. + */ + and opaqueSlots = + | OpaqueSlot(Slots.t('slot, 'nextSlots)): opaqueSlots + and instance = { + mutable element, + children: list(element), + node: option(ReconcilerImpl.node), + rootNode: ReconcilerImpl.node, + mutable childInstances, + mutable effectInstances: Effects.effectInstances, + slots: opaqueSlots, + context: Context.HeterogenousHashtbl.t, + container: t, + } + and container = { + onBeginReconcile: Event.t(ReconcilerImpl.node), + onEndReconcile: Event.t(ReconcilerImpl.node), + rootInstance: ref(option(instance)), + containerNode: ReconcilerImpl.node, + } + and t = container + and childInstances = list(instance) + and hook('t) = + | Hook(element, 't) + and state('s) = + | State('s) + and reducer('r) = + | Reducer('r) + and effect = + | Effect + and context('t) = + | Context('t) + and emptyHook = hook(unit); + + type node = ReconcilerImpl.node; + type primitives = ReconcilerImpl.primitives; + + /* + Internal helpers to aid the hooks type representation, + the wrapping variants could be optimized away in the future as they're not used at runtime + */ + let addHook = (newHook, Hook(element, h)) => Hook(element, (h, newHook)); + let addState = (~state as s) => addHook(State(s)); + let addEffect = element => addHook(Effect, element); + let addReducer = (~reducer as r) => addHook(Reducer(r)); + let addContext = (~value as v) => addHook(Context(v)); + let elementToHook = x => Hook(x, ()); + let elementToHook = x => Hook(x, ()); + let elementFromHook = (Hook(x, _)) => x; + let elementFromHook = (Hook(x, _)) => x; + + /* + A global, non-pure container to hold effects + during the course of a render operation. + */ + let __globalEffects = Effects.create(); + + let _uniqueIdScope = ComponentId.createScope(); + + /* + A global, non-pure container to hold current + context during hte course of a render operation. + */ + let noContext = Context.create(); + let __globalContext = ref(noContext); + + /* + Container API + */ + type reconcileNotification = node => unit; + + let createContainer = + ( + ~onBeginReconcile: option(reconcileNotification)=?, + ~onEndReconcile: option(reconcileNotification)=?, + rootNode: ReconcilerImpl.node, + ) => { + let be = Event.create(); + let ee = Event.create(); + + switch (onBeginReconcile) { + | Some(x) => + let _ = Event.subscribe(be, x); + (); + | _ => () + }; + + switch (onEndReconcile) { + | Some(x) => + let _ = Event.subscribe(ee, x); + (); + | _ => () + }; + + let ret: container = { + onBeginReconcile: be, + onEndReconcile: ee, + containerNode: rootNode, + rootInstance: ref(None), + }; + + ret; + }; + + let empty: emptyHook = + elementToHook(Empty(_slots => ([], [], __globalContext^))); + + let render = (id: ComponentId.t, lazyElement, ~children) => { + ignore(children); + let ret: emptyHook = + elementToHook( + Component( + id, + slots => { + Effects.resetEffects(__globalEffects); + let childElement = lazyElement(slots) |> elementFromHook; + let children = [childElement]; + let effects = Effects.getEffects(__globalEffects); + let renderResult: elementWithChildren = ( + children, + effects, + __globalContext^, + ); + renderResult; + }, + ), + ); + ret; + }; + + module type Component = { + type hooks; + type createElement; + let createElement: createElement; + }; + + let createComponent = + ( + type c, + type h, + create: + ( + (opaqueSlots => hook(h), ~children: list(emptyHook)) => emptyHook + ) => + c, + ) + : (module Component with type createElement = c and type hooks = h) => { + let id = ComponentId.newId(_uniqueIdScope); + let boundFunc = create(render(id)); + (module + { + type hooks = h; + type createElement = c; + let createElement = boundFunc; + }); + }; + let component = createComponent; + + let primitiveComponent = (~children, prim) => { + let comp: emptyHook = + elementToHook( + Primitive( + prim, + _slots => ( + List.map(elementFromHook, children), + [], + __globalContext^, + ), + ), + ); + comp; + }; + + /* Context */ + let __contextId = ref(0); + type providerConstructor('t) = + (~children: list(emptyHook), ~value: 't, unit) => emptyHook; + type contextValue('t) = { + initialValue: 't, + id: int, + }; + + let createContext = (initialValue: 't) => { + let contextId = __contextId^; + __contextId := __contextId^ + 1; + let ret: contextValue('t) = {initialValue, id: contextId}; + ret; + }; + + let getProvider = ctx => { + let provider = (~children, ~value, ()) => { + let ret: emptyHook = + elementToHook( + Provider( + _slots => { + let contextId = ctx.id; + let context = Context.clone(__globalContext^); + Context.set(context, contextId, Object.to_object(value)); + (List.map(elementFromHook, children), [], context); + }, + ), + ); + ret; + }; + provider; + }; + + let useContext = (ctx: contextValue('t)) => { + let value = + switch (Context.get(__globalContext^, ctx.id)) { + | Some(x) => Object.of_object(x) + | None => ctx.initialValue + }; + value; + }; + + let useContext = (ctx: contextValue('t), continuation) => { + let value = useContext(ctx); + continuation(value) |> addContext(~value); + }; + + let useEffect = + ( + ~condition: Effects.effectCondition=Effects.Always, + e: Effects.effectFunction, + ) => + Effects.addEffect(~condition, __globalEffects, e); + + let useEffect = + ( + ~condition: Effects.effectCondition=Effects.Always, + e: Effects.effectFunction, + continuation, + ) => { + useEffect(~condition, e); + continuation() |> addEffect; + }; + + let _getEffectsFromInstance = (instance: option(instance)) => + switch (instance) { + | None => [] + | Some(i) => i.effectInstances + }; + + let _getPreviousChildInstances = (instance: option(instance)) => + switch (instance) { + | None => [] + | Some(i) => i.childInstances + }; + + let rec getFirstNode = (node: instance) => + switch (node.node) { + | Some(n) => Some(n) + | None => + switch (node.childInstances) { + | [] => None + | [c] => getFirstNode(c) + | _ => None + } + }; + + let isInstanceOfComponent = (instance: option(instance), element: element) => + switch (instance) { + | None => false + | Some(x) => + switch (x.element, element) { + | (Primitive(a, _), Primitive(b, _)) => + Utility.areConstructorsEqual(a, b) + | (Component(a, _), Component(b, _)) => a === b + | _ => x.element === element + } + }; + + /* + * Instantiate turns a component function into a live instance, + * and asks the reconciler to append it to the root node. + */ + let rec instantiate = + ( + rootNode, + previousInstance: option(instance), + element: element, + context: Context.t, + container: t, + ) => { + /* Recycle any previous effect instances */ + let previousEffectInstances = _getEffectsFromInstance(previousInstance); + + let isSameInstanceAsBefore = + isInstanceOfComponent(previousInstance, element); + + let slots = + switch (previousInstance) { + | None => OpaqueSlot(Slots.create()) + | Some(i) => i.slots + }; + + __globalContext := context; + let (children, effects, newContext) = + switch (element) { + | Primitive(_, render) + | Component(_, render) + | Provider(render) + | Empty(render) => render(slots) + }; + /* Once rendering is complete, we don't need this anymore */ + __globalContext := noContext; + + let newEffectCount = List.length(effects); + + let newEffectInstances = + isSameInstanceAsBefore ? + Effects.runEffects( + ~previousInstances=previousEffectInstances, + effects, + ) : + { + Effects.drainEffects(previousEffectInstances); + let emptyInstances = + Effects.createEmptyEffectInstances(newEffectCount); + Effects.runEffects(~previousInstances=emptyInstances, effects); + }; + + let primitiveInstance = + switch (element) { + | Primitive(p, _render) => Some(ReconcilerImpl.createInstance(p)) + | _ => None + }; + + let nextRootPrimitiveInstance = + switch (primitiveInstance) { + | Some(i) => i + | None => rootNode + }; + + let previousChildInstances = _getPreviousChildInstances(previousInstance); + let childInstances = + reconcileChildren( + nextRootPrimitiveInstance, + previousChildInstances, + children, + newContext, + container, + ); + + let instance: instance = { + element, + node: primitiveInstance, + rootNode: nextRootPrimitiveInstance, + children, + childInstances, + effectInstances: newEffectInstances, + slots, + context: newContext, + container, + }; + + instance; + } + and reconcile = (rootNode, instance, component, context, container) => { + let newInstance = + instantiate(rootNode, instance, component, context, container); + + let r = + switch (instance) { + | None => + switch (newInstance.node) { + | Some(n) => ReconcilerImpl.appendChild(rootNode, n) + | None => () + }; + + newInstance; + | Some(i) => + let ret = + switch (newInstance.node, i.node) { + | (Some(a), Some(b)) => + /* Only both replacing node if the primitives are different */ + switch (newInstance.element, i.element) { + | (Primitive(newPrim, _), Primitive(oldPrim, _)) => + if (oldPrim !== newPrim) { + /* 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)) { + ReconcilerImpl.updateInstance(b, oldPrim, newPrim); + i.element = newInstance.element; + i.effectInstances = newInstance.effectInstances; + i.childInstances = + reconcileChildren( + b, + i.childInstances, + newInstance.children, + context, + container, + ); + i; + } else { + ReconcilerImpl.replaceChild(rootNode, a, b); + newInstance; + }; + } else { + /* The node itself is unchanged, so we'll just reconcile the children */ + i.effectInstances = newInstance.effectInstances; + i.childInstances = + reconcileChildren( + b, + i.childInstances, + newInstance.children, + context, + container, + ); + i; + } + | _ => + print_endline( + "ERROR: Should only be nodes if there are primitives!", + ); + newInstance; + } + | (Some(a), None) => + /* If there was a non-primitive instance, we need to get the top-level node - */ + /* and then remove it */ + let currentNode = getFirstNode(i); + switch (currentNode) { + | Some(c) => ReconcilerImpl.removeChild(rootNode, c) + | _ => () + }; + ReconcilerImpl.appendChild(rootNode, a); + newInstance; + | (None, Some(b)) => + ReconcilerImpl.removeChild(rootNode, b); + newInstance; + | (None, None) => newInstance + }; + + ret; + }; + r; + } + and reconcileChildren = + ( + root: node, + currentChildInstances: childInstances, + newChildren: list(element), + context: Context.t, + container: t, + ) => { + let currentChildInstances: array(instance) = + Array.of_list(currentChildInstances); + let newChildren = Array.of_list(newChildren); + + let newChildInstances: ref(childInstances) = ref([]); + + for (i in 0 to Array.length(newChildren) - 1) { + let childInstance = + i >= Array.length(currentChildInstances) ? + None : Some(currentChildInstances[i]); + let childComponent = newChildren[i]; + let newChildInstance = + reconcile(root, childInstance, childComponent, context, container); + newChildInstances := + List.append(newChildInstances^, [newChildInstance]); + }; + + /* Clean up existing children */ + for (i in + Array.length(newChildren) to + Array.length(currentChildInstances) - 1) { + switch (currentChildInstances[i].node) { + | Some(n) => ReconcilerImpl.removeChild(root, n) + | _ => () + }; + }; + + newChildInstances^; + }; + + let useReducer: type state. ( + (state, 'action) => state, + state, + (state, 'nextSlot), + opaqueSlots, + ) => int = (reducer, initialState, continuation, opaqueSlots) => { + let OpaqueSlot(slots) = opaqueSlots; + let state = Slots.use(~default=initialState, continuation, slots); + let (getState, updateState) = + DeprecatedComponentState.pushNewState( + deprecatedGlobalState, + componentState, + ); + + let currentContext = + DeprecatedComponentState.getCurrentContext(deprecatedGlobalState); + + let dispatch = (context: ref(option(instance)), action: 'action) => { + let newVal = reducer(getState(), action); + updateState(newVal); + switch (context^) { + | Some(i) => + let {rootNode, element, _} = i; + Event.dispatch(i.container.onBeginReconcile, rootNode); + let _ = + reconcile(rootNode, Some(i), element, i.context, i.container); + Event.dispatch(i.container.onEndReconcile, rootNode); + (); + | _ => print_endline("WARNING: Skipping reconcile!") + }; + }; + + (componentState, dispatch(currentContext)); + }; + + let _useReducer = + ( + reducer: ('state, 'action) => 'state, + initialState: 'state, + continuation, + ) => + continuation(useReducer(reducer, initialState)); + + /* + There's an internal and a public version of `useReducer`. The internal version, + which has no "hooks types propagation", allows to keep the types in `useState` + (which reuses `useReducer`) simpler + */ + let useReducer = (reducer, initialState, continuation) => + _useReducer(reducer, initialState, continuation) |> addReducer(~reducer); + + 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)); + (componentState, setState); + }; + + let useState = (initialState, continuation) => + _useReducer( + useStateReducer, + initialState, + ((componentState, dispatch)) => { + let setState = newState => dispatch(SetState(newState)); + continuation((componentState, setState)) + |> addState(~state=initialState); + }, + ); + + let updateContainer = (container, hook) => { + let {containerNode, rootInstance, onBeginReconcile, onEndReconcile} = container; + let prevInstance = rootInstance^; + Event.dispatch(onBeginReconcile, containerNode); + let nextInstance = + reconcile( + containerNode, + prevInstance, + elementFromHook(hook), + noContext, + container, + ); + rootInstance := Some(nextInstance); + Event.dispatch(onEndReconcile, containerNode); + }; +}; + +module State = DeprecatedState; +module Event = Event; +module Utility = Utility; +module Object = Object; \ No newline at end of file diff --git a/lib/ReactifyExperimental.rei b/lib/ReactifyExperimental.rei new file mode 100644 index 0000000..d059c5c --- /dev/null +++ b/lib/ReactifyExperimental.rei @@ -0,0 +1,13 @@ +open Reactify_TypesExperimental; + +module Make: + (ReconcilerImpl: Reconciler) => + + React with + type node = ReconcilerImpl.node and + type primitives = ReconcilerImpl.primitives; + +module State = DeprecatedState; +module Event = Event; +module Utility = Utility; +module Object = Object; diff --git a/lib/Reactify_Types.re b/lib/Reactify_Types.re index 0acd529..066327f 100644 --- a/lib/Reactify_Types.re +++ b/lib/Reactify_Types.re @@ -35,9 +35,7 @@ module type React = { type effect; type context('t); - type renderedElement = - | RenderedPrimitive(node) - and elementWithChildren = (list(element), Effects.effects, Context.t) + type elementWithChildren = (list(element), Effects.effects, Context.t) and render = unit => elementWithChildren and element = | Primitive(primitives, render) diff --git a/lib/Reactify_TypesExperimental.re b/lib/Reactify_TypesExperimental.re new file mode 100644 index 0000000..affbaac --- /dev/null +++ b/lib/Reactify_TypesExperimental.re @@ -0,0 +1,139 @@ +/* + * Interface to bridge a React-style functional API + * with a mutable back-end. Similiar in spirit + * to the reconciler interface provided via 'react-conciler'. + */ + +module type Reconciler = { + /* + Primitives is a variant type describing metadata needed + to instantiate something in the mutable world. + */ + type primitives; + + /* + A node is a live instance representing a mutable object + */ + type node; + + let appendChild: (node, node) => unit; + + let createInstance: primitives => node; + + let replaceChild: (node, node, node) => unit; + + let removeChild: (node, node) => unit; + + let updateInstance: (node, primitives, primitives) => unit; +}; + +module type React = { + type primitives; + type node; + type state('s); + type reducer('r); + type effect; + type context('t); + + type elementWithChildren = (list(element), Effects.effects, Context.t) + and render = unit => elementWithChildren + and renderXp = opaqueSlots => elementWithChildren + and element = + | Primitive(primitives, render) + | Component(ComponentId.t, render) + | Provider(render) + | Empty(render) + and elementXp = + | PrimitiveXp(primitives, renderXp) + | ComponentXp(ComponentId.t, renderXp) + | ProviderXp(renderXp) + | EmptyXp(renderXp) + and hook('t) = + | Hook(element, 't) + and hookXp('t) = + | HookXp(elementXp, 't) + and emptyHook = hook(unit) + and emptyHookXp = hookXp(unit) + and opaqueSlots; + + type t; + + /* + Container API + */ + type reconcileNotification = node => unit; + let createContainer: + ( + ~onBeginReconcile: reconcileNotification=?, + ~onEndReconcile: reconcileNotification=?, + node + ) => + t; + let updateContainer: (t, emptyHook) => unit; + + /* + Component creation API + */ + let primitiveComponent: + (~children: list(emptyHook), primitives) => emptyHook; + + module type Component = { + type hooks; + type createElement; + let createElement: createElement; + }; + + let createComponent: + ( + ((opaqueSlots => hook('h), ~children: list(emptyHook)) => emptyHook) => + 'c + ) => + (module Component with type createElement = 'c and type hooks = 'h); + let component: + (((unit => hook('h), ~children: list(emptyHook)) => emptyHook) => 'c) => + (module Component with type createElement = 'c and type hooks = 'h); + + /* + Component API + */ + + type providerConstructor('t) = + (~children: list(emptyHook), ~value: 't, unit) => emptyHook; + type contextValue('t); + + let getProvider: contextValue('t) => providerConstructor('t); + let createContext: 't => contextValue('t); + let useContext: contextValue('t) => 't; + let useContextXp: + (contextValue('t), 't => hook('a)) => hook(('a, context('t))); + + let empty: emptyHook; + + let useEffect: + (~condition: Effects.effectCondition=?, Effects.effectFunction) => unit; + + let useEffectXp: + ( + ~condition: Effects.effectCondition=?, + Effects.effectFunction, + unit => hook('a) + ) => + hook(('a, effect)); + + let useState: 'state => ('state, 'state => unit); + + let useStateXp: + ('state, (('state, 'state => unit)) => hook('a)) => + hook(('a, state('state))); + + let useReducer: + (('state, 'action) => 'state, 'state) => ('state, 'action => unit); + + let useReducerXp: + ( + ('state, 'action) => 'state, + 'state, + (('state, 'action => unit)) => hook('a) + ) => + hook(('a, reducer(('state, 'action) => 'state))); +}; \ No newline at end of file diff --git a/lib/Slots.re b/lib/Slots.re new file mode 100644 index 0000000..5a095e9 --- /dev/null +++ b/lib/Slots.re @@ -0,0 +1,12 @@ + type t('slot, 'nextSlots) = ref(option(('slot, 'nextSlots))); + let create = () => ref(None); + let use = (~default, continuation, slots: t(_)) => { + switch (slots^) { + | None => + let slot = default; + let nextSlots = create(); + slots := Some((slot, nextSlots)); + continuation(slot, nextSlots); + | Some((slot, nextSlots)) => continuation(slot, nextSlots) + }; + }; \ No newline at end of file diff --git a/lib/Slots.rei b/lib/Slots.rei new file mode 100644 index 0000000..fde84df --- /dev/null +++ b/lib/Slots.rei @@ -0,0 +1,9 @@ +type t('slot, 'nextSlots); +let create: unit => t('slot, 'nextSlots); +let use: + ( + ~default: 'slot, + (('slot, t('slot2, 'nextSlots))) => 'c, + t('slot, t('slot2, 'nextSlots)) + ) => + 'c; \ No newline at end of file diff --git a/test/StateTest.ts b/test/StateTest.ts deleted file mode 100644 index eb93dca..0000000 --- a/test/StateTest.ts +++ /dev/null @@ -1,28 +0,0 @@ -module State = Reactify.State; - -test("Object conversion works with ints", () => { - let i = 1; - let stateI = Object.to_object(i); - let rehydratedI = Object.of_object(stateI); - assert(i == rehydratedI); -}); - -test("Object conversion works with tuples", () => { - let p = (1, "a"); - let stateP = Object.to_object(p); - let rehydratedP = Object.of_object(stateP); - assert(p == rehydratedP); -}); - -test("Object can be used to create list of different types", () => { - let stateList: list(State.t) = []; - - let stateList = [Object.to_object(1), ...stateList]; - let stateList = [Object.to_object(("a", "b")), ...stateList]; - - let firstElement = List.nth(stateList, 0); - assert(Object.of_object(firstElement) == ("a", "b")); - - let lastElement = List.nth(stateList, 1); - assert(Object.of_object(lastElement) == 1); -}); From 98e025b32c900bdf889822005053ed2c36f8a407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Tue, 8 Jan 2019 01:10:48 +0100 Subject: [PATCH 2/2] Starting useReducer, removing all non-used hooks-related type --- examples/dom/WebReconciler.re | 18 +- lib/ReactifyExperimental.re | 331 ++++++++++++++---------------- lib/ReactifyExperimental.rei | 2 +- lib/Reactify_TypesExperimental.re | 91 ++++---- lib/Slots.re | 24 ++- lib/Slots.rei | 8 +- 6 files changed, 229 insertions(+), 245 deletions(-) diff --git a/examples/dom/WebReconciler.re b/examples/dom/WebReconciler.re index d45db42..8a20193 100644 --- a/examples/dom/WebReconciler.re +++ b/examples/dom/WebReconciler.re @@ -127,7 +127,7 @@ module Reconciler = { }; /* Step 5: Hook it up! */ -module JsooReact = Reactify.Make(Reconciler); +module JsooReact = Reactify__ReactifyExperimental.Make(Reconciler); open JsooReact; /* Define our primitive components */ @@ -152,14 +152,14 @@ let reducer = (state, action) => | Decrement => state - 1 }; -let renderCounter = () => - useReducerExperimental(reducer, 0, ((count, dispatch)) => - -