Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lifecycle hooks #927

Closed
Rich-Harris opened this issue Jul 3, 2014 · 6 comments
Closed

Lifecycle hooks #927

Rich-Harris opened this issue Jul 3, 2014 · 6 comments

Comments

@Rich-Harris
Copy link
Member

At present, lifecycle hooks are a bit of a mess - partly for historical reasons, partly a design failure (mea culpa). This is what you have to work with:

MyRactive = Ractive.extend({
  beforeInit: function ( options ) {
    // this is called whenever the component is instantiated,
    // immediately before any setup/rendering happens
  },
  init: function ( options ) {
    // this will be called once the component has been rendered
    // (assuming it *is* rendered to the DOM)
  },
  complete: function () {
    // this is called asynchronously once any intro transitions
    // have completed - again, assuming it is rendered
  }
});

To do any cleanup on teardown, you have to do something like this:

MyRactive = Ractive.extend({
  init: function () {
    this.on( 'teardown', cleanup );
  }
});

If you're not creating a component with Ractive.extend(), there's just the complete() method - no beforeInit() or init(). Harmless, perhaps (you can do the beforeInit() work before new Ractive(), and the init() work afterwards), but this has proven to be a source of confusion.

The problem? Firstly, these hooks are badly named. init() and complete(), in particular, offer little clue as to what they're for. Secondly, it's possible to use Ractive without targeting a DOM node - maybe you're using it on the server, maybe you have a viewless instance that you're using for managing state within your app, maybe the instance is responsible for attaching itself to a DOM node that it doesn't yet know about. In any of these cases you would probably expect the init() method to be called, but it isn't. It's more like an onRender() method (it's actually called from within the render() method).

And the beforeInit() method is pretty much redundant now that we have option functions for dynamically defining templates and partials and so on.

Fixing this isn't hard from an implementation standpoint - it's just a design question that we need to try and get right. It's also an opportunity to consider a couple of related issues.

The hooks themselves

