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. Event handlers are registered using the on{EventName}$ attribute. For example, the onClick$ attribute is used to listen for click events.

<button onClick$={() => alert('CLICKED!')}>click me!</button>

Inline handler

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>.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Increment {count.value}
    </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 be loaded into the JavaScript VM until the user triggers the click event, however, it will be loaded into the browser cache eagerly so as not to cause delays on first interactions.

In real-world 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.

Reusing event handlers

If we want to reuse the same event handler for multiple elements or event, we need to import $ from @builder.io/qwik and wrap the event handler in it.

This way we must extract the event handler into a QRLs and pass it to the event listener.

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const increment = $(() => count.value++);
  return (
    <>
      <button onClick$={increment}>Increment</button>
      <p>Count: {count.value}</p>
    </>
  );
});

Note: If you extract the event handler then you must manually wrap the event handler in the $(...handler...) so that it can be lazy attached.

Multiple event handlers

If we want to register multiple event handlers for the same event, we can pass an array of event handlers to the on{EventName}$ attribute.

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const print = $((ev) => console.log('CLICKED!', ev));
  const increment = $(() => count.value++);
 
  // The button when clicked will print "CLICKED!" to the console, increment the count and send a event to Google Analytics.
  return (
    <button
      onClick$={[print, increment, $(() => {
        ga.send('click', { label: 'increment' });
      })]}
    >
      Count: {count.value}
    </button>
  );
});

Event object

The first argument of the event handler is the Event object. This object contains information about the event that triggered the handler. For example, the Event object for a click event contains information about the mouse position and the element that was clicked. You can check out the MDN docs to know more details about each DOM event.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const position = useSignal<{ x: number; y: number }>();
  return (
    <div
      onClick$={(event) => (position.value = { x: event.x, y: event.y })}
      style="height: 100vh"
    >
      <p>
        Clicked at: ({position.value?.x}, {position.value?.y})
      </p>
    </div>
  );
});

Asynchronous Events

Because of the async nature of Qwik, an event's handler execution might be delayed because the implementation has not been loaded into the JavaScript VM yet. Because of the asynchronous nature of event processing in Qwik the following APIs on an Event object will not work:

  • event.preventDefault()
  • event.currentTarget

Prevent default

Because event handling is asynchronous, you can't use event.preventDefault(). To solve this Qwik introduces a declarative way to prevent default through preventdefault:{eventName} attribute.

import { component$ } from '@builder.io/qwik';
 
export default 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 handler is dispatched asynchronously.
        alert('Do something else to simulate navigation...');
      }}
    >
      Go to about page
    </a>
  );
});

Event target

Because event handling is asynchronous, you can't use event.currentTarget. To solve this Qwik handlers provide a currentTarget as a second argument.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const currentElm = useSignal<HTMLElement|null>(null);
  const targetElm = useSignal<HTMLElement|null>(null);
 
  return (
    <section onClick$={(event, currentTarget) => {
      currentElm.value = currentTarget;
      targetElm.value = event.target as HTMLElement;
    }}>
      Click on any text <code>target</code> and <code>currentElm</code> of the event.
      <hr/>
      <p>Hello <b>World</b>!</p>
      <hr/>
      <ul>
        <li>currentElm: {currentElm.value?.tagName}</li>
        <li>target: {targetElm.value?.tagName}</li>
      </ul>
    </section>
  );
});

Note: currentTarget in the DOM points to the element that the event listener was attached to. In the example above it will always be the <DIV> element.

Synchronous event handling

In some cases, it is necessary to handle an event traditionally, because some APIs need to be used synchronously. For example, dragstart event must be processed synchronously and therefore it can't be combined with Qwik's lazy code execution.

To do this, we can leverage a useVisibleTask to programmatically add an event listener using the DOM API directly.

import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const draggableRef = useSignal<HTMLElement>();
  const dragStatus = useSignal('');
 
  useVisibleTask$(({ cleanup }) => {
    if (draggableRef.value) {
      // Use the DOM API to add an event listener.
      const dragstart = () => (dragStatus.value = 'dragstart');
      const dragend = () => (dragStatus.value = 'dragend');
 
      draggableRef.value!.addEventListener('dragstart', dragstart);
      draggableRef.value!.addEventListener('dragend', dragend);
      cleanup(() => {
        draggableRef.value!.removeEventListener('dragstart', dragstart);
        draggableRef.value!.removeEventListener('dragend', dragend);
      });
    }
  });
 
  return (
    <div>
      <div draggable ref={draggableRef}>
        Drag me!
      </div>
      <p>{dragStatus.value}</p>
    </div>
  );
});

NOTE Using VisibleTask to listen for events in an anti-pattern in Qwik because it causes eager execution of code in the browser defeating resumability. Only use it when you have no other choice. most of the time, you should use JSX to listen for events: <div onClick$={...}> or useOn(...) event methods if you need to listen programmatically.

PropFunction

When creating your components it is often useful to pass what looks like event handlers, (even though they are not DOM events, only callbacks.) Component boundaries in Qwik must be serializable, and functions are not serializable unless they are converted to a QRL using an optimizer. This is done through $ suffix. QRLs are asynchronous and therefore we need to tell TypeScript that the function can't be called synchronously, we do this through PropFunction type.

<CmpButton onClick={() => alert('CLICKED!')}>click me!</CmpButton>

The above is not possible because onClick is a function that is not serializable. Instead, we need to tell Optimizer to convert our function into a QRL. This is done by naming the property with $ suffix as in this example.

import { type PropFunction, component$, Slot } from '@builder.io/qwik';
 
export default component$(() => {
  return <CmpButton onClick$={() => alert('CLICKED!')}>click me!</CmpButton>;
});
 
export const CmpButton = component$<{
  // Important to tell TypeScript that this is async
  onClick$?: PropFunction<() => void>;
}>((props) => {
  return (
    <button onClick$={props.onClick$}>
      <Slot />
    </button>
  );
});

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.

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[|window|document] Hook

  • useOn(): listen to events on the current component's root element.
  • useOnWindow(): listen to events on the window object.
  • useOnDocument(): listen to events on the document object.

useOn[|window|document]() hook will add a DOM-based event listener at the component level programmatically. This is often useful when you want to create your own use hooks or if you don't know the event name at the time of compilation.

import { $, component$, useOnDocument, useStore } from '@builder.io/qwik';
 
// Assume reusable use method that does not have access to JSX
// but needs to register event handlers.
function useMousePosition() {
  const position = useStore({ x: 0, y: 0 });
  useOnDocument(
    'mousemove',
    $((event) => {
      const { x, y } = event as MouseEvent;
      position.x = x;
      position.y = y;
    })
  );
  return position;
}
 
export default component$(() => {
  const pos = useMousePosition();
  return (
    <div>
      MousePosition: ({pos.x}, {pos.y})
    </div>
  );
});

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • voluntadpear
  • the-r3aper7
  • RATIU5
  • manucorporat
  • nnelgxorz
  • adamdbradley
  • hamatoyogi
  • fleish80
  • cunzaizhuyi
  • Pika-Pool
  • mhevery
  • AnthonyPAlicea
  • amatiash