Events
For a web application to be interactive, there needs to be a way to respond to user events. This is done by registering callback functions in the JSX template.
const Counter = component$(() => {
const store = useStore({count: 0});
return (
<button onClick$={() => store.count++}>
{store.count}
</button>
);
});
In the above example, the onClick$
attribute of the <button>
element is used to let Qwik know that a callback () => store.count++
should be executed whenever the click
event is fired by the <button>
.
Notice that onClick$
ends with $
. This is a hint to both the Qwik Optimizer and the developer that a special transformation occurs at this location. The presence of the $
suffix implies a lazy-loaded boundary here. The code associated with the click
handler will not download until the user triggers the click
event. See: Optimizer Rules for more details.
In the above example, the click
listener is trivial in implementation. But in real applications, the listener may refer to complex code. By creating a lazy-loaded boundary, Qwik can tree-shake all of the code behind the click listener and delay its loading until the user clicks on the event.
DOM
const Counter = component$(() => {
const store = useStore({count: 0});
return (
<button onClick$={() => store.count++}>
{store.count}
</button>
);
});
In the above example, the onClick$
is placed on <button>
. This means that the listener needs to be registered with the DOM. The registration of the listener creates two problems in the context of the SSR/SSG that Qwik needs to solve. (For context, remember that Qwik is resumable, that is, it can continue executing the application from where the server paused without being forced to download and execute code eagerly.)
- listener location: Qwik needs to know where the events are in the HTML which came from the SSR/SSG.
- listener code: Qwik needs to know what code should run if the event is triggered.
Without the above information, Qwik would be forced to download the component template and execute it so that the listener location and closure can be recovered. This process is known as hydration, and Qwik explicitly tries to avoid hydration.
Qwik serializes the event listeners into DOM in the form of QRLs. For the above example, the resulting HTML would look something like this:
<div q:host>
<button q:obj="1" on:click="./chunk-a.js#Counter_button_onClick[0]">0</button>
</div>
The critical thing to notice is that Qwik generated an on:click
attribute, containing the value ./chunk-a.js#Counter_button_onClick[0]
. In the above example the on:click
attribute solves the listener location problem, and the attribute value solves the listener code location problem. By serializing the listeners into the HTML Qwik, applications do not need to perform hydration on application startup.
Qwikloader
For the browser to understand the on:click
attribute syntax, a small JavaScript known as Qwikloader is needed. The Qwikloader is small (about 1kb) and fast (about 5ms) to execute. The Qwikloader is inlined into the HTML so that it can be executed quickly.
When a user interacts with the application, the browser fires relevant events that bubble up the DOM. At the root of the DOM, Qwikloader listens for the events and then tries to locate the corresponding on:<event>
attribute. If such an attribute is found, then the value of the attribute is used to resolve the location where code can be downloaded from and then executed.
State recovery
const Counter = component$(() => {
const store = useStore({ count: 0 });
return (
<button onClick$={() => store.count++}>
{store.count}
</button>
);
});
At first sight, it may appear that the Qwik simply lazy loads the onClick$
function. But upon closer inspection, it is important to realize that the Qwik lazy loads a closure rather than a function. (A closure is a function that lexically captures the state inside its variables. In other words, closures carry state, whereas functions do not.) The capturing of the state is what allows the Qwik application to simply resume where the server left off because the recovered closure carries the state of the application with it.
In our case, the onClick$
closure captures store
. Capturing of store
allows the application to increment the count
property on click
without having to re-run the whole application. Let's look at how closure capturing works in Qwik.
The HTML generated by the above code is something like this:
<div q:host>
<button q:obj="1" on:click="./chunk-a.js#Counter_button_onClick[0]">0</button>
</div>
Notice that on:click
attribute contains three pieces of information:
./chunk-a.js
: The file which needs to be lazy-loaded.Counter_button_onClick
: The symbol which needs to be retrieved from the lazy-loaded chunk.[0]
: An array of lexically capture variable references (State of the closure).
In our case () => store.count++
only captures store
, and hence it contains only a single reference 0
. 0
is an index into the q:obj
attribute which contains a reference to the actual serialized object referring to store
. (The exact mechanisms and syntax is an implementation detail that can change at any time.)
import()
Comparison to JavaScript supports dynamic import()
. At first glance, it may seem that the same can be achieved by import()
, but there are a few differences worth mentioning.
Dynamic import()
:
- Is relative to the file which contains it. This works great for
file-a.js
trying to loadfile-b.js
asimport('./file-b.js')
. However, when the./file-a.js
gets serialized into HTML then we lose its relative nature. It is the framework that reads the./file-b.js
from HTML and performs theimport()
. This means that all imports now become relative to the framework, which is incorrect. - Requires that the developer writes
import('./file-a.js')
, which means the developer is in charge of deciding where the lazy-loaded boundaries are. This limits our ability of the tooling to move code around in an automated way. - Supports import of top-level functions only which don't capture the state. This is the biggest difference. Qwik allows the imported symbol to be a closure that carries all of its state with it.