There's a bunch of lifecycle events we need to consider:

  • Instantiation (new Ractive(...))
  • Rendering (before and after? Or just after?)
  • Completion of any initial transitions (though arguably, if you need this hook, you should simply omit the el option and then call ractive.render(el), since that returns an equivalent Promise)
  • Detach and insert
  • Unrender
  • Teardown/destroy (which includes unrender but isn't the same - you can unrender and re-render without destroying the instance)

(If I've missed any, please point them out!)

Should each of these hooks be a method? Or should some, or all (except the instantiation hook) be events that you subscribe to? A method allows things like this._super(), which is potentially useful, and it means one less level of indentation:

MyRactive = Ractive.extend({
  onDetach: function () {
    // do something
  }
});

// versus
MyRactive = Ractive.extend({
  onInit: function () {
    this.on( 'detach', function () {
      // do something
    });
  }
});

It also means we have fewer 'system' event names which can't be used for DOM proxy events (e.g. on-click='change' is banned, because there's a system change event).

On the flip side, events are a simple, well-understood mechanism, and they allow you (and other consumers of a component, not just the component itself) to attach multiple handlers. (In any case, throwing out things like the teardown event would break existing code, so they'd have to be deprecated gradually if at all.)

If we went with methods (or a hybrid approach), what should they be called? Is onInit(), onRender() and so on the right convention to use?

Defining methods at the point of new Ractive()

It would eliminate the confusion about beforeInit() working with Ractive.extend() but not new Ractive() if properties of the options object were simply added to the instance - so if you specify an onInit() method, it gets called:

var ractive = new Ractive({
  onInit: function () {
    // some (arguably pointless, but hey) init logic happens
  }
});

// is the same as
MyRactive = new Ractive.extend({
  onInit: function () {
    // ...
  }
});

var ractive = new MyRactive();

This dovetails nicely with the discussion happening around allowing proxy event directives to call methods directly - we could do things like this without having to faff around creating components:

var audio = new Audio();
audio.src = 'klaxon.wav';

var ractive = new Ractive({
  el: 'body',
  template: '<button on-click="klaxon()">click me!</button>',
  klaxon: function () {
    audio.play();
  }
});

Mixins

The other thing that would potentially fall out of this is mixin support. If a mixin is defined as a set of methods that are called during the appropriate lifecycle events, you can start adding reusable chunks of functionality that are completely orthogonal to the implementation details of a particular component. I'll be honest, I don't really know what the applications of this could be (logging? persistence?) but it seems like an idea with potential. There's some discussion of it on this issue.


So, that was another long-winded stream of consciousness. Congrats if you made it this far. Would welcome all feedback! Thanks.

@Rich-Harris Rich-Harris added this to the post-0.5.0 milestone Jul 3, 2014
@martypdx
Copy link
Contributor

martypdx commented Jul 4, 2014

hooks

Instantiation (new Ractive(...))

The current beforeInit offers an opportunity to message the options as passed in to the constructor (often used by extends and components to massage the options). The other configuration hook would be after the options have been resolved with the existing baseclass. The later is probably the more useful. We added functions and such, but this would give you the ability to tweak any of the registries and settings to whatever degree you wanted.

So, in theory, these could be the beforeConfig and config hooks. Or if we just keep the later, perhaps it becomes the (new) beforeInit

I think we may still need an init that is pre-render. This would be where node.js uses could set things up like observers and other non-render.

One thing to keep in mind is that some of these hooks will run multiple times during an instance lifetime. Observers and handlers don't need to be redone (normally), so it would be good to have a place to put them that didn't get called more than once. Conceptually, it seems onInit seems like the right place.

Rendering (before and after? Or just after?)

If init runs only once, you'd need a beforeRender to redo anything.

Completion of any initial transitions (though arguably, if you need this hook, you should simply omit the el option and then call ractive.render(el), since that returns an equivalent Promise)
Detach and insert

is this the detach and insert of the root fragment?

Unrender
Teardown/destroy (which includes unrender but isn't the same - you can unrender and re-render without destroying the instance)

methods or events?

I've been leaning to methods, mostly because I like keeping the on( eventname for dealing with template and component events, having methods keeps lifecycle 'events' separate, and to your point, avoids polluting acceptable proxy names. You can still propagate them out as events if you want:

  onRender: function(){
    this.fire('rendered')
 }

There's something attractive about having them all start with on, but then again that might raise more confusion between these methods and the on('proxy'. And some of the compound names get long: onBeforeRender, etc. Also names like init, beforeRender, render would be familiar from other libraries. So I'm leaning towards the shorter monikers, but not strongly.

new Ractive as extend

At first I thought there was some esoteric reason for new options being "options" and extend options being "options plus extensions". And it used to be that:

// beforeInit goes here
var ractive = new Ractive({})
// init goes here

But it is confusing, and in fact the code would be simpler, if we just "extended" before init. I'm all for it. Basically it makes Ractive.extend(options).prototype and the config step of new Ractive(options) equivalent.

I still have reservations about calling the ractive methods from the template, but I'll continue that discussion on the other thread we started.

mixins

Sounds good, basically we process all the arguments as options. ES6 splats FTW!

@skeptic35
Copy link

Personally it took me a while to fully understand how and when each of these hooks can (and can not) be used but meanwhile I'm using all of them quite heavily and wouldn't be too happy if I'd have to refactor all that code ;).
I use beforeInit to adjust the options, init to set up my Event-Handlers and complete to do stuff that requires the DOM. I sometimes would have liked to have a hook that comes between beforeInit and init. When the Ractive instance is basically set up (e.g. I'd already have access to ._parent in a component) but is not rendered yet. But those were edge cases and I always found an other way to do what I wanted.
Being able to use the hooks with new Ractive() as well would definitely make things less confusing.

I agree with @martypdx on the subject of methods vs. events. I don't know but adjusting the options of a Ractive instance in an event handler somehow just wouldn't feel quite right. Same goes for setting up the events in an event handler.

@Rich-Harris I Didn't quite understand why you wrote this:

To do any cleanup on teardown, you have to do something like this:

MyRactive = Ractive.extend({
  init: function () {
    this.on( 'teardown', cleanup );
  }
});

I always do it like that:

MyRactive = Ractive.extend({
  // beforeInit, init, complete 
  // ...
  teardown: function() {
    // cleanup stuff...
    this._super();
  }
});

Anything wrong with that approach?

@martypdx
Copy link
Contributor

martypdx commented Jul 9, 2014

Thinking on this some more, one of the issues with using methods for the lifecycle events is that it doesn't work well with mixins. Mixins should work autonomously and they may very well need to use the lifecycle events to attach observers, etc.

var C = Ractive.extend(mixin1, mixin2, { template: 'xyz' })

I don't think we want to have mixin1's init call _super to get mixin2's init to work.

Having easy, declarative events would be ideal for mixins. Which brings me back to the on discussion (#405, #360) and the ability to do options like:

{
    on: {
        handler1: function () {...}
    }
}

Assuming we figure out how to distinguish event handlers vs lifecycle events (maybe we have two keywords like events and on, or lifecycle has some naming convention like onInit or _init, or we decide they should be together), the issue we ran into was how to handle inheritance. It occurred to me that we have good inheritance with methods and maybe we could just introduce an (optional) string option to bridge between the declarative on format and those methods:

{ 
    on: {
        init: 'setup',
        foo: function(){...}
    },
    setup: function(){...}
}

In this case, all on events get subscribed across mixins and subclasses. Component authors, as best practice if they are to be broadly consumed and are likely to be extended, delegate to methods. But it allows use the declarative events and gives the simple function option for one-off or other times when you don't care about inheritance.

@martypdx
Copy link
Contributor

fyi - Here's how some other libraries are handling:

React uses a specific mixing property .Mixins have a specific lifecycle API largely around mount, unmount, and update. The are specified as a dedicated property on the init options:

React.createClass({
  mixins: [SetIntervalMixin], // Array of mixins
 ...

Ember uses a dedicated method to create mixins, then they are passed as first param of extend:

App.Editable = Ember.Mixin.create({
  edit: function() {
    console.log('starting to edit');
    this.set('isEditing', true);
  },
  isEditing: false
});

// Mix mixins into classes by passing them as the first arguments to
// .extend.
App.CommentView = Ember.View.extend(App.Editable, {
  template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}')
});

Angular doesn't offer anything specific, extend just copies keys and you can specify multiple sources

angular.extend(dst, src1, src2);

@richarddli
Copy link

The documentation at http://docs.ractivejs.org/latest/components still references init(), beforeInit(), although the code appears to have been updated.

@martypdx
Copy link
Contributor

martypdx commented Sep 2, 2015

Going to close this as most of it has been implemented. Mixins were the one straggler item, let's raise new issues if we still want to add something. (or reopen this issue)

@martypdx martypdx closed this as completed Sep 2, 2015
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants