Preact: lifecycle events as attributes?

Created on 24 Aug 2016  路  15Comments  路  Source: preactjs/preact

I've been thinking about this a bit lately. The only reason you would ever need a class-based component (with a redux-style architecture) is if you need the lifecycle events.

The lifecycle events seem awfully similar to the onClick, onMouseDown event handlers, so I'm wondering if it would be possible to include those events like onMount as attributes on the vnodes?

Maybe they would only work on the component level, but with mutation observers, I think they could work on any element. Here's some repos I've used in the past for the mutation events:

discussion

Most helpful comment

for what it's worth, this is what I settled on for functional components:

  return function mounts (node) {
    let attrs = node.attributes
    if (!attrs) return
    else if (!attrs.onMount && !attrs.onUnmount) return

    // create a mount using the ref
    let ref = node.attributes && node.attributes.ref

    node.attributes.ref = (el, send) => {
      const parent = el && el.parentNode

      if (attrs.onMount && el && !parent) {
        // when initially mounted, el will
        // exist but there won't be a parent.
        // we want to defer to ensure that
        // the dimensions have been calculated
        defer(function () { attrs.onMount(el, send) })
      } else if (attrs.onUnmount && !el && !parent) {
        // when unmounting, both el and
        // the parent will be null.
        defer(function () { attrs.onUnmount(null, send) })
      }

      // call original ref, if there is one
      ref && ref(el, send)
    }
  }

The problem I ran into with the previous approaches in the functional case was that inst would get reset and onMount would get called every re-render. By checking the actual element, it will only get called when the element is added and removed.

UPDATE: You'll also need to add a check for the server. Here's the complete example: https://github.com/matthewmueller/vcom/blob/2cbffdc7fe4ac2e10c9527c3cb9a0b4541333c7e/index.js#L179-L211

All 15 comments

Hey Matt,

Been thinking about this myself a bit too! It's probably not worth noting that the ref prop can be used as a mount lifecycle hook (though obviously the signature is slightly different). The timing semantics are pretty much a perfect match :o

Just thought up an implementation that works using a standard vnode hook:

import { options } from 'preact';

let hook = options.vnode;
options.vnode = vnode => {
  let attr = vnode.attributes;
  if (typeof vnode.nodeName==='string' && attr && (attr.onMount || attr.onUnmount)) {
    attr.ref = createMountRef(attr.ref, attr.onMount, attr.onUnmount);
  }
  hook && hook(vnode);
};

function createMountRef(oldRef, onMount, onUnmount) {
  let prev;
  return c => {
    if (prev && prev!==c && onUnmount) onUnmount(prev);
    if ((prev=c) && onMount) onMount(c);
    if (oldRef) oldRef(c);
  };
}

It would invoke onMount and onUnmount with the mounted/unmounted element or component instance:

const Foo = () => (
  <div>
    <div
      onMount={ div => console.log('mounted: ', div) }
      onUnmount={ div => console.log('un-mounted: ', div) }
    />
    <Bar
      onMount={ bar => console.log('mounted: ', bar) }
      onUnmount={ bar => console.log('un-mounted: ', bar) }
    />
  </div>
);

render(<Foo />);

The nicest part about this is that there is no observers or DOM stuff needed. Preact just invokes the ref hooks when adding and removing elements/components, so we can hijack that for the purpose of lifecycle events.

Even more interesting: if you pass onMount / onUnmount to a Pure Functional Component, it still functions perfectly, and your lifecycle handlers are given a reference to the created DOM element (even for deeply nested high order functional components!).

@matthewmueller also: I wonder if it would be better to just steal the corresponding lifecycle method names from the Web Components spec?

<div
  createdCallback={ () => console.log('instantiated') }
  attachedCallback={ () => console.log('mounted') }
  detachedCallback={ () => console.log('unmounted') }
/>

Then we could really start to look into making Preact a mechanism for building and shipping Web Components...

hahaha awesome! i literally was writing something like this, walking the vnodes:

gah, i think i understand the options.vnode thing now too, so it's called by core preact before preact renders to dom or to string? if i'm understanding that properly, i think that could clean up this: render(mountables(effects(styles(App({ url }), css))), document.body)

this is so great cause you can also do things like, apply _all_ styling right before rendering and insert your redux dispatchers in all event handers :-D

