brumate

Effects and browser code

A component renders on the server, so its script never runs in the browser. To run code in the browser, use a handler, $effect, $client or a #client block. Each one starts at a different time.

$effect

$effect(callback) runs the callback in the browser when a value that it reads changes:

App.brmEdit the code to run it again

The sample runs when it comes into view.

For code that must run when the page loads, use $client.

Cleanup

The callback can return a function. It runs before the next run of the effect, and when the component is removed from the page:

App.brmEdit the code to run it again

The sample runs when it comes into view.

Stopping writes running, so the effect runs again. Its cleanup clears the timer of the last run first.

Effects depend on the functions they call

An effect depends on every value that it reads. That includes values read in the functions that it calls, and in callbacks that it creates, such as the timer above. This is why the timer uses $untrack(seconds++): without it, the effect would depend on seconds and restart the timer every second.

A function that writes a signal it also reads is the common mistake:

<script>
  import { $effect, $signal } from 'brumate/signals';

  let query = $signal('');
  let saves = $signal(0);

  function save() {
    // Reads and writes `saves`.
    saves++;
  }

  // Depends on `query`, and on `saves` through `save`.
  $effect(() => {
    void query;
    save();
  });
</script>

The effect depends on saves, so each save runs the effect again. Call such functions inside $untrack, which reads without depending on anything inside:

App.brmEdit the code to run it again

The sample runs when it comes into view.

brumate inspect shows what each effect depends on, as reads signal query.

Values that stay across visits

A preference such as the theme, a dismissed banner or the width of a panel belongs in a cookie. The server reads it, so the first render has the right value, and an $effect writes it when the value changes:

<script>
  import { getEvent } from 'brumate/server';
  import { $effect, $signal } from 'brumate/signals';

  let theme = $signal(getEvent().getCookie('theme') ?? 'light');

  $effect(() => {
    document.cookie = `theme=${theme}; path=/; max-age=31536000; samesite=lax`;
  });
</script>

<div class="page" data-theme=theme>
  <button onClick=(() => (theme = theme === 'dark' ? 'light' : 'dark'))>Theme</button>
</div>

localStorage and sessionStorage work, but the server cannot read them. The page renders the default first, and the browser has to run code on load to read the stored value, so the page changes after it shows, and every page that does it loads code before anyone interacts. Keep them for values that the first render does not show.

Element references

ref=$bind(name) on an element writes the element into the signal name, so browser code can use it:

App.brmEdit the code to run it again

The sample runs when it comes into view.

$client

$client(callback, trigger) runs the callback in the browser once, when the trigger fires. It does not wait for a change:

App.brmEdit the code to run it again

The sample runs when it comes into view.

Trigger Runs when
'visible' The element that the callback reads through ref enters the viewport, or else the first element of the component. This is the default.
'idle' The browser is idle after the page loads.
'load' The page loads.

#client blocks

A #client block skips its body on the server. The browser renders it when the trigger fires, with the same triggers as $client:

App.brmEdit the code to run it again

The sample runs when it comes into view.

Use it for markup that needs the browser, such as a map or a value from localStorage. The page shows nothing in its place until the block renders.

An #else body renders on the server in place of the body, until the body renders:

App.brmEdit the code to run it again

The sample runs when it comes into view.

Behaviors of an element

A behavior that belongs to an element, such as a click outside, a field that grows with its text, or a chart, is a plain function that takes the element and returns its cleanup. A component gives it the element with ref and runs it with $client:

// behaviors.ts
export function clickOutside(element: HTMLElement, onOutside: () => void): () => void {
  const listener = (event: PointerEvent) => {
    if (!element.contains(event.target as Node)) {
      onOutside();
    }
  };
  document.addEventListener('pointerdown', listener);
  return () => document.removeEventListener('pointerdown', listener);
}
<script>
  import { $client, $signal } from 'brumate/signals';
  import { autosize, clickOutside } from './behaviors.ts';

  let open = $signal(true);
  let menu = $signal<HTMLElement>();
  let field = $signal<HTMLTextAreaElement>();

  $client(() => clickOutside(menu!, () => (open = false)));
  $client(() => autosize(field!));
</script>

<div class="menu" ref=$bind(menu) hidden=(!open)>…</div>
<textarea ref=$bind(field)></textarea>

Which one to use

You want to Use
Respond to something the user does A handler, such as onClick
Run code when a value changes $effect
Run code when the page loads, or when it comes into view $client
Render markup that needs the browser #client
Reach an element from browser code ref=$bind(name)
Add a behavior to an element ref=$bind(name) and $client

Handlers are often enough. A value that a handler writes updates the page by itself, so an effect is only needed for work outside the page, such as saving to localStorage or talking to a library.

Errors in the browser

Code that runs in the browser can throw: a handler, the promise that an async handler returns, an effect, or a $client callback. The error goes to the nearest #try around the scope that the code belongs to, which renders its #catch body with the error and reset:

#try {
  <button onClick=(async () => { await save(); })>Save</button>
} #catch (error, reset) {
  <p>{String(error)}</p>
  <button onClick=reset>Try again</button>
}