Preact: Allow to skip sub tree patching.

Created on 19 Apr 2017  路  12Comments  路  Source: preactjs/preact

I'm trying to use preact as rendering tool for web components custom elements.
I saw the proposal solution on https://github.com/bspaulding/preact-custom-element

But in my case it is not works properly.
First is that I do not using shadow-dom (shadowDOM is imperative and do not allow prerender custom-elements on server side, what I want to achieve).
Second is that I'm trying to mix preact app and preact base custom-elements, or same effect will be when I will nest the preact base custom-elements within preact base custom-elements.

I prepared example https://www.webpackbin.com/bins/-Ki44_xg5yie0o-jHaXv

Problem is that there is no any isolation between the parent render scope (eg root vnodes tree) and nested render scope (eg. some subbranch of vnode tree).

Custom elements should care about render cycle by itself, diff algorithm should never tray to patch the content of custom-elements (even the custom-element will not use the preact). The proposal https://github.com/bspaulding/preact-custom-element toVdom is not solving the problem and is very inefficient.

I tried locally simple patch:

https://github.com/developit/preact/blob/master/src/vdom/diff.js#L128
replaced by:

var isCE = !!window.customElements.get(vnode.nodeName);

if (!isCE) {
    // Optimization: fast-path for elements containing a single TextNode:
    if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' &&
        fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
        if (fc.nodeValue!=vchildren[0]) {
            fc.nodeValue = vchildren[0];
        }
    }
    // otherwise, if there are existing or new children, diff them:
    else if (vchildren && vchildren.length || fc!=null) {
        innerDiffNode(out, vchildren, context, mountAll, hydrating ||
            props.dangerouslySetInnerHTML!=null);
    }
} 

And after such change everything is works fine.
I know this is not something what preact team will accept :)
But maybe you can add some handler to the options like:

options.skip: (vnode, dom) 

and use it:

var skip= options.skip && options.skip(vnode, dom);

if (!skip) {
    // Optimization: fast-path for elements containing a single TextNode:
    if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' &&
        fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
        if (fc.nodeValue!=vchildren[0]) {
            fc.nodeValue = vchildren[0];
        }
    }
    // otherwise, if there are existing or new children, diff them:
    else if (vchildren && vchildren.length || fc!=null) {
        innerDiffNode(out, vchildren, context, mountAll, hydrating ||
            props.dangerouslySetInnerHTML!=null);
    }
} 

and usage for custom elements:

preact.options.skip = (vnode) => typeof vnode == 'string' && !!window.customElements.get(vnode.nodeName);

Such skip option can be used also as a solution for 'render once' when we know that some sub tree do not have to be never rerendered:

    // preact-one
   function once(vnode) {
        vnode.renderOnce = true;
        return vnode
   }

   preact.options.skip = (vnode, dom) => vnode.renderOnce && dom

and usage

    renderApp() {
         render(
           <div> 
                once(<div>Static header which will be never rerendered</div>)
                <div>Content rerendered always<div> 
           </div>, document.body);
    }

The other possibility is to add the property to vnode and instead of condition on options.skip check the property.

if (!vnode.skipChildren) {
    // Optimization: fast-path for elements containing a single TextNode:
    if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' &&
        fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
        if (fc.nodeValue!=vchildren[0]) {
            fc.nodeValue = vchildren[0];
        }
    }
    // otherwise, if there are existing or new children, diff them:
    else if (vchildren && vchildren.length || fc!=null) {
        innerDiffNode(out, vchildren, context, mountAll, hydrating ||
            props.dangerouslySetInnerHTML!=null);
    }
} 

Then options.vnode can be use to decorate vnode with such options.

As a last word, I can say that many virtual dom implementations provides possibility to 'skip' the subtree patching:
https://github.com/google/incremental-dom/blob/master/test/functional/skip.js
https://github.com/Matt-Esch/virtual-dom/blob/master/docs/thunk.md
...

Best Regards

All 12 comments

We have shouldComponentUpdate:false for this, though I admit it's not ideal since it doesn't work for static nodes. I did find a solution though, posted it on your other issue since it is a modification to preact-custom-element:
https://github.com/bspaulding/preact-custom-element/issues/6#issuecomment-295319646

Thx @developit
(Lets talk in this issue)
Solution is really interesting :)
After last my Issue, I was sure that you will answer in this way :)

Unfortunately proposed solution still is not work as expected.
Please look at https://www.webpackbin.com/bins/-Ki9r5YTAK4A4uxSjIx9

