|
|
@@ -0,0 +1,285 @@ |
|
|
# Reconcilers |
|
|
|
|
|
After working and playing with multiple reconcilers, react implementations, etc i've been comparing most of them and looking through their weaknesses and strong points. |
|
|
|
|
|
# [ReactMini](https://github.com/reasonml/reason-react/tree/master/ReactMini/) |
|
|
|
|
|
The first reconciler in the wild. As far as i know, it was an implementation of ReasonReact to check that the implementation was sound. |
|
|
|
|
|
#### Immutable instance tree: |
|
|
|
|
|
This has great benefits given that two instances can be checked referentially instead of structually and we can bail out reconcilation work as soon as we are sure two instances are equally by using `===`. This also has great benefits in a future multicore ocaml implementation, given that immutable data structures can be used by multiple threards and return new instances. |
|
|
|
|
|
#### State is in the instance record: |
|
|
|
|
|
This is a great benefit in terms of creating a hot code reloadable reconciler given that contrary to Reactjs class component, the state is not inside an oop object hidden and can't be extracted, in this case state is part of an instance tree. |
|
|
This means that whenever we `let instance = React.render(<Component/>)` all state is in instance, and from here we can swap state, modify it, seralize it and share it between two computers via wifi, etc. This idea surely has it's pitfalls but is just mind blowing. |
|
|
|
|
|
#### Pending updates list: |
|
|
|
|
|
This has the benefit of aggregating updates and executing them in order. |
|
|
|
|
|
Let's say we have this function. |
|
|
|
|
|
```reason |
|
|
let Counter = () => { |
|
|
let (value, setValue) = useState(0); |
|
|
<Button onClick={_ => { |
|
|
setValue(1); |
|
|
setValue(2); |
|
|
setValue(3); |
|
|
}}> |
|
|
{React.string(string_of_int(value))} |
|
|
</Button> |
|
|
}; |
|
|
|
|
|
React.render(<Counter />); |
|
|
``` |
|
|
|
|
|
If we reconcile immediatly after calling `setValue` we will have this workflow: |
|
|
|
|
|
`setValue(1) -> reconcile -> setValue(2) -> reconcile -> setValue(3) -> reconcile` |
|
|
|
|
|
In this case we will have 2 reconcile computations that are going to be in vain. We can easliy imagine how quickly this can get out of hand, let's imagine how `for (i in 1 to 10000) { setValue(i) }` can make this computation extremely expensive. |
|
|
|
|
|
Pending updates will solve this by aggregating a list of updates. This will end up looking like |
|
|
|
|
|
`setValue(1) -> setValue(2) -> setValue(3) -> reconcile` |
|
|
|
|
|
#### Update Log |
|
|
|
|
|
This reconciler uses an UpdateLog (in React fiber this is more or less a [list of effects](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiber.js#L154) that is produced by perfomUnitOfWork). |
|
|
|
|
|
```reason |
|
|
module UpdateLog = { |
|
|
type update = { |
|
|
oldId: int, |
|
|
newId: int, |
|
|
oldOpaqueInstance: opaqueInstance, |
|
|
newOpaqueInstance: opaqueInstance, |
|
|
componentChanged: bool, |
|
|
stateChanged: bool, |
|
|
subTreeChanged: bool |
|
|
}; |
|
|
type entry = |
|
|
| UpdateInstance(update) |
|
|
| NewRenderedElement(renderedElement); |
|
|
type t = ref(list(entry)); |
|
|
let create = () => ref([]); |
|
|
let add = (updateLog, x) => updateLog := [x, ...updateLog^]; |
|
|
}; |
|
|
|
|
|
``` |
|
|
|
|
|
This list of effects have great benefits, e.g: |
|
|
|
|
|
Let's say we are rendering a tree of elements, and we arrive at two components that the subtree is fairly complex, this will allow us to reconcile those subTrees at the same time and get two logs of updates ready to be executed by the main thread. |
|
|
|
|
|
#### [Remote Actions](https://github.com/reasonml/reason-react/blob/master/ReactMini/src/React.re#L870) |
|
|
|
|
|
This is a little pearl of this implementation. We can test components by executing remote actions |
|
|
like in this [test](https://github.com/reasonml/reason-react/blob/master/ReactMini/src/Test.re#L328): |
|
|
|
|
|
```reason |
|
|
startTest(~msg="Test Update on Alternate Clicks"); |
|
|
let rAction = RemoteAction.create(); |
|
|
let rendered0 = renderAndPrint(~msg="Initial", <UpdateAlternateClicks rAction />); |
|
|
RemoteAction.send(rAction, ~action=Click); |
|
|
let rendered1 = flushAndPrint(~msg="First click then flush", rendered0); |
|
|
RemoteAction.send(rAction, ~action=Click); |
|
|
let rendered2 = flushAndPrint(~msg="Second click then flush", rendered1); |
|
|
RemoteAction.send(rAction, ~action=Click); |
|
|
let rendered3 = flushAndPrint(~msg="Third click then flush", rendered2); |
|
|
RemoteAction.send(rAction, ~action=Click); |
|
|
let rendered4 = flushAndPrint(~msg="Fourth click then flush", rendered3); |
|
|
printAll([rendered0, rendered1, rendered2, rendered3, rendered4]); |
|
|
``` |
|
|
|
|
|
Just a nice module to have when testing a component. |
|
|
|
|
|
# [Brisk](https://github.com/briskml/brisk) |
|
|
|
|
|
A child of ReactMini with a lot of extra benefits. |
|
|
|
|
|
#### Host Implementation Functor |
|
|
|
|
|
This is similar to a [host config](https://github.com/facebook/react/tree/master/packages/react-reconciler#practical-examples) in react. Is defined by this module type: |
|
|
|
|
|
```reason |
|
|
module type HostImplementation = { |
|
|
type hostView; |
|
|
|
|
|
let getInstance: int => option(hostView); |
|
|
let memoizeInstance: (int, hostView) => unit; |
|
|
|
|
|
let markAsDirty: unit => unit; |
|
|
|
|
|
let beginChanges: unit => unit; |
|
|
|
|
|
let mountChild: |
|
|
(~parent: hostView, ~child: hostView, ~position: int) => unit; |
|
|
let unmountChild: (~parent: hostView, ~child: hostView) => unit; |
|
|
let remountChild: |
|
|
(~parent: hostView, ~child: hostView, ~position: int) => unit; |
|
|
|
|
|
let commitChanges: unit => unit; |
|
|
}; |
|
|
``` |
|
|
|
|
|
This means that we can create multiple implementations with different types, (macOS, iOS, Linux, Android, Windows, etc). |
|
|
|
|
|
#### Native Element (Third party elements|modules|components) |
|
|
|
|
|
This is a brilliant idea given that we can define new elements that have not been implemented (custom calendar view created in Cocoa, iOS Image compoonent with extra features, etc). |
|
|
|
|
|
```reason |
|
|
type nativeElement('state, 'action) = { |
|
|
make: unit => Implementation.hostView, |
|
|
updateInstance: (self('state, 'action), Implementation.hostView) => unit, |
|
|
shouldReconfigureInstance: (~oldState: 'state, ~newState: 'state) => bool, |
|
|
children: reactElement, |
|
|
} |
|
|
``` |
|
|
|
|
|
We can define a nativeElement almost like we define a statless,stateful,reducerComponent |
|
|
|
|
|
```reason |
|
|
module View = { |
|
|
let component = statelessNativeComponent("NSView"); |
|
|
let make = (~layout, ~style, children) => { |
|
|
...component, |
|
|
render: _ => { |
|
|
make: () => { |
|
|
let view = NSView.make(); |
|
|
{view, layoutNode: makeLayoutNode(~layout, view)}; |
|
|
}, |
|
|
shouldReconfigureInstance: (~oldState as _, ~newState as _) => true, |
|
|
updateInstance: (_self, {view}) => { |
|
|
let {red, green, blue, alpha} = style.backgroundColor; |
|
|
NSView.setBackgroundColor(view, red, green, blue, alpha); |
|
|
NSView.setBorderWidth(view, style.borderWidth); |
|
|
let {red, green, blue, alpha} = style.borderColor; |
|
|
NSView.setBorderColor(view, red, green, blue, alpha); |
|
|
}, |
|
|
children, |
|
|
}, |
|
|
}; |
|
|
let createElement = (~layout, ~style, ~children, ()) => |
|
|
element(make(~layout, ~style, listToElement(children))); |
|
|
}; |
|
|
``` |
|
|
|
|
|
More components in this [file](https://github.com/briskml/brisk/blob/master/renderer_cocoa/lib/React_Components.re) |
|
|
|
|
|
#### Improved UpdateLog |
|
|
|
|
|
This implementation improves the idea of the UpdateLog in ReactMini with all the same benefits, but improving [the operations](https://github.com/briskml/brisk/blob/master/core/lib/ReactCore_Internal.re#L252) that can be executed. |
|
|
|
|
|
```reason |
|
|
type subtreeChangeReact = [ |
|
|
| `Nested |
|
|
| `NoChange |
|
|
| `Reordered |
|
|
| `PrependElement(renderedElement) |
|
|
| `ReplaceElements(renderedElement, renderedElement) |
|
|
]; |
|
|
``` |
|
|
|
|
|
# [Revery](https://github.com/revery-ui/reason-reactify) |
|
|
|
|
|
This implementation stands out for having Functional Components and Hooks as well as context that i have not seen in any other implementation. |
|
|
|
|
|
#### Functional Components and Hooks |
|
|
|
|
|
functional components are a very practical and simpler than ReasonReact components. |
|
|
|
|
|
```reason |
|
|
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)) |
|
|
); |
|
|
``` |
|
|
|
|
|
The use of hooks make them as powerful as their Composite components (ReasonReact) counterpart. |
|
|
Algebraic effects (not yet) and [Linear Types](https://gist.github.com/cristianoc/cef37bcfc0446da482da4723dc3319a8) makes them a novelty as well (still in research). |
|
|
|
|
|
With the help of a ppx hooks can be safely typed and the compiler will take care of us using hooks properly. |
|
|
 |
|
|
|
|
|
Another excelent helper ppx can help with the creation of functional components in an elegant way. |
|
|
 |
|
|
|
|
|
#### Containers |
|
|
|
|
|
A great part of this reconciler, this will allow us to hook into the reconciler lifecycle. |
|
|
|
|
|
```reason |
|
|
/* |
|
|
Container API |
|
|
*/ |
|
|
type reconcileNotification = node => unit; |
|
|
let createContainer: |
|
|
( |
|
|
~onBeginReconcile: reconcileNotification=?, |
|
|
~onEndReconcile: reconcileNotification=?, |
|
|
node |
|
|
) => |
|
|
t; |
|
|
let updateContainer: (t, component) => unit; |
|
|
``` |
|
|
|
|
|
This will allow us to make the container aware about hot code reloading preserving the instance while re rendering the whole tree with our latests components using (Dynlink or Parcel). |
|
|
|
|
|
#### Context |
|
|
|
|
|
A great way to use context to have the same benfits as Reactjs context creation. |
|
|
|
|
|
`let ctx = useContext(testContext);` |
|
|
|
|
|
This context type is opaque, so any usage should be via an useContext instead of destructing the context, we still need to find a way of how this context could be use in a composite component (ReasonReact style). If you have any ideas around it please let me know! |
|
|
|
|
|
# [Pure](https://github.com/lpalmes/pure) |
|
|
|
|
|
This project is an implementation of a reconciler a la fiber in Reason. |
|
|
|
|
|
This has some benefits considering that we can use a lot of the fiber features, but it comes at the cost of mutations. |
|
|
|
|
|
#### Fiber reconciler |
|
|
|
|
|
```reason |
|
|
type fiber('state) = { |
|
|
tag: fiberTag, |
|
|
fiberType: option(Pure.Types.pureElement), |
|
|
parent: option(opaqueFiber), |
|
|
mutable state: option('state), |
|
|
mutable child: option(opaqueFiber), |
|
|
mutable sibling: option(opaqueFiber), |
|
|
alternate: option(opaqueFiber), |
|
|
mutable effectTag: option(effectTag), |
|
|
mutable stateNode: option(Config.hostNode), |
|
|
mutable effects: list(opaqueFiber), |
|
|
} |
|
|
``` |
|
|
|
|
|
This uses a fiber record which has features as a list of efffects (ReactMini's UpdateLog), and a list of fibers that can be interrupted and resumed at any moment. This is a nice benefit but given how the ocaml multicore project is advancing we can replace most of this behaviour, in a type safe manner without reimplementing the runtime. While i don't see right now how the ReactMini implementation would be able to pause and resume work i know that multiple cores can outweight this feature (Main thread execution of effects and background threads doing reconcilation). |
|
|
|
|
|
#### Reconciler composition |
|
|
|
|
|
This [module](https://github.com/lpalmes/pure/blob/master/reconciler_plus_layout/reconciler_plus_layout.re) takes care of creating a parllel tree of Flexbox nodes. While still accepting a module to act as the normal reconciler functor. This attaches to the normal reconciler a layout that can be reused by multiple implementations to manage the layout and don't recreate layout per implementation. |
|
|
|
|
|
This is a bit hacky right now, any ideas on how to improve this are welcome! |
|
|
|
|
|
# Final thoughts |
|
|
|
|
|
While i'm really happy to have so many reconcilers/experiments and ideas all around i would love to have one reconciler to unify the project and make it stronger, battle tested and resuable by projects willing to conform to the React module that defines the components, hooks, element types, etc. In my opinion a reconciler following the steps of ReactMini/Brisk will be really great, adding support functional components, hooks, hot code reloading and native elements will make this a great reconciler to use between all projects. |
|
|
|
|
|
Any thougths or ideas please share them here! |