GithubHelp home page GithubHelp logo

briskml / brisk-reconciler Goto Github PK

View Code? Open in Web Editor NEW
131.0 131.0 17.0 717 KB

React.js-like reconciler implemented in OCaml/Reason

License: MIT License

C++ 0.41% OCaml 8.43% Reason 90.87% JavaScript 0.28%
brisk ocaml reason-react reasonml revery

brisk-reconciler's People

Contributors

bryphe avatar ericluap avatar et7f3 avatar glennsl avatar jchavarri avatar naartjie avatar ozanmakes avatar rauanmayemir avatar ulrikstrid avatar wokalski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

brisk-reconciler's Issues

Adopt relevant parts of React.js's test suite

We need a solid test suite. I don't think there exists a better place to take tests from than react js repo. We are probably going to need ot modify a fair share of them and discard some (both because brisk doesn't aim for feature parity with react and that we might have different semantics for some cases.)

Feature: Express single child components in the type system

(Related to the nesting-reconcilers discussion in #30, and the compositional component discussion in revery: revery-ui/revery#489) Also inspired by @jordwalke 's exploration on statically typed React trees: https://github.com/reasonml/reason-react/tree/StaticReactExperiment/explorations

There have been cases that have come up where it'd be nice to make some constraints on the tree at compile-time:

  • Single-child components - It'd be helpful to express, via the type system, that a component can only have a single child. This came up in the compositional-component discussion in revery-ui/revery#489, as it'd be useful for items like <Transform />

Based on discussion with @wokalski , this seems doable with a PPX

  • Constraints between parent <-> children - Another useful case would be to express that only certain components can be children of a specific parent. An example might be a FlexLayout widget, that can only take Row, Column as first-level children. Other examples could be a <Menu> that only allows <MenuItem /> as children, etc. Fixed in #46.

I think this may be something that could be supported via nested reconcilers - @wokalski mentioned that perhaps we could model this as a type constraint where element('a) is generic, element(menuComponent) is specific to a menu, etc.

Potential bug: Passing items from stateful component to receiving reducer

Not sure if this is a bug or not, but when passing items from one component to another one ends up with no items, despite them being available in e.g. the initialState-function.

let initialState = items => {
  Console.log(("We've got some items here:", items));
  items;
};

let%component childItem = (~items, ()) => {
  let%hook (allItems, setItems) =
    Hooks.reducer(~initialState=initialState(items), (value, _) => value);

  // Works when using the effect-hook
  /* let%hook () = */
  /*   Hooks.effect( */
  /*     OnMountAndIf((!=), items), */
  /*     () => { */
  /*       setItems(items); */

  /*       None; */
  /*     }, */
  /*   ); */

  <View>
    {allItems
     |> List.map(item => <Text style=Style.[color(Colors.red)] text=item />)
     |> React.listToElement}
  </View>;
};

let%component container = () => {
  let%hook (items, setItems) = React.Hooks.state([]);

  let%hook () =
    Hooks.effect(
      OnMount,
      () => {
        setItems(_prev => ["One", "Two", "Three"]);

        None;
      },
    );

  <childItem items />;
};

Developer Experience: Stale `set` functions from Hooks

After upgrading to latest brisk - we experienced a couple regressions in our functionality:

  • Slider control was no longer updating (#342)
  • Scroll bounce was no longer working (#346)

These occurred after the update to the immutable hooks API: revery-ui/revery@eda0fe6

The issue was a problem in our code - we had long-lived closures that held on to set functions past re-renders. For #346 , for example, a fix was to switch to a shorter-lived effect hook.

However, it wasn't very diagnosable - we just started seeing unexpected behavior when we used the set functions in this way. It'd be helpful if we could at least detect this case - that we're calling a stale set and throw a descriptive error - like how the React devtools warn.

A message like ERROR: Stale setter used for reducer hook; check that you aren't holding a setter in a long-lived function would be helpful to catch the error close to the source.

"Time Travel"

Reading through #14 made me think about this - eventually, for Revery and beyond - I'd like to be able to have a 'time travel' development experience. Like Redux, but maybe without necessitating keeping a global app state.

I was curious if it'd be possible to hold on to past rendered trees, and call executeHostViewUpdates on old versions in order to rewind the tree to an earlier state.

The 'developer experience' I'm thinking about would be a sort of slider where you could rewind to any particular point in time, and see the UI in that 'rewound' state. I'm interested in this especially in the context of debugging animations (ie, frames where we hit a discontinuity). Implementation-wise, in debug mode, we'd record 'snapshots' of the rendered tree and hold them in memory for some amount of limit.

One challenge I see is that I'm not sure how 'effects' would play into this...

The nice thing about this developer experience - if it works with Brisk - is that this DX would apply to any brisk-reconciler powered app - which would be amazing ๐Ÿ˜„

I love the idea of this 'time travel' debugging experience, but it's always been finicky to set up and use - but I hypothesize the functional nature of Reason/Brisk would at least bring this closer to being a usable reality.

Would this be doable as-is with the current reconciler infrastruture, @wokalski ? Or do you see any deltas?

let%component rejects functions with locally abstract types

let%component make = (type a, ~someProp: a, ...) =>
  ...

is rejected with the error fun expected because type a results in a Pexp_newtype, while a Pexp_fun is expected.

And because of #53 it's not possible to use a stand-alone type annotation either.

If Hook is not fired on mount

This was desirable design decision but it's both inconsistent with React.js and only causes headaches. If someone doesn't want to fire it on mount they can add a piece of state like isFirstRender which will be initially true.

Hooks API - remove the need for annotations

We're probably going to change the API for hooks a little bit. I.e. instead of annotating the hooks on the last call we're going to either:

  • Create a function like Hooks.ignore(hooks) (better name suggestions are welcome)
  • Return hooks from render along with the element like this:
      (hooks, <Component prop />)
    
    (iff we manage to switch the baking data structure to a heterogenous immutable list which will enable us to easily implement getDerivedStateFromProps and should be more performant (thanks, immutability))

Unable to use a prop called "component" with %component syntax

let%component myComponent - (~component, (), hooks) => { ... }

will expand to (approximately)

let myComponent ={
  let component =  Expert.component("myComponent");
  (~component, ()) => 
    component(hooks => { ... })
);

hence the locally defined component will be shadowed by the prop/labelled argument, resulting in a type error. The PPX should instead use an invalid identifier name, like brisk-component, so it cannot be shadowed.

Also do the same for %hook, which while it uses a more cryptic name also has a microscopic chance of being shadowed.

Static renderers

I just had a very random idea.

We could expose Brisk.Static renderers which would contain renderers for static output (i.e. Static wouldn't expose update, flushPendingUpdates, shouldUpdate, or any other update related stuff). The rationale is that I feel like it could be cool to use a react like component system to build static content like websites, md files, formatted text, etc.

`OutputTree.deleteNode`: Consider passing the child's position

The index of the child to remove would be useful for rendering to some kinds of platforms. For example, React Native bases its removals on indices: https://github.com/facebook/react-native/blob/1af390be19000b2756fc646e72f152dd383a0bb1/React/Modules/RCTUIManager.m#L845

If brisk-reconciler has this information easily available, it would be convenient to pass it along. If not, it's not a big deal. The consumer of brisk-reconciler can create a data structure to store the node positions and update it in insertNode and moveNode.

`reducer` is not executed immediately and in order of calls to `dispatch`

Calls to dispatch currently results in deferred execution of the reducer and does not guarantee that messages are processed in the order they were dispatched. This can cause subtle bugs when the reducer has side-effects that depend on ordering.

I understand the motivation for this is to have potentially blocking calls in the reducer not block the UI thread. As I understand it, the deferred execution of reducer is still run on the UI thread however, but even if it wasn't I think it would be better to have a separate mechanism for deferring side-effects and dispatching messages back to the reducer to update state. Similarly to how ReasonReact has the reducer return a variant Update | SideEffect, or how Elm has the update function return a tuple of the state and a Cmd.

I therefore propose first changing the reducer to execute immediately, but still have the rendering deferred, and then to consider a separate mechanism for async side-effects to be implemented later.

let%component ppx transforms type annotation incorrectly

Type annotations on let%component functions are transformed to apply to only the return value, not the entire function.

1. Annotation of return type

module ShouldAllowComponentProp = {
  let%component make = (~component, (), hooks): element(node) =>
    (<Div> component </Div>, hooks);
}

Transforms to:

module ShouldAllowComponentProp =
  struct
    let make =
      let brisk-component =
        Brisk_reconciler.Expert.component ~useDynamicKey:false
          "test/Components.re.ShouldAllowComponentProp.make" in
      fun ?(key= Brisk_reconciler.Key.none) ->
        fun ~component ->
          fun () ->
            brisk-component ~key
              (fun hooks ->
                 (((let brisk-component = Div.make in
                    ((brisk-component
                        ~children:(Brisk_reconciler.Expert.jsx_list
                                     [component]) ())
                      [@JSX ])), hooks) : node element))
  end

Which produces the type error:

File "test/Components.re", line 239, characters 4-35:
239 |     (<Div> component </Div>, hooks);
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type 'a * 'b
       but an expression was expected of type node element

2. Annotation of entire function type

module ShouldAllowComponentProp = {
  let%component make
    : (~key:int=?, ~component: element(node), unit) => element(node) 
    = (~component, (), hooks) =>
      (<Div> component </Div>, hooks);
}

Transforms to:

module ShouldAllowComponentProp =
  struct
    let make =
      let brisk-component =
        Brisk_reconciler.Expert.component ~useDynamicKey:false
          "test/Components.re.ShouldAllowComponentProp.make" in
      fun ?(key= Brisk_reconciler.Key.none) ->
        brisk-component ~key
          (fun ~component ->
             fun () ->
               fun hooks ->
                 ((let brisk-component = Div.make in
                   ((brisk-component
                       ~children:(Brisk_reconciler.Expert.jsx_list
                                    [component]) ())
                     [@JSX ])), hooks) : ?key:int ->
                                           component:node element ->
                                             unit -> node element)
  end

Which produces the error:

File "test/Components.re", line 240, characters 4-62:
240 |     (~component, (), hooks) => (<Div> component </Div>, hooks);
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This function should have type
         ?key:int -> component:node element -> unit -> node element
       but its first argument is labelled ~component

Switch to Rely test runner

According to @bryphe Rely alleviates some of our problems with building for windows (#2). Rely also has quite a few nice features and mostly removes the need for our own diffing which is awesome.

Warn on CI if esy.lock is not up to date

If any of the esy.lock's is not up to date we should warn.

Additionally I'm wondering if it's possible to put all the locks and package-ci-*.jsons in a ci directory and have it still work.

Hooks API - Context

Was just working on porting Oni2 over to the latest Revery + Brisk reconciler, and reminded me of the context issue. We're using context right now to provide a context-based ThemeProvider.

A potential API could look like:

  • createContext: 'a -> context('a)
  • Hooks.useContext: context('a) -> 'a
  • getProvider: context('a) -> providerComponent('a)

I believe handling the typing correctly in terms of passing the nested context down will be challenging - so we might need to alter the API to make it workable in a strongly-typed environment w/o resorting to Obj.magic.

Not a blocker, I'll workaround it, but just wanted to log an issue to start the discussion.

Automate npm releases

The update logic is already implemented in the github action but unfortunately there's some problem with authentication and this step doesn't pass. It'd be great to move it to azure pipelines together with the rest of CI.

Set up CI for windows

Certain dependencies don't build on windows like ppx_deriving which we're using for tests. Adding the platform itself to the CI pipeline is very easy (17b7977) however the build fails.

Node placed at the wrong position when using `listToElement`

The impact is that nodes can end up on screen in a different order than the developer expects.

I wrote this test to illustrate the issue:

  (
    "Test transitioning from empty list to non-empty list",
    `Quick,
    () =>
      render(
        Components.(<Div> <Box title="ImABox0" /> {listToElement([])} </Div>),
      )
      |> executeSideEffects
      |> reset
      |> update(
           Components.(
             <Div>
               <Box title="ImABox0" />
               {listToElement([<Box title="ImABox1" />])}
             </Div>
           ),
         )
      |> executeSideEffects
      |> expect(
           ~label="It appends the 2nd node",
           [
             Implementation.BeginChanges,
             UnmountChild(div, box("ImABox0")),
             MountChild(div, box("ImABox0"), 0),
             MountChild(div, box("ImABox1"), 1),
             CommitChanges,
           ],
         )
      |> ignore,
  ),

Expected: The new node ("ImABox1") is placed at position 1.

Actual: The new node ("ImABox1") is placed at position 0.

Test Output

image

Allow declaring components on one line

The current dance with:

let c = component("name");
let somethingElse = (~prop, ~prop, ()) => component(hooks => ...

is really annoying. It feels like using something very rough unpolished. We should strive to make possible the following:

let somethingElse = component((~prop, ~prop, ()) => ...)

Consider cases where the root node is a different type than the children.

It's often the case that the root is of a different type than the children elements. It happens with toolbar (i.e. toolbar is a parent and items are children), it happens with SegmentedControl. It currently requires some strange wrapping. We should have a separate function and type for the root (if a given reconciler requires it).

The set of requirements here will become clearer if we implement a reconciler for navigation which should be a stress test for the current abstraction.

Toolbar is also an interesting case where there are 3 types of elements, the window where toolbar is mounted, toolbar which mounts items and items themselves which render normal views behind the scenes. A lot going on.

Implement JSX PPX

Currently, there's additional boilerplate involved when using JSX; users have to manually implement createElement which is the function call that JSX is mapped to in Reason. Such function call is decorated with the [@JSX] attribute which allows any library to implement custom logic for the transformation.

We'd like to leverage it to remove the boilerplate. The specific implementation of this preprocessor will depend on the component creation API. A potential lightweight first class module based API like this is possible.

Another important question is if we want to use mainly upper case or lower case JSX. I am in favour of keeping the JSX as close to component definitions as possible. I.e. if we end up with the API above, I think we should have the lower case JSX. If we have a functor api for instance, we should use upper case JSX.

Render issue with the state hook

Hello !
I wrote a component like this:

let%component make = (~render, ()) => {
  let%hook (currentRoute, setCurrentRoute) =Hooks.state(Store.route^);

  let%hook () =
    Hooks.effect(
      OnMount,
      () => {
        Store.subscribe((_, newRoute) => setCurrentRoute(_ => newRoute));
        None;
      },
    );

  <View> {render(state.currentRoute)} </View>;
};

It's supposed to rerender every time the route update. It works on the first update of my route, and then the component doesn't update at all. I added some log inside the setCurrentRoute, it is triggered, but component still not rerender.

Someone suggested that I make it with a reducer and it worked perfectly:

type action =
  | Route(string);

type state = {currentRoute: string};

let reducer = (action, state) => {
  switch (action) {
  | Route(route) => {currentRoute: route}
  };
};

let%component make = (~render, ()) => {
  let%hook (state, dispatch) =
    Hooks.reducer(~initialState={currentRoute: Store.route^}, reducer);

  let%hook () =
    Hooks.effect(
      OnMount,
      () => {
        Store.subscribe((_, newRoute) => dispatch(Route(newRoute)));
        None;
      },
    );

  <View> {render(state.currentRoute)} </View>;
};

Unable to build the lambda-term example

I got the following the error when I tried building the lambda-term example

info esy 0.6.7 (using esy.json)
info checking https://github.com/ocaml/opam-repository for updates...
info checking https://github.com/esy-ocaml/esy-opam-override for updates...
info resolving esy packages: done
info solving esy constraints: done
info resolving npm packages: done
info fetching: done
info installing: done
info building @opam/ocamlbuild@opam:0.14.0@6ac75d03
info building @opam/ocamlfind@opam:1.8.1@ff07b0f9
error: build failed with exit code: 1
  build log:
    # esy-build-package: building: @opam/ocamlfind@opam:1.8.1
    # esy-build-package: pwd: /home/ducaale/.esy/3/b/opam__s__ocamlfind-opam__c__1.8.1-d095496f
    # esy-build-package: running: 'bash' '-c' 'true'
    # esy-build-package: running: './configure' '-bindir' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/bin' '-sitelib' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/lib' '-mandir' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/man' '-config' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/lib/findlib.conf' '-no-custom' '-no-topfind'
    Welcome to findlib version 1.8.1
    Configuring core...
    Checking for #remove_directory...
    Testing threading model...
    systhread_supported: true
    Testing DLLs...
    Testing whether ppxopt can be supported...
    Checking for ocamlc -opaque...
    Configuring libraries...
    native dynlink: found
    labltk: not present
    ocamlbuild: not present
    camlp4: not present (normal since OCaml-4.02)
    compiler-libs: found
    dbm: not present (normal since OCaml-4.00)
    num: not present (normal since OCaml-4.06)
    bytes: found, installing fake library
    spacetime: found
    graphics: not found
    Configuration for dynlink written to site-lib-src/dynlink/META
    Configuration for str written to site-lib-src/str/META
    Configuration for threads written to site-lib-src/threads/META
    Configuration for unix written to site-lib-src/unix/META
    Configuration for stdlib written to site-lib-src/stdlib/META
    Configuration for bigarray written to site-lib-src/bigarray/META
    Configuration for ocamldoc written to site-lib-src/ocamldoc/META
    Configuration for compiler-libs written to site-lib-src/compiler-libs/META
    Configuration for bytes written to site-lib-src/bytes/META
    Configuration for raw_spacetime written to site-lib-src/raw_spacetime/META
    Detecting compiler arguments: (extractor built) FAILED (see the file ocargs.log for details)
    error: command failed: './configure' '-bindir' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/bin' '-sitelib' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/lib' '-mandir' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/man' '-config' '/home/ducaale/.esy/3__________________________________________________________________/s/opam__s__ocamlfind-opam__c__1.8.1-d095496f/lib/findlib.conf' '-no-custom' '-no-topfind' (exited with 1)
    esy-build-package: exiting with errors above...

  building @opam/ocamlfind@opam:1.8.1
esy: exiting due to errors above

Environment

  • esy v0.6.7
  • Ubuntu 20.04.1 on WSL2

Maintaining component state when reparenting

It is often useful to maintain the state of a component when reparenting or changing the order of elements. Currently you cannot express the former and you express the latter by using keys. The use cases of reparenting are kind of an open question. Replacing key with constant identity stored in state could be an elegant and performant alternative solution for keys. The API would not be cumbersome with Hooks.

In the example below, foo and bar would be passed the same constant identity

let foo = (~contentId as id, children) = component(hooks => {
  <view id />
});

let bar = (~contentId as id, children) = component(hooks => {
  <view id />
});

let baz = () = component(hooks => {
  let (id, hooks) = Hooks.componentId(hooks);
  (hooks, Random.bool() ? <foo contentId=id /> : <bar contentId=id />)
});

An open question is what happens when you render two views with the same component identity at the same time (which is also problematic with keys but you expect keys to break in such cases)

brisk-reconciler.ppx prevents usage of other standard JSX apis

I've noticed that the brisk-reconciler.ppx prevents using other JSX apis. I believe the ppx expects there to be a make function but the standard JSX api built in to reason uses a createElement function.

This comes up when trying to use something like Pastel.

print_endline(<Pastel color=Red> "Hello world" </Pastel>);

It also seems like the brisk reconciler ppx doesn't support list elements like normal Reason JSX:

<View> ...someListOfElements </View>

Is it possible to have the ppx translate let%component to the standard createElement form of JSX? Or is there some technical limitations around this?

Add API for nesting reconcilers

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.

Tests for duplicate keys

Currently React goes belly up if we use duplicate keys. The possible arrangements are:

// From:
<Container> <Component key=key1 /> </Container>

// To:
<Container> <Component key=key1 /> <Component key=key1 />  </Container>
// From:
<Container> <Component key=key1 /> <Component key=key1 />  </Container>

// To:
<Container> <Component key=key1 /> </Container>
// From:
<Container> <Component key=key1 /> <Component key=key1 />  </Container>

// To:
<Container> <Component key=key1 /> <Component /> </Container>
// From:
<Container> <Component key=key1 /> <Component /> </Container>

// To:
<Container> <Component key=key1 /> <Component key=key1 /> </Container>

For now, the behaviour in all those cases is undefined.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.