Wrapping to component and close shouldComponentUpdate to false, is also stoping rerender after props/attrs change. I know that I can check the changes in props in shouldComponentUpdate and change the flag to true if there was some changes, but such solution will rerender custom element twice, once rendering of parent will remove children, second rerender of component inside will create nodes again.

Yup yup. So I can better understand your use-case - are you wanting to keep the Custom Elements in the DOM, or are you always rendering the CE via Preact and it's always implemented via Preact? In the latter case I have some work lying around that implements CE's within VDOM, might be interesting.

Ok so goal is to support all scenarios :)

Please look at https://github.com/skatejs/skatejs this is small lib based incremental-dom.
In general this lib is a jsx-to-vdom (jsx-to-incremental-dom) + some additional abstraction over web components standard. So it is very similar to what preact/react is but it is focused on custom-elements and it is not supports server side rendering.

I want to do the same, but by using Preact as vdom+jsx-to-vdom, and do not create own new abstraction over the standard.

At the end I was thinking about solution where:

  1. I'm able to write custom elements base on standard api, where preact is used only as a rendering engine. So CE is within own (standard) live cycle calling render(..., this. this.firstChild) by self.
  2. I'm able to create whole application based on custom-elements which are made in this way.

In general I want to be able use Preact only as rendering layer.

But I see that if I will be able to achieve that goals we will be also able to create custom-elements by wrapping preact components.

@majo44 SkateJS 5.0 uses preact instead of incremental-dom.

Wow, so skate going in same direction, but probably because they are always using shadow dom they do not see this problem. (I will try to convince them to add possibility to render also without shadow dom :) This will give a possibility to render skate.js components on server side by domino)

So as I said, in case when skatejs is rendering to light DOM, the problem with patching nested content appears.

https://www.webpackbin.com/bins/-KoOzIcTUxPE7oDkLAWq

Are we able to consider any solution for this problem ?

Wow, so skate going in same direction, but probably because they are always using shadow dom they do not see this problem.

Just for posterity, we allow this now via a custom renderRoot property.

@treshugart Cool, is that already documented somewhere?

@developit
Are we able to consider to add simple vnode check in diff:

if (!vnode.skipChildren) {
    // Optimization: fast-path for elements containing a single TextNode:
    if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' &&
        fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
        if (fc.nodeValue!=vchildren[0]) {
            fc.nodeValue = vchildren[0];
        }
    }
    // otherwise, if there are existing or new children, diff them:
    else if (vchildren && vchildren.length || fc!=null) {
        innerDiffNode(out, vchildren, context, mountAll, hydrating ||
            props.dangerouslySetInnerHTML!=null);
    }
} 

This change is not a breaking change. I understand this will have some impact on performance but just a little. I was think a lot about other possible solutions but I did not found any better :(

@treshugart
On the skatejs with such change we will be able to use this change by:

let oldVnodeHook = options.vnode;
options.vnode = (vnode) => {
  if (oldVnodeHook) oldVnodeHook(vnode);
  vnode.skipChildren =
    typeof vnode.nodeName === 'string' &&
    vnode.nodeName.indexOf('-') > 0 &&
    isDefined(vnode.nodeName);
};
````
I even created test in skatejs which is pass after such changes:
customElements.define('x-child', class extends Component {
  get renderRoot () { return this;}
  renderCallback () {
    return 'Child';
  }
});

customElements.define('x-parent', class extends Component {
  static props = { 'name': {attribute: true}};
  get renderRoot () { return this;}
  name = 'Foo';
  renderCallback () {
    return vdom('div', {},
      vdom('span', {}, this.name),
      vdom('x-child', {})
    );
  }
});

let fix = fixture('<x-parent/>');

afterMutations(
  () =>  expect(fix.firstChild.firstChild.children[1].innerHTML).toBe('Child'),
  () =>  expect(fix.firstChild.firstChild.children[0].innerHTML).toBe('Foo'),
  () => fix.children[0].name = 'Boo',
  () =>  expect(fix.firstChild.firstChild.children[1].innerHTML).toBe('Child'),
  () =>  expect(fix.firstChild.firstChild.children[0].innerHTML).toBe('Boo'),
  done);

```

@jmaicher not yet. Will be before final 5.0 release.

FYI: Bailing out of rendering is now supported natively in Preact by returning the same vnode as before. This is best done via memoization via memo, useMemo or a similar approach.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

matthewmueller picture matthewmueller  路  3Comments

Zashy picture Zashy  路  3Comments

SabirAmeen picture SabirAmeen  路  3Comments

matuscongrady picture matuscongrady  路  3Comments

youngwind picture youngwind  路  3Comments