question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Alternative ideas for a V2

See original GitHub issue

Your V2 proposal (#35) tries to solve a very common performance problem with React and JSX in general - a problem that I’ve also been trying to solve in different ways for a while.

It made me think again about some ideas I’ve wanted to share with you for a while - I haven’t done so, because it’s probably a somewhat radical departure from React-likes.

There are two problems, really - let me start with the simplest problem.

Problem with hooks

The concept of hooks is partially responsible for some of the performance problems - mainly, hooks (by design) ignore the fact that components actually have at least a two-step life-cycle (initialization and rendering) and often a three-step life-cycle. (destruction.)

That is, things like initialization of state gets crammed into the render function, and get executed every time - for example:

function Component() {
  const [state, setState] = useState([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]);
  return (
    // ...
  );
}

Because state initialization is crammed into a single function, that function will construct a 15-element array at every render - but it won’t actually use it after the first render, it just gets ignored and thrown away.

This can be quite weird and surprising - if you’re used to reading JavaScript, and you’re just learning about hooks, this is “magic” - everything we’ve been taught not to do, with a design that completely relies on side-effects.

Do you know ivi?

Without going too much into detail, ivi enables the same kind of functional composition as React hooks, without relying on magic - the core difference between ivi and React components is the component signature:

               (props) => VDOM      <- React
(component) => (props) => VDOM      <- ivi

As you can see, ivi adds an initialization function wrapping the render-function itself. This is not only more efficient, it’s also easier to understand - the component instance isn’t a hidden internal detail, the render function doesn’t rely on side-effects and doesn’t ignore (or repeat) state (or effect) initializations after the first render. It works just like regular JS - no surprises, and you don’t need to learn any rules of hooks or use a linter to tell you about all the pitfalls and exemptions from regular JS.

This has other benefits, like avoiding closure regeneration and unnecessary caching/updates/removals of event-handlers - your const onClick=() => {} function evaluates only once during the initialization phase, and remains constant during updates, meaning the reconciler can tell that lastHandler === newHandler when diffing during updates and avoid extra calls to removeEventHandler and addEventHandler just to reattach a new instance of the exact same function.

I did manage to implement a prototype of a similar pattern in React - look at the examples in the second half of the code, you can see examples of most of what I just explained here.

Hooks are great, because they allow composition of behavior - but I think we should be able to achieve that without resorting to magic, sacrificing on performance, or requiring a linter or documentation to teach users about caveats that don’t apply to normal JS.

Adding a wrapper function to the API creates a convenient and natural scope for the initialization of the component instance, which I feel is very much lacking with React hooks.

On to the second problem.

Problem with control of render boundaries

You highlighted this problem in your V2 proposal: render boundaries in React (and inherently with JSX in general) are strictly equal to component boundaries - Babel’s React transformation doesn’t enable an engine to implement (for example) memoization of individual nodes, as these are just expressions, and there’s no way to know which expressions depend on specific properties of the props and state objects. (Side note: the JSX spec does not prescribe what the compiled output should look like - the normal transformation of JSX to JS is defined by React itself, not by the JSX standard. I’ll return to this point later.)

This is particularly problematic if you use e.g. Redux or context and useReducer as shown in this article and many others recently - what they largely ignore, is the fact that, if your root component depends on the application state, then any update affecting that state will update the entire UI. For example, a single character being typed into an input on a form, might well cause an update of the entire UI.

And then, yes, of course there are ways to optimize with shouldComponentUpdate or useMemo, etc. - but the frameworks default to poor performance in these scenarios: making a seemingly insignificant change to a large program can have surprising and drastic performance implications.

Frameworks such as Svelte (and Marko, Imba, probably others) get around this problem by inventing new syntax and using a compiler and static analysis to learn, ahead of run-time, which names (from props/state) are required to update each individual node in the tree: render boundaries aren’t equal to component boundaries; updates can happen anywhere in the DOM when the dependencies of expressions used in specific nodes actually change. It can differentiate nodes that can change from nodes that can’t - so nodes with no expressions never need to update, and so on.

The idea of compiling React components ahead of time was even discussed here, and I think someone even implemented a prototype of a compiler, though I couldn’t find that link.

I attempted to solve this problem myself with this state container prototype, which uses extra component instances to establish render boundaries - try clicking around, and watch the red render counters… as you can see, the root application never updates, only the exact nodes affected by a specific state change get updated, so updates can be as small as a value on a single input or a single text-node.

This is implemented by maintaining subscriptions at every possible render-boundary, which isn’t very efficient - it requires a lot of overhead from subscription management and the extra component instance at every possible render-boundary. Obviously, what e.g. Svelte does is much more efficient and much more user-friendly, and this was just a proof-of-concept attempting to solve the problem with JSX at run-time.

What am I getting at? 😏

I’d really like to see a framework that solves both of these problems without inventing another custom syntax.

As I mentioned earlier, the JSX specification does not in fact prescribe what the compiled output of a JSX expression should look like - just that we’re used to the standard React transformation where the output is a single expression with inline JS expressions, which inherently get evaluated when the JSX expression itself gets evaluated.

What if we create a different JSX transformation?

One that:

  • emits something more closely resembling an AST.
  • emits enough information to make it possible to compute an update from any individual node.
  • emits expressions with dependencies somehow available to evaluate on demand, e.g. via functions rather than inline expressions.
  • assumes an initialization step, separate from rendering.

So for example:

/** @jsx h */

import { h, createComponent, render } from "fre";

const App = createComponent(component => {
  let list = [1,2,3];
  let count = 0;

  function setCount(value) {
    count = value;
  }

  component.useEffect(() => {
    console.log("effect");
    
    return () => console.log("clean up");
  });
  
  return (
    <div>
      <ul>
        {list.map(item => (
          <li>{item}</li>
        ))}
      </ul>
      <h1>{count}</h1>
      <button onClick={()=>setCount(count+1)}>add count</button>
    </div>
  );
});

render(<App/>, document.body);

With the React JSX tranform, this example is meaningless - the compiled output wouldn’t work. For example, setting count = value would have no effect.

I don’t have a complete idea yet, but imagine a different JSX transform - one that sees createComponent being imported from fre, and transforms the AST of the function being passed in a call to that function.

This would be able to see that e.g. {count} refers to a variable in the parent scope, and therefore emits more than just the literal JS - for example:

  • statements like let count = 0 emit some sort of container for that state value, e.g. let $count = fre.createContainer(0).
  • assignments like count = value emit some sort of container update statement, e.g. $count.set(value).
  • JSX expressions such as {count} emit some kind of function that closes around the $count container, e.g. () => $count.value.

I don’t know yet precisely what the compiled JSX nodes would look like, but likely these would be functions of some sort as well, and the emitted containers and node update functions would depend on each other somehow.

I know this is very long and a very loose idea - and possibly too far removed from current fre to even make sense as a version 2 - but what do you think?

I have seen talk of compiling JSX, but I haven’t seen anyone doing it yet? 🤔

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

4reactions
fantasticsoulcommented, Dec 1, 2019

sorry for disturbing your discussion , I just want to let you know a cute idea…

3reactions
fantasticsoulcommented, Dec 1, 2019

hi, my friend, my name is fantasticsoul, I am the author of Concent, the problem you point out above: things like initialization of state gets crammed into the render function, and get executed every time , has been solved perfectly by Concent’s setup feature.

here is online demo: js ver https://codesandbox.io/s/concent-guide-xvcej ts ver https://codesandbox.io/s/concent-guide-ts-zrxd5

let us open our imagination, we can treat hooks as a special portal in react, it offer us amazing features like define state, define effect and etc.

So Concent use hook ability to create setup feature, now you can define component like this:

import { registerHookComp, useConcent } from "concent";

const iState = ()=> ({
  visible: false,
  activeKeys: [],
  name: '',
});

// setup will only been executed before component instance first rendering
const setup = ctx => {
  ctx.on("openMenu", (eventParam) => { /** code here */ });
  ctx.computed("visible", (newVal, oldVal) => { /** code here */ });
  ctx.watch("visible", (newVal, oldVal) => { /** code here */ });
  ctx.effect( () => { 
     /** code here */ 
     return ()=>console.log('clean up');
   }, []);
   // if visible or name changed, this effect callback will been triggered!
   ctx.effect( () => { /** code here */ }, ['visible', 'name']);
   ctx.effect( () => { /** will been triggered in every render period */ });
   // second param[depStateKeys] pass null means effect cb will been executed after every render
   // third param[immediate] pass false means let Concent ignore it after first render
   ctx.effect( () => { /** mock componentDidUpdate */ }, null, false);
  
  const doFoo = param =>  ctx.dispatch('doFoo', param);
  const doBar = param =>  ctx.dispatch('doBar', param);
  const emitSomething =() =>  ctx.emit('emitSomething', param);
  const syncName = ctx.sync('name');
  
  return { doFoo, doBar, syncName, emitSomething };
};

const render = ctx => {
  const {state, settings} = ctx;

  return (
    <div className="ccMenu">
      <input value={state.name} onChange={settings.syncName} />
      <button onClick={settings.doFoo}>doFoo</button>
      <button onClick={settings.doBar}>doBar</button>
    </div>
  );
};

export default registerHookComp({
  state: iState, 
  setup,  
  module:'foo',
  render
});

// this export is equal as code below:
export React.memo(function(props){
  const ctx = useConcent({
      state: iState, 
      setup,  
      module:'foo',
  });
 
  const {state, settings} = ctx;
  // return your ui
})

to know more details you can check the online demo, with setup:

  • the class component and function component can share the business logic code elegantly!!!

  • no effect definition or state definition in every render time any more

Read more comments on GitHub >

github_iconTop Results From Across the Web

NEW DALL-E 2 AI Alternatives that you can try right ... - YouTube
If you guys want an invite to Midjourney, Comment below your prompt ideas and I will select my favorite five! (You need discord...
Read more >
The Top 2-Year Anniversary Gift Ideas of 2022 - The Knot
Collage of four 2nd anniversary gift ideas ... We've also gathered some of our favorite alternative gifts for the second anniversary.
Read more >
Alternative Class-Specific Battle Goals V2 - Description in the ...
I've completed most goals for every class in the game, plus Jaws classes, but I've only posted spoiler-free classes here. A lot of...
Read more >
5 alternatives to CAPTCHA that won't baffle or frustrate users
5 alternatives to CAPTCHA that won't baffle or frustrate users · 5. Gamification · 4. Simple questions · 3. Slider · 2. Checkbox...
Read more >
How To Celebrate Your Child's Birthday Without A Party
These birthday party alternatives are fun for kids and stress free for ... what to do instead of having a birthday party, then...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found