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.

In the following 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 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.

export const Counter = component$(() => {
  const store = useStore({ count: 0 });

  return <button onClick$={() => store.count++}>{store.count}</button>;
});

In a real applications, the listener may refer to complex code. By creating a lazy-loaded boundary (with the $), Qwik can tree-shake all of the code behind the click listener and delay its loading until the user clicks the button.

Another option is passing QRLs as values for event listeners. For instance, the above example could also be written in the following way:

import { component$, useStore, $ } from '@builder.io/qwik';

export const Counter = component$(() => {
  const store = useStore({ count: 0 });
  const incrementCount = $(() => store.count++);

  return <button onClick$={incrementCount}>{store.count}</button>;
});

Prevent default

Because of the async nature of Qwik, an event's handler execution might be delayed because the implementation has not been downloaded yet.

This introduces a problem when the event's handler needs to prevent the default behavior of the event. Traditional event.preventDefault() will not work, so instead use Qwik's preventdefault:{eventName} attribute:

export const PreventDefaultExample = component$(() => {
  return (
    <a
      href="/about"
      preventdefault:click // This will prevent the default behavior of the "click" event.
      onClick$={(event) => {
        // PreventDefault will not work here, because handle is dispatched asynchronously.
        // event.preventDefault();
        singlePageNavigate('/about');
      }}
    >
      Go to about page
    </a>
  );
});

Window and Document events

So far, we have discussed how to listen to events that originate from elements. There are events (for example, scroll and mousemove) that require that we listen to them on the window or document. For this reason, Qwik allows for the document:on and window:on prefixes when listening for events.

export const EventExample = component$(() => {
  const store = useStore({
    scroll: 0,
    mouse: { x: 0, y: 0 },
    clickCount: 0,
  });

  return (
    <button
      window:onScroll$={(e) => (store.scroll = window.scrollY)}
      document:onMouseMove$={(e) => {
        const { x, y } = e;
        store.mouse = { x, y };
      }}
      onClick$={() => store.clickCount++}
    >
      scroll: {store.scroll}
      mouseMove: {store.mouse.x}, {store.mouse.y}
      click: {store.clickCount}
    </button>
  );
});

The purpose of the window:on/document: is to register an event at a current DOM location of the component but have it receive events from the window/document. There are two advantages to this:

  1. The events can be registered declaratively in your JSX.
  2. The events get automatically cleaned up when the component is destroyed (No explicit bookkeeping and cleanup is needed).

useOn Hooks

useOn

useOn hook will add a DOM based event listener at component level.

export const ClickableComponent = component$(() => {
  useOn(
    'click',
    $(() => {
      alert('Alert from Clickable Component.');
    })
  );
  return <div>click from other component 1</div>;
});

export const HoverComponent = component$(() => {
  const store = useStore({ isHover: false });
  useOn(
    'mouseover',
    $(() => {
      store.isHover = true;
    })
  );
  return <div>{store.isHover ? 'Now Hovering' : 'Not Hovering'}</div>;
});

export default component$(() => {
  return (
    <>
      <ClickableComponent />
      <HoverComponent />
    </>
  );
});

useOnWindow

useOnWindow hook will add a event listener to window.

export const Online = component$(() => {
  useOnWindow(
    'online',
    $(() => {
      alert('Your Device is now Online');
    })
  );
  useOnWindow(
    'offline',
    $(() => {
      alert('Your Device is now Offline');
    })
  );
  return <div></div>;
});

export default component$(() => {
  return <Online />;
});

useOnDocument

useOnDocument hook will add a event listener to document.

export const KeyBoard = component$(() => {
  const store = useStore({ keyPressed: '' });
  useOnDocument(
    'keydown',
    $((event) => {
      store.keyPressed = store.keyPressed + event.key;
    })
  );
  return <div>{store.keyPressed}</div>;
});

export default component$(() => {
  return <KeyBoard />;
});

Events and Components

Components are functions, not elements. Since DOM events do not exist naturally, it's possible to define custom events as props.

Now let's look at how to declare a child component that can be used with events as props.

import { PropFunction } from '@builder.io/qwik';

interface CmpButtonProps {
  onClick$?: PropFunction<() => void>;
}

export const CmpButton = component$((props: CmpButtonProps) => {
  return (
    <button onClick$={props.onClick$}>
      <Slot />
    </button>
  );
});

As far as Qwik is concerned, passing events to a component is equivalent to passing props.

In our example, we declare all props in the CmpButtonProps interface. Specifically, notice onClick$: PropFunction<() => void> declaration.

Then, when we want to use <CmpButton>, we can do the following:

<CmpButton onClick$={() => store.cmpCount++}>{store.cmpCount}</CmpButton>

Working with QRLs

Let's look at a variation of the above <CmpButton> implementation.

In this example, we would like to demonstrate passing callbacks to components. For this reason, we have created a onDblclick$ listener:

interface CmpButtonProps {
  onClick$?: PropFunction<() => number>;
}

export const CmpButton = component$((props: CmpButtonProps) => {
  return (
    <button
      onClick$={props.onClick$}
      onDblclick$={async () => {
        const nu = await props?.onClick$();
        console.log('double clicked', nu);
      }}
    >
      <Slot />
    </button>
  );
});

Notice that we can pass the props.onClick$ method directly to the onDblclick$ attribute as seen on <button> element (see attribute onDblclick$={props.onClick$}).

This is because both the inputting prop onClick$ as well as JSX prop onDblclick are of type PropFunction<?> (and both have $ suffix).

However, it is not possible to pass props.onClick$ to onClick$ because the types don't match (This would result in the type error: onClick$={props.onClick$}).

Instead, the $ is reserved for inlined closures. In our example, we would like to console.log("double clicked") after we process the props.onClick$ callback. We can do this with the props.onClick$() method.

This method will:

  1. Lazy load the code
  2. Restore the closure state
  3. Invoke the closure

The operation is asynchronous and therefore returns a promise, which we can resolve using the await statement.

State recovery

export const Counter = component$(() => {
  const store = useStore({ count: 0 });

  return <button onClick$={() => store.count++}>{store.count}</button>;
});

At first glance, it may appear that Qwik is simply lazy loading the onClick$ function. But, upon closer inspection, it is important to realize that 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.

Read more about closures here.

Capturing state is what allows a Qwik application to 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 which 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 would look something like this:

<div>
  <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:

  1. ./chunk-a.js: The file which needs to be lazy-loaded.
  2. Counter_button_onClick: The symbol which needs to be retrieved from the lazy-loaded chunk.
  3. [0]: An array of lexically captured 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 that 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).

Qwikloader

A small JavaScript script known as the Qwikloader is needed to allow browsers to understand the on:click attribute syntax. The Qwikloader is small (about 1kb) and fast (about 5ms) in execution.

The Qwikloader script is inlined into the HTML so that it can be executed quickly.

When a user interacts with the application, the browser fires the relevant events which bubble up to the DOM.

At the root of the DOM, the Qwikloader listens to 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.

See Qwikloader for more details.

Made with โค๏ธ by