couldn't have said it better myself ;)
options.vnode is crazy, and maybe scary, but also insanely powerful. It gives you absolute control over every virtual dom node preact creates.

ahh cool yah, those names are pretty goofy, but i definitely think it's better to use a standard name vs something arbitrary.

Here's a HOC that implements the above (no classes still!). I am actually wondering if this is the cleanest, since it's basically standalone.

It's kinda crazy to me that so much ends up being possible with pure functional components. They are like a weird bypass technique that ends up being able to incorporate literally every feature of classful components except state... thinking this lifecycle HOC/hook could be killer with preact-cycle...

import { cloneElement } from 'preact';

const On = ({ create, mount, unmount, children }) => {
  if (create) create();  // this one might be silly actually
  let inst;   // tracks current element/component
  return cloneElement(children[0], {
    // we just render the child (HOC style) but override `ref`:
    ref(c) {
      if (inst && unmount) unmount(inst);
      if ((inst=c) && mount) mount(inst);
    }
  });
};

render(
  <On mount={()=>log('mounted')} unmount={()=>log('unmounted')}>
    <div>I get passed to mount/unmount!</div>
  </On>
);

ahh sweet, i wonder if that would muck with inputs at all? i don't really use the ref stuff that much anyway though.

so it's standalone if they're composed functions that walk & transform the vnodes separately

function clickable (vnode) {
  walk(vnode, function (node) {
    if (node.attributes && node.attributes.onClick) {
      let original = node.attributes.onClick
      node.attributes.onClick = function (e) {
        original(e, (l) => console.log('here!', l))
      }
    }
  })
  return vnode
}

function walk (vnode, fn) {
  if (!vnode.children) return fn(vnode)
  for (let child of vnode.children) {
    walk(child, fn)
  }
  fn(vnode)
}

one thing to consider that is probably not a big deal with the options.vnode thing, is that it's a singleton affecting all instances of render. i think the most flexible is either the HOC or something like this:

const transform = compose(mountables(), effects(), styles(css))
render(transform(App({ url })), document.body)

Yep - the singleton thing is for sure the drawback. It's possible to use a global flag to enable your hook only during a render (since render itself is synchronous). I'm definitely liking the HOC, it seems pretty clean. Shouldn't be any trouble with inputs that I'm aware of.

I like the separation you're able to achieve with the transform() step, just thinking it'd be able to support subtree re-rendering if done via the HOC or hook.

ahh i haven't gotten that far yet haha, yah that's a big problem. definitely liking the HOC approach the most then!

the concern i have with the ref is if that attribute already exists on that div.

edit: oh i think you could just return the original value of the ref at the end

insane thing to try: replace a vnode with a vnode that contains that vnode 馃槇

@matthewmueller yeah, you can reach into the original props and invoke it:

const On = ({ mount, unmount, children:[child] }) => {
  let inst, oldRef = child.attributes && child.attributes.ref;
  return cloneElement(child, {
    ref(c) {
      if (inst && unmount) unmount(inst);
      if ((inst=c) && mount) mount(inst);
      if (oldRef) oldRef(c);
    }
  });
};

awesomeee. so i think this is best left outside of core, despite it being pretty great. i also sort of think that the class stuff could actually be moved to preact-compat, given that you can do everything in a function that you can do in the class variant :-P

closing! unless you'd like to keep it open for further discussion :-)

for what it's worth, this is what I settled on for functional components:

  return function mounts (node) {
    let attrs = node.attributes
    if (!attrs) return
    else if (!attrs.onMount && !attrs.onUnmount) return

    // create a mount using the ref
    let ref = node.attributes && node.attributes.ref

    node.attributes.ref = (el, send) => {
      const parent = el && el.parentNode

      if (attrs.onMount && el && !parent) {
        // when initially mounted, el will
        // exist but there won't be a parent.
        // we want to defer to ensure that
        // the dimensions have been calculated
        defer(function () { attrs.onMount(el, send) })
      } else if (attrs.onUnmount && !el && !parent) {
        // when unmounting, both el and
        // the parent will be null.
        defer(function () { attrs.onUnmount(null, send) })
      }

      // call original ref, if there is one
      ref && ref(el, send)
    }
  }

