# Composables

Source: https://signy.dev/examples/composables/

Functions that return signals, the Signy answer to hooks.

A composable is a plain function. It creates signals and effects, and because a component calls it from `setup()`, the component owns them and disposes them on unmount. There are no rules about call order, as there are for React hooks.

## Preview

[Open the page on its own](https://signy.dev/examples/composables/demo/index.html.md)

## Code

Save it as an `.html` file and open it in a browser. It loads Signy and Iris CSS from the CDN, so there is nothing to install or build.

**Show the full page**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>Composables - Signy example</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <main class="container" id="app"></main>

    <script>
      const { Component, Signal, application, html, effect, onCleanup, Show, For, DOM } = Signy;

      // A composable is a function that creates signals and effects. Called from a
      // component's setup(), they belong to the component and go away with it.

      function useLocalStorage(key, initial) {
        let stored;
        try {
          stored = JSON.parse(localStorage.getItem(key));
        } catch {}
        const value = new Signal(stored ?? initial);
        effect(() => {
          try {
            localStorage.setItem(key, JSON.stringify(value.value));
          } catch {}
        });
        return value;
      }

      function useDebounced(source, ms) {
        const value = new Signal(source());
        effect(() => {
          const next = source();
          const timer = setTimeout(() => (value.value = next), ms);
          onCleanup(() => clearTimeout(timer));
        });
        return value;
      }

      function useMediaQuery(query) {
        const list = matchMedia(query);
        const matches = new Signal(list.matches);
        DOM.on(list, "change", () => (matches.value = list.matches));
        return matches;
      }

      function useMouse(log) {
        const position = new Signal({ x: 0, y: 0 });
        DOM.on(window, "pointermove", (event) => (position.value = { x: event.clientX, y: event.clientY }));
        effect(() => onCleanup(() => log("useMouse removed its pointermove listener")));
        return position;
      }

      function useInterval(callback, ms, log) {
        effect(() => {
          const timer = setInterval(callback, ms);
          onCleanup(() => {
            clearInterval(timer);
            log("useInterval cleared its timer");
          });
        });
      }

      const log = new Signal([]);
      const write = (line) => log.set((lines) => [...lines.slice(-3), line]);

      class Tracker extends Component {
        seconds = new Signal(0);

        setup() {
          this.mouse = useMouse(write);
          useInterval(() => this.seconds.set((s) => s + 1), 1000, write);
        }

        render() {
          return html`
            <p>Pointer at ${() => this.mouse.value.x}, ${() => this.mouse.value.y}. Mounted for ${() => this.seconds.value}s.</p>
          `;
        }
      }

      class Playground extends Component {
        tracking = new Signal(true);

        setup() {
          this.userName = useLocalStorage("signy-composables-name", "Ada");
          this.query = new Signal("");
          this.debounced = useDebounced(() => this.query.value, 400);
          this.wide = useMediaQuery("(min-width: 40rem)");
          this.dark = useMediaQuery("(prefers-color-scheme: dark)");
        }

        render() {
          return html`
            <h1>Composables</h1>

            <article class="card">
              <header><strong>useLocalStorage</strong></header>
              <label for="name">Your name, kept after a reload</label>
              <input id="name" value=${this.userName.peek()} onInput=${(e) => (this.userName.value = e.currentTarget.value)} />
              <p>Hello, ${() => this.userName.value}!</p>
            </article>

            <article class="card">
              <header><strong>useDebounced</strong></header>
              <label for="query">Type quickly</label>
              <input id="query" onInput=${(e) => (this.query.value = e.currentTarget.value)} />
              <p>Debounced: "${() => this.debounced.value}"</p>
            </article>

            <article class="card">
              <header><strong>useMediaQuery</strong></header>
              <p>The window is ${() => (this.wide.value ? "wide" : "narrow")}, the system theme is ${() => (this.dark.value ? "dark" : "light")}. Resize to see it change.</p>
            </article>

            <article class="card">
              <header><strong>useMouse and useInterval</strong></header>
              <label>
                <input type="checkbox" role="switch" checked onChange=${(e) => (this.tracking.value = e.currentTarget.checked)} />
                Mount the tracker
              </label>
              ${Show({ when: () => this.tracking.value, children: () => html`<${Tracker} />` })}
              <ul aria-label="Cleanup log">
                ${For({ each: () => log.value, children: (line) => html`<li><small>${line}</small></li>` })}
              </ul>
            </article>
          `;
        }
      }

      application(Playground).mount("#app");
    </script>
  </body>
</html>

```

## What it uses

- [effect() and onCleanup()](https://signy.dev/documentation/1/reactivity/effects/index.html.md)
- [setup()](https://signy.dev/documentation/1/components/lifecycle/index.html.md) as the place to call them
- [Signals](https://signy.dev/documentation/1/reactivity/signals/index.html.md) as return values
