flow.once()

The flow.once() method allows one trigger through in the pipeline and simultaneously stops the flow it was called on whenever that happens.

Syntax

flow.once();

Parameters

None.

Return value

The same Flow object the method was called on.

Details

The .once() method participates in the flow pipeline, but in a bit of a special way. Once a trigger reaches .once() in the callback pipeline, two things happen.

As a result, there is more flexibility in where you put .once(), but its position in the pipeline still has meaning. In particular, this behavior is perfect for await expressions; more about this in the "Awaiting flows" example below.

Warning: Theoretically, event listeners can use the native { once: true } option; when using flows, it is strongly recommended to use .once(). This is because with .once(), on top of the event listener being cleaned up, the flow itself is also stopped, whereas with { once: true } the flow will remain "alive" while the underlying listener has been removed.

Examples

Loading an image

Some events are expected to fire only once, such as the load event. For events like these, .once() is perfect; it takes down the flow whenever the event fires, so that there are no concerns about memory leaks. As an example, let's use when() in conjunction with .once() to load an image and append it to the DOM.

const img = document.createElement('img'); when(img).loads().once().then(() => { document.body.append(img); }); img.src = '/assets/img/mythical-beast.webp';

It's also possible to have the .then() call before the .once(); in this case it doesn't matter.

Positioning .once()

For callback pipelines using only .then(), it doesn't matter whether .once() is put before any .then() methods, in between, or after. However, once other methods are thrown in the mix, it starts mattering. First, let's see what happens with .if().

Similarly, when using .await(), .debounce(), or any other asynchronous pipeline callbacks, then the position .once() has in the callback pipline is also relevant. To take .debounce() as an example:

The .once() method acts somewhat like a gatekeeper; it lets once trigger through, and tells all other triggers to just give up. Once the trigger that was let through reaches the end of the callback pipeline, the flow is stopped completely and cleaned.

It can also be useful to compare .once() to .until(() => true). The .until() method stops the flow whenever its callback returns something truthy, so at first glance this might seem similar to .once(). The main difference is that .until() stops the flow right away, and does not let any triggers through to the rest of the callback pipeline. More specifically, .once().then() allows the .then() callback to run, whereas in .until(() => true).then(), triggers never get past the .until().

Awaiting flows

We can rewrite the image loading example using await, and make it (in a way) more sequential.

const img = document.createElement('img'); await when(img).loads().once().after(() => { img.src = '/assets/img/mythical-beast.webp'; }); document.body.append(img);

Here, we use .after() to set the .src property on the image. This is because it is not possible to set the .src after the await expression, since then the image never has something to load in the first place, and we can't set the .src before attaching the event either, because the event might fire before the event listener was even set up (even if this is not actually possible - it feels unstable, and could be possible in other similar cases).

Defer until connected

Some components could have heavy logic they need to run to instantiate. Sometimes, that logic can be deferred to when the component first connects to the DOM. For this, we can use .once() in together with the connected() hook.

<title>complex-rendering</title> <template mode="closed"> <div id="render-container"></div> </template> <script> const renderContainer = query('#render-container'); connected(() => { renderComplexThingIntoContainer(renderContainer); // … }).once(); </script>

This causes the connected() lifecycle to fire only once; when the component disconnects and connects again, this callback is no longer fired. Note that this does mean any event listeners set up inside the handler, or otherwise monitorable items, are taken down immediately. Instead, if this is not desired, there's an alternative. Instead of stopping the flow returned by connected() immediately after it triggers (which is what .once() does), it can instead be stopped when the component disconnects through .until(disconnected()).

Usage notes

Calling .once() more than once on the same flow does nothing; only the first call is relevant to the behavior of the callback pipeline.

See also