ryansolid / babel-plugin-jsx-dom-expressions

A JSX to DOM plugin that wraps expressions for fine grained change detection
MIT License
60 stars 10 forks source link

Question about dynamic values #13

Closed teevik closed 5 years ago

teevik commented 5 years ago

Hey, first of all i just wanted to say i absolutely love this library ❤ It honestly seems like the future of frontend development.

I had two questions about dynamic values:

1

My main issue with the JSX syntax is the need for parenthesises around dynamic expressions. I tried making a version where it treats every expression as a dynamic one, but i'm guessing you would get terrible performance from doing this? I made an example here: https://codesandbox.io/s/mobx-counter-nb87z

2

This is kind of a continuation on the first question. Would it be possible to make a implementation which doesn't rely on proxies for reactivity, but rather use some structure like BehaviorSubject from rxjs? (Would probably be better suited using an state atom though, for example like in https://github.com/grammarly/focal)

Then a component would look something like this, and no parenthesises are needed since it can check count instanceof BehaviorSubject to find out if it's dynamic

const App = () => {
  const count = new BehaviorSubject(0)

  const timer = setInterval(() => count.next(count.value + 1), 1000);
  onCleanup(() => clearInterval(timer));

  return <div>{count}</div>;
};
ryansolid commented 5 years ago

These are great questions. And very related as you pointed out. Throughout all these it is important to separate the fined grained change detection library (MobX, Knockout, Solid) and this rendering mechanism.

The cost of wrapping everything isn't that bad. You'd end up in Svelte/Preact performance territory, but adding this was very intentional since the cost of computations. For most Fine Grained change detection libraries this is a flat cost. Solid actually detects if computations have tracked children and can basically discard the computation (recycle it for the next dynamic binding) so the cost is a lot less. However Solid's proxy state doesn't even make the data trackable if it is not accessed inside a computation. So choosing to not wrap a computation even reduces memory. It actually is a double saving in performance.

To be fair this library doesn't depend on ES proxies for reactivity. It's MobX or Solid that can use proxies. The change library used here does not need to. Although if you mean proxies in the general sense (intercepting access/sets) then I suppose that's true. I actually started with RxJS when I started Solid since I liked the idea of a generalizable observables especially with the TC-39 looking at standardizing on it. However it is not a great match here as a full solution. You basically want BehaviorSubject at all readable points. So might start with a BehaviorSubject transform it and then have a BehaviorSubject on the end to binding. Alternatively bindings become verbose since you have to do all resolution combineLatest explicitly so you end up only binding the output. Rx Observables are also cold, and unicast. So while there are ways around that generally each binding to a stream would create a new producer, like add a new event listener.

I think a slightly more complicated scenario hits these issues uses almost immediately. Like doing a Todo list. Like here was an attempt from a few years ago: (https://github.com/xialvjun/rx-domh). Here is there JS Frameworks Benchmark entry (https://github.com/ryansolid/js-framework-benchmark/blob/solid-variations/frameworks/keyed/rx-domh-rxjs/src/main.js). I actually took this a bit further than this reducing the syntax further and the ability to write a dynamic expression in the view was greatly missed. Like taking 2 pieces of information firstName + lastName required piping. Now I suppose with compilation it might be possible to mitigate some of this but it just isn't an area I've spent time in. It is definitely more complicated.

The truth I think is Rx best fits on more course grained data transformations. It is never as clean in the view layer unless you use it to feed into a full render function. Virtual DOM then handling the diffing makes a lot of sense, ala say Cycle.js. I still have a lot of love for Rx and I think it fits into the full solution but more as the store technology transformation. I view Solid's proxy state as a basically a data state tree that triggers fine grained updates to the DOM. It is sort of a hub of many fine grained signals, that can trigger other streams or be the output of other streams. A way of combining the data before doing it in the DOM to make the DOM updates performant and clean. RxJS role is to feed into that tree.

I added the Proxies in Solid to begin with so that I could seamlessly handle the data reconciliation outside of the DOM so that libraries like Redux, Apollo, RxJS would work well with it. Most fine grained libraries cannot handle diffing very well since they are fine grained in nature. Diffing is not even a concept for them, but I knew for Solid to work coherently in a modern JS world it was essential (plus it let me score well in all the benchmarks designed to punish this approach that showed up back in the 2014-15 time period).

teevik commented 5 years ago

I actually took this a bit further than this reducing the syntax further and the ability to write a dynamic expression in the view was greatly missed. Like taking 2 pieces of information firstName + lastName required piping.

Oh yeah, the syntax is probably always going to be more complicated, but I think I would still like it over the "magic" you get when using something like mobx

When using something like https://github.com/grammarly/focal (where Atom extends BehaviorSubject) you can write it like

const firstName$ = Atom.create("John")
const lastName$ = Atom.create("Doe")

const fullName$ = Atom.combine(
  firstName$,
  lastName$,
  ([firstName, lastName]) => firstName + " " + lastName
)

return <p>{fullName$}</p>

or

const person$ = Atom.create({
  firstName: "John",
  lastName: "Doe"
})

const fullName$ = person$.view(({ firstName, lastName }) => firstName + " " + lastName)

return <p>{fullName$}</p>

Thanks for answering 😀