The problem I ran into with the previous approaches in the functional case was that inst would get reset and onMount would get called every re-render. By checking the actual element, it will only get called when the element is added and removed.

UPDATE: You'll also need to add a check for the server. Here's the complete example: https://github.com/matthewmueller/vcom/blob/2cbffdc7fe4ac2e10c9527c3cb9a0b4541333c7e/index.js#L179-L211

Just thought up an implementation that works using a standard vnode hook:

import { options } from 'preact';

let hook = options.vnode;
options.vnode = vnode => {
  let attr = vnode.attributes;
  if (typeof vnode.nodeName==='string' && attr && (attr.onMount || attr.onUnmount)) {
    attr.ref = createMountRef(attr.ref, attr.onMount, attr.onUnmount);
  }
  hook && hook(vnode);
};

function createMountRef(oldRef, onMount, onUnmount) {
  let prev;
  return c => {
    if (prev && prev!==c && onUnmount) onUnmount(prev);
    if ((prev=c) && onMount) onMount(c);
    if (oldRef) oldRef(c);
  };
}

It would invoke onMount and onUnmount with the mounted/unmounted element or component instance:

const Foo = () => (
  <div>
    <div
      onMount={ div => console.log('mounted: ', div) }
      onUnmount={ div => console.log('un-mounted: ', div) }
    />
    <Bar
      onMount={ bar => console.log('mounted: ', bar) }
      onUnmount={ bar => console.log('un-mounted: ', bar) }
    />
  </div>
);

render(<Foo />);

The nicest part about this is that there is no observers or DOM stuff needed. Preact just invokes the ref hooks when adding and removing elements/components, so we can hijack that for the purpose of lifecycle events.

Even more interesting: if you pass onMount / onUnmount to a Pure Functional Component, it still functions perfectly, and your lifecycle handlers are given a reference to the created DOM element (even for deeply nested high order functional components!).

Just thought up an implementation that works using a standard vnode hook:

import { options } from 'preact';

let hook = options.vnode;
options.vnode = vnode => {
  let attr = vnode.attributes;
  if (typeof vnode.nodeName==='string' && attr && (attr.onMount || attr.onUnmount)) {
    attr.ref = createMountRef(attr.ref, attr.onMount, attr.onUnmount);
  }
  hook && hook(vnode);
};

function createMountRef(oldRef, onMount, onUnmount) {
  let prev;
  return c => {
    if (prev && prev!==c && onUnmount) onUnmount(prev);
    if ((prev=c) && onMount) onMount(c);
    if (oldRef) oldRef(c);
  };
}

It would invoke onMount and onUnmount with the mounted/unmounted element or component instance:

const Foo = () => (
  <div>
    <div
      onMount={ div => console.log('mounted: ', div) }
      onUnmount={ div => console.log('un-mounted: ', div) }
    />
    <Bar
      onMount={ bar => console.log('mounted: ', bar) }
      onUnmount={ bar => console.log('un-mounted: ', bar) }
    />
  </div>
);

render(<Foo />);

The nicest part about this is that there is no observers or DOM stuff needed. Preact just invokes the ref hooks when adding and removing elements/components, so we can hijack that for the purpose of lifecycle events.

Even more interesting: if you pass onMount / onUnmount to a Pure Functional Component, it still functions perfectly, and your lifecycle handlers are given a reference to the created DOM element (even for deeply nested high order functional components!).

Hi @developit , i am using below code in our project:-
import { options } from 'preact';

let hook = options.vnode;
options.vnode = vnode => {
let attr = vnode.attributes;
if (typeof vnode.nodeName==='string' && attr && (attr.onMount || attr.onUnmount)) {
attr.ref = createMountRef(attr.ref, attr.onMount, attr.onUnmount);
}
hook && hook(vnode);
};

When i check the options.js in Preact library, there is only export default { //some comments }. Now we have decided to move from preact to react, but there is no options.js in react library so the let hook = options.vnode; fails with an error saying Cannot read property 'vnode' of undefined. Any suggestion how i can come over this problem?? Any alternative to options.vnode?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kossnocorp picture kossnocorp  路  3Comments

matthewmueller picture matthewmueller  路  3Comments

k15a picture k15a  路  3Comments

paulkatich picture paulkatich  路  3Comments

jasongerbes picture jasongerbes  路  3Comments