Au 2.0: Concerning au-compose, activate is called when the model is replaced, but deactivate is not called on the outgoing composition

Hello,

I’m making some improvements to my tabbed viewer as I migrate from Au 1.0 1o Au 2.0.

As I click through the tabs, activate is called and all is well for the incoming component; deactivate, however, is not called on the outgoing component.

And, actually, only the model and session are being replaced. The component isn’t changing. I have this:

<div class="tsi-doc-viewer-view">
  <au-compose    
    component.bind="dm.activeDoc.docComponent"
    model.bind="dm.activeDoc.model"
    session.bind="dm.activeDoc.session"
  ></au-compose>
</div>

So, dm.activeDoc.docComponent doesn’t change. However, dm.activeDoc.model does. session is forwarded to the component composed.

How can Aurelia reliably call activate when a new model is bound, but not call deactivate on the outgoing component? I know I can take a component ref on the composition, and then imperatively call deactivate() on the component. But it seems like I shouldn’t have to do that. I don’t do that in the Au 1.0 app.

Did something change in 2.0?

This is by design in Aurelia 2, and it’s actually an intentional optimisation.

In Au2’s au-compose, when only the model changes but the component stays the same:

  1. deactivate() is NOT called on the existing component
  2. activate() IS called again with the new model
  3. The component instance is reused (no teardown/recreation)

This is a lightweight update path. Which you can see in the au-compose.ts source code from the main au2 repo:

if (name === ‘model’ && this._composition != null) {
this._composition.update(this.model); // Just calls activate(model) again
return; // No recomposition
}

The update() function internally just calls comp.activate?.(model) on the existing instance.

It avoids unnecessary teardown/setup cycles when only the data changes. The same component instance handles multiple models sequentially.

How to adapt your code:

Your activate() method should handle being called multiple times with different models. If you need cleanup logic when switching between models, do it at the beginning of activate():

activate(model: MyModel) {
// Cleanup from previous model (if any)
this.cleanupPreviousState();

// Set up for new model
this.currentModel = model;
this.initializeForModel();

}

Alternatively, if you truly need deactivate semantics, you could:

  1. Use @watch on the model to detect changes
  2. Implement a custom modelChanged() lifecycle callback that performs cleanup

This differs from Au1 where the compose element would fully recompose on model changes, triggering the full lifecycle. Au2’s approach is more performant but requires this pattern adjustment.

Hello Dwayne,

You know, I read your post before I went to dinner, and as I was eating, I realized what was going on. Since the composition isn’t changing (or, should I say, the component), we’re right there: the instance of the component is live and just right there.

And it definitely is blazing fast! I’m rewriting the tabbed viewer for Au 2.0, and it was so fast, I thought nothing was changing (my test harness uses project names that vary only slightly)!

It struck me that you and I might be using the term “deactivate” differently. I mean it as a hook strictly in symmetry with activate, and specifically for dynamic composition. I know when I call deactivate on an enhancement, it triggers a full tear-down (at least I think), and I’m not referring to that level of deactivation.

I disagree a bit on the absence of deactivate. It provides a logical place for that code to live. And if activate is lightweight, then surely deactivate would be, too (distinct from a full teardown). Whether I’m calling clean-up code imperatively out of activate, or in response to a change handler on model, I still have to call it, and consequently the hit to performance will be same in any case. Referring to the framework code you provided, would this change truly affect performance:

if (name === ‘model’ && this._composition != null) {
this._composition.cleanUp() // Called before the update
this._composition.update(this.model);  // Just calls activate(model) again
return;  // No recomposition
}

Where the code inside #cleanUp might look like this:

comp.deactivate?.(this.model) 

I think your suggestions as to how to deal with deactivate are all excellent, of course, and I’ll implement one of the approaches for sure (as I really need to). But the suggestions highlight an issue: without a deactivate hook, clean-up code might be implemented a number of different ways, and all to overcome the absence of a deactivate hook.