Turbo-compatible Stimulus controllers
Lifecycle callback cheatsheet
1) Use idempotent transformations
If you need to make changes to the DOM make sure these are idempotent. That means, applying the code multiple times does
not do any harm. Why is this important? Because cache entries are made before the DOM gets removed, so any cleanup in a
disconnect() method has no effect.
Example
After connecting, this is what the DOM looks like and what will get cached:
When a cache entry is restored, the connect function will run again, producing unwanted output:
Detect or guard transformations
To prevent transformations, that were applied multiple times, …
- … the change can be detected (in the above example: test for the existence of the
foodiv) - … an attribute is set in the DOM (for instance a
data-initializedattribute) and checked for - … the element is restored to its original state before a cache entry is made
- … the element is completely removed before a cache entry is made
CAVEAT: If you are using 1) or 2) and you detect an already transformed state, it is important to note, that the DOM you are looking at is basically dead. It was restored from cache, so there are no event listeners or live objects. If your controller uses these, you still need to attach them. This is especially tricky with 3rd party code that enhances the DOM (choices, ace, tinyMCE, …) - for these cases it might be necessary to use 3) or 4).
Cleanup before caching
Turbo dispatches the turbo:before-cache event right before creating the DOM copy that is stored in cache. Here we can
perform some cleanup. In Contao, we are calling the beforeCache method on any Stimulus controller, that implements it.
Alternatively, a resource that is ’temporary in nature’ like a flash message, can be annotated with the
data-turbo-temporary attribute. Then, on turbo:before-cache this element will be removed completely.
2) Restore resources after removal
Whenever an element gets removed from the DOM, the disconnect() method will run. Always assume that this can happen at
any time. Cleanup CSS classes on parent elements, created sibling elements, etc.
Prevent memory leaks
Make sure you are not creating any memory leaks. These could come in the form of event listeners on anything outside the element or resources such as class instances to which you or the DOM is still holding a reference after this method has run!
Be resilient
If you are cleaning up objects or elements, try to think of scenarios where these do not exist anymore at this point. In
that case you might want to introduce checks or use the ?. operator instead of . to access properties.
Here are some considerations:
- Do you perform cleanup in a
beforeCachemethod? If so, this could have run before. - Could the DOM have been altered in the meantime by another controller or 3rd party code?
- Was a resource optional (i.e. not instantiated at all) because the DOM was already transformed and you skipped
evaluation in the
connect()method?
3) Conventions
Method naming
In JS, there aren’t any private methods. To differentiate between what is API (for example a Stimulus action) and what is used internally, prefix the “private” methods with an underscore:
Events
- Avoid registering events by calling
addEventListener, use thedata-actionnotation instead. This way adding/deleting is handled by Stimulus. - If you still need to register events manually (for example if they are on dynamically created elements), make sure to
remove them again in the
disconnect()method. This step can be omitted if the target is the controller element itself or a child of it (because it will be removed from the DOM anyway) and you do not hold a reference that would prevent garbage collection. - Bindings on the
windowwill not go out of scope as Turbo Drive only replaces/morphs thebodyandhead.