Skip to content

Add API for nesting reconcilers #30

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

Closed
wokalski opened this issue Mar 29, 2019 · 1 comment · Fixed by #46
Closed

Add API for nesting reconcilers #30

wokalski opened this issue Mar 29, 2019 · 1 comment · Fixed by #46

Comments

@wokalski
Copy link
Member

There are two issues with nesting reconcilers now:

  1. Weird and repetitive logic like this: (https://github.com/briskml/brisk/blob/master/renderer-macos/lib/components/Toolbar.re#L79)
  2. There's no way to chain them in such a way that processing updates like state changes, effects, executing host view updates can be done at the same time for all reconcilers in the tree

The solution here is to probably change the architecture so that there'll be one reconciler per app with a different mechanism to define output nodes. It should solve also other problems with naming etc.

@wokalski
Copy link
Member Author

I think it's possible to achieve that by moving most of the update logic outside of Make functor like so:

module Make = (OutputTree: OutputTree) => {
  module GlobalState = {
    let debug = ref(true);
    let componentKeyCounter = ref(0);
    let reset = () => {
      debug := true;
      componentKeyCounter := 0;
    };

    /**
     * Use physical equality to recognize that an element was added to the list
     * of children.
     * Note: this currently does not check for pending updates on components in
     * the list.
     */
    let useTailHack = ref(false);
  };

  module Key = {
    type t = int;

    let equal = (==);
    let none = (-1);
    let dynamicKeyMagicNumber = 0;
    let create = () => {
      incr(GlobalState.componentKeyCounter);
      GlobalState.componentKeyCounter^;
    };
  };
-  type internalOutputNode =
-    | Node(OutputTree.node)
-    | UpdatedNode(OutputTree.node, OutputTree.node);
-  type outputNodeContainer = Lazy.t(internalOutputNode);
-  type outputNodeGroup = list(outputNodeContainer);
-  type id('a) = ..;
-  type instance('hooks, 'initialHooks, 'elementType, 'outputNode) = {
-    hooks: Hooks.state('hooks, unit),
-    component: component('hooks, 'initialHooks, 'elementType, 'outputNode),
-    element,
-    instanceSubForest: instanceForest,
-    subElements: 'elementType,
-    hostInstance: 'outputNode,
-  }
-  and element =
-    | Element(component('hooks, 'initialHooks, 'elementType, 'outputNode))
-      : element
-  and syntheticElement =
-    | Flat(element)
-    | Nested(list(syntheticElement))
-  and outputTreeElement = {
-    make: unit => OutputTree.node,
-    configureInstance:
-      (~isFirstRender: bool, OutputTree.node) => OutputTree.node,
-    children: syntheticElement,
-  }
-  and elementType('elementType, 'outputNode) =
-    | Host: elementType(outputTreeElement, outputNodeContainer)
-    | React: elementType(syntheticElement, outputNodeGroup)
-  and instanceForest =
-    | IFlat(opaqueInstance)
-    | INested(list(instanceForest), int /*subtree size*/)
-  and component('hooks, 'initialHooks, 'elementType, 'outputNode) = {
-    debugName: string,
-    key: int,
-    elementType: elementType('elementType, 'outputNode),
-    id: id(instance('hooks, 'initialHooks, 'elementType, 'outputNode)),
-    eq:
-      'a.
-      (
-        'a,
-        id('a),
-        id(instance('hooks, 'initialHooks, 'elementType, 'outputNode))
-      ) =>
-      option(instance('hooks, 'initialHooks, 'elementType, 'outputNode)),
-
-    render:
-      Hooks.t('hooks, unit, 'initialHooks, 'initialHooks) =>
-      (Hooks.t(unit, unit, 'hooks, unit), 'elementType),
-  }
-  and opaqueInstance =
-    | Instance(instance('hooks, 'initialHooks, 'elementType, 'outputNode))
-      : opaqueInstance;
-
-  type renderedElement = {
-    nearestHostOutputNode: outputNodeContainer,
-    instanceForest,
-    enqueuedEffects: list(list(unit => unit)),
-  };
-
-  type opaqueInstanceUpdate = {
-    nearestHostOutputNode: outputNodeContainer,
-    opaqueInstance,
-    enqueuedEffects: list(list(unit => unit)),
-  };
};

Next

We can simply move it out of the functor and add a generic type parameter for the OutputTree.node type (called 'outputTreeNode). Additionally we'll be able to move most of the logic outside of this file.

module type OutputTree = {
  type node;

  let markAsStale: unit => unit;

  let insertNode: (~parent: node, ~child: node, ~position: int) => node;
  let deleteNode: (~parent: node, ~child: node, ~position: int) => node;
  let moveNode: (~parent: node, ~child: node, ~from: int, ~to_: int) => node;
};

+ type internalOutputNode('outputTreeNode) =
+   | Node('outputTreeNode)
+   | UpdatedNode('outputTreeNode, 'outputTreeNode);
+ type outputNodeContainer('outputTreeNode) =
+   Lazy.t(internalOutputNode('outputTreeNode));
+ type outputNodeGroup('outputTreeNode) =
+   list(outputNodeContainer('outputTreeNode));
+ type id('a) = ..;
+ type instance(
+   'hooks,
+   'initialHooks,
+   'elementType,
+   'outputNode,
+   'outputTreeNode,
+ ) = {
+   hooks: Hooks.state('hooks, unit),
+   component:
+     component(
+       'hooks,
+       'initialHooks,
+       'elementType,
+       'outputNode,
+       'outputTreeNode,
+     ),
+   element,
+   instanceSubForest: instanceForest,
+   subElements: 'elementType,
+   hostInstance: 'outputNode,
+ }
+ and element =
+   | Element(
+       component(
+         'hooks,
+         'initialHooks,
+         'elementType,
+         'outputNode,
+         'outputTreeNode,
+       ),
+     )
+     : element
+ and syntheticElement =
+   | Flat(element)
+   | Nested(list(syntheticElement))
+ and outputTreeElement('outputTreeNode) = {
+   make: unit => 'outputTreeNode,
+   configureInstance:
+     (~isFirstRender: bool, 'outputTreeNode) => 'outputTreeNode,
+   children: syntheticElement,
+ }
+ and elementType('elementType, 'outputNode, 'outputTreeNode) =
+   | Host: elementType(
+             outputTreeElement('outputTreeNode),
+             outputNodeContainer('outputTreeNode),
+             'outputTreeNode,
+           )
+   | React: elementType(
+              syntheticElement,
+              outputNodeGroup('outputTreeNode),
+              'outputTreeNode,
+            )
+ and instanceForest =
+   | IFlat(opaqueInstance)
+   | INested(list(instanceForest), int /*subtree size*/)
+ and component(
+   'hooks,
+   'initialHooks,
+   'elementType,
+   'outputNode,
+   'outputTreeNode,
+ ) = {
+   debugName: string,
+   key: int,
+   elementType: elementType('elementType, 'outputNode, 'outputTreeNode),
+   id:
+     id(
+       instance(
+         'hooks,
+         'initialHooks,
+         'elementType,
+         'outputNode,
+         'outputTreeNode,
+       ),
+     ),
+   eq:
+     'a.
+     (
+       'a,
+       id('a),
+       id(
+         instance(
+           'hooks,
+           'initialHooks,
+           'elementType,
+           'outputNode,
+           'outputTreeNode,
+         ),
+       )
+     ) =>
+     option(
+       instance(
+         'hooks,
+         'initialHooks,
+         'elementType,
+         'outputNode,
+         'outputTreeNode,
+       ),
+     ),
+ 
+   render:
+     Hooks.t('hooks, unit, 'initialHooks, 'initialHooks) =>
+     (Hooks.t(unit, unit, 'hooks, unit), 'elementType),
+ }
+ and opaqueInstance =
+   | Instance(
+       instance(
+         'hooks,
+         'initialHooks,
+         'elementType,
+         'outputNode,
+         'outputTreeNode,
+       ),
+     )
+     : opaqueInstance;
+ 
+ type renderedElement('outputTreeNode) = {
+   nearestHostOutputNode: outputNodeContainer('outputTreeNode),
+   instanceForest,
+   enqueuedEffects: list(list(unit => unit)),
+ };
+ 
+ type opaqueInstanceUpdate('outputTreeNode) = {
+   nearestHostOutputNode: outputNodeContainer('outputTreeNode),
+   opaqueInstance,
+   enqueuedEffects: list(list(unit => unit)),
+ };

module Make = (OutputTree: OutputTree) => {
  module GlobalState = {
    let debug = ref(true);
    let componentKeyCounter = ref(0);
    let reset = () => {
      debug := true;
      componentKeyCounter := 0;
    };

    /**
     * Use physical equality to recognize that an element was added to the list
     * of children.
     * Note: this currently does not check for pending updates on components in
     * the list.
     */
    let useTailHack = ref(false);
  };

  module Key = {
    type t = int;

    let equal = (==);
    let none = (-1);
    let dynamicKeyMagicNumber = 0;
    let create = () => {
      incr(GlobalState.componentKeyCounter);
      GlobalState.componentKeyCounter^;
    };
  };
};

Result

It'll essentially allow us to nest any syntheticElements inside each other. Thanks to that, here we'll simply pass the children that we've received and reconciliation will happen as expected.

The only unsolved problem here is that we don't have a way of defining a context for the OutputTree operations. If we simply move the types and related functions out of the functor, OutputTree.insertNode and friends won't compile anymore - nearestHostOutputNode might be of any type. For instance let's assume we do this:

<view>
  <SegmentedControl.item />
</view>

It cannot work - there's no way to mount a SegmentedControl.item into a view. We need a way to define those contexts for a given hierarchy (like view hierarchy, segmentedControl hierarchy etc.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant