Skip to content

Instantly share code, notes, and snippets.

@lpalmes

lpalmes/react.md

Last active Sep 10, 2020
Embed
What would you like to do?
React and Reconcilers in Reason

React

This is the base of all projects and it will include the foundation for all potential react-based projects in Reason.

This base package should include a ReasonReact api to promote collaboration and familiarity with people using a ReasonReact, and for the modern world of React this should also include a Hooks api that currently revery uses.

React module

All blocks in Jsx are of type React.reactElement. This reactElement should represent:

  • Strings (React.string("Hello friend"))
  • nil/empty/null (React.nil | React.null | React.empty)
  • List/Array (React.list([...]) | React.array([|...|]))
  • ReasonReact components (let component = React.statelessComponent("Header"))
  • Functional components with Hooks (let header = () => <div>{React.string("I'm the Header")}</div>)

Both this expressions are equal:

<div>{React.string("Hello")</div>
Nested(div, [ Flat([String("Hello")]) ])

ReasonReact components (Composite components)

This component mimics the ReasonReact api, and is the closest of the primitives to a class component in React(js).

It provides multiple lifecycles:

  • didMount
  • didUpdate
  • willUnmount
  • willUpdate
  • shouldUpdate

A way to store and manipulate state:

  • initialState
  • reducer

And of course a render: self => reactElement function.

This function takes a record self (similar to this in js) that contains the state defined by the user and a send func to send actions to the reducer (a small state machine) and return a reactElement

Stateless Component

   module Greeter = {
     let component = React.statelessComponent("Greeter");

     let createElement = (~name="", ~children, ()) => {
         ...component,
         render: _self =>
         <View>
            <Text>
                {React.string("Hello " ++ name ++ "!")}
            </Text>
         </View>,
       };
   };

Reducer Component

This example was taken from the ReasonReact documentation

type state = {
  count: int,
  show: bool,
};
type action =
  | Click
  | Toggle;

let component = React.reducerComponent("Example");

let make = (~greeting, ~children, ()) => {
  initialState: () => {count: 0, show: true},
  reducer: (action, state) =>
    switch (action) {
    | Click => React.Update({...state, count: state.count + 1})
    | Toggle => React.Update({...state, show: !state.show})
    },
  render: self => {
    let message =
      "You've clicked this " ++ string_of_int(self.state.count) ++ " times(s)";
    <div>
      <button onClick=(_event => self.send(Click))>
        (React.string(message))
      </button>
      <button onClick=(_event => self.send(Toggle))>
        (React.string("Toggle greeting"))
      </button>
      (
        self.state.show
          ? React.string(greeting)
          : React.null
      )
    </div>;
  },
};

Functional Components (Hooks)

So finally we are on the bleeding edge. Let's see how this components could work starting with a stateless component.

let header = (~name, ~children, ()) => {
    print_endline("I'm hooked");
    <View>
        <Text>
            {React.string("Hello " ++ name ++ "!")}
        </Text>
    </View>
};

/* This ends up like */
<Header name="Lorenzo" />

What about some state?

let Calculator3000 = (~children, ()) => {
    let (value, setValue) = useState(0);
    <View>
        <Button onClick={_ => setValue(value + 1)}>
          {React.string("Increment")}
        </Button>
        <Text>
          {React.string("Value: " ++ string_of_int(value))}
        </Text>
        <Button onClick={_ => setValue(value - 1)}>
          {React.string("Decrement")}
        </Button>
    </View>
};

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

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 and 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.

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 computation:

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 that is produced by perfomUnitOfWork).

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 has great benefits, e.g:

Let's say we are rendering a tree of elements and we arrive at two components that their subtrees are fairly complex, this will allow us to reconcile those subTrees at the same time (in differen threads) and get two logs of updates ready to be executed by the main thread.

Remote Actions

This is a little pearl of this implementation. We can test components by executing remote actions like in this test:

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

A child of ReactMini with lots of extra benefits.

Host Implementation Functor

This is similar to a host config in react. It's defined by this module type:

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 and host components (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 defined in the library, but accept future components (custom calendar view created in Cocoa, iOS Image compoonent with extra features, etc).

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.

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

Improved UpdateLog

This implementation improves the idea of the UpdateLog in ReactMini with all the same benefits, but improving the list of operations that can be executed.

 type subtreeChangeReact = [
      | `Nested
      | `NoChange
      | `Reordered
      | `PrependElement(renderedElement)
      | `ReplaceElements(renderedElement, renderedElement)
];

Revery

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

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 records) counterpart. Algebraic effects (not yet) and Linear Types 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. Hooks ppx

Another excelent helper ppx can help with the creation of functional components in an elegant way. Functional component ppx

Containers

A great part of this reconciler. This will allow us to hook into the reconciler lifecycle.

  /*
       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 by preserving the instance while reconciling the whole tree with our latests components using (Dynlink for native or Parcel, Webpack HMR, etc).

Context

A great way to use context that has 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). If you have any ideas around it please let me know!

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

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 linked 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 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!

@wokalski

This comment has been minimized.

Copy link

@wokalski wokalski commented Jan 1, 2019

My favourite part of brisk is the nativeElement type which really gives freedom for different backends. I like the hooks api of revery too, it's really cool. Another cool thing about brisk is that we built it in such a way that it can run on two threads. It's useful in cases where the main program doing event processing is in C (think of a UI framework like Cocoa). The reconciler can run on a background thread, you can choose to feed it events asynchronously and then invoke the side effects on the main thread.

@jchavarri

This comment has been minimized.

Copy link

@jchavarri jchavarri commented Jan 1, 2019

For completion purposes, we are discussing something along these lines in revery-ui/reason-reactify#35.

Some thoughts:

Supporting two models (class-based and hooks-based model) or just one

I think that creating a shared interface between reconcilers will be very challenging on its own, let alone if we try to bake the two models React supports today: the class components, and the functional components + hooks. Both models are pretty much equivalent in their expressiveness, but very different on their API.

It seems that functional+hooks will get more momentum over time so I think I would prefer to bet just on that model for simplicity. I could be wrong though about that momentum.

Interface vs implementation

The Reason type system provides a way to define types interfaces, and we could leave to each reconciler the details on how they want to implement it. For example, the let%reve ppx is a purely implementation detail because Revery decided to use first-class modules to define components. But Brisk or Pure could settle in a totally different approach, if the "component definition" was only limited to the specification of the render function (i.e. one reconciler could wrap the function with a module, but another could wrap it with a record for example).

I tried to start defining in revery-ui/reason-reactify#35 (comment) the different parts of this interface. What would be the minimal interface that we could define so allow individual exploration in the different reconcilers, but still while allowing for a meaningful % of user-created components to be reused across the reconcilers? Does that interface even exist?

Find a balance between sharing too much vs too few

These are some of the areas where I think we could find a shared interface definition, but it's challenging to do so without restricting too much each reconciler evolution:

  • Dual models (class vs function+hooks), as mentioned above
  • Shared hooks APIs: supposing we include the function+hooks into the shared interface, maybe we could just follow ReactJS on thisto play it safe
  • Shared "fixed" props: like style or children: for any meaningful % of components being reused, we probably want to define these
  • Minimal set of shared component primitives (View, Text, Image,... what else?)
  • Minimal Layout API (part of fixed prop style): should it be included?
  • Minimal Event API
  • Fiber vs non-fiber: Does a fiber implementation have any impact on the minimal shared interface?
@lpalmes

This comment has been minimized.

Copy link
Owner Author

@lpalmes lpalmes commented Jan 1, 2019

@wokalski the nativeElement is a brilliant piece, is a great modular way to handle new components as well. And i think you are right, as you told me a few months ago, the update log can give us so much of the fiber behavior and more, so that's why i think a reconciler based on ReactMini is the way to go.

This might sound like a crazy idea but the fact that is doable totally blows my mind.
Whenever you have a button is common to have a handler that updates state and that makes changes in the UI (let's say selecting a different tab),
so whenever we detect a onHover event from the user we can work the changes needed, and when the user clicks on the button just apply those changes (the result UpdateLog from the onHover event) and swap the current instance tree, as long as we have a way to do background thread reconciling.

This use case might be a micro optimization and kind of silly, but i'm sure there are millons of great use cases here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.