Charts (via a plain library)

Zoijs has no chart component and doesn't need one: any vanilla charting library that draws into an element works as-is. The bridge is the ref binding — it hands you the real DOM node, you create the chart, and you return a cleanup that destroys it. No wrapper package, no plugin.

The pattern works for Chart.js, uPlot, ApexCharts, ECharts, D3 — anything with a new Chart(el, …) / update() / destroy() shape.

import { html, mount, createState, effect, onCleanup } from "@zoijs/core";
import Chart from "chart.js/auto"; // any vanilla chart lib

function Sales() {
  const data = createState([12, 19, 9, 17, 23]);
  let chart; // the library instance

  // ref: runs after the <canvas> is inserted; the returned fn is the cleanup.
  const canvas = (el) => {
    chart = new Chart(el, {
      type: "bar",
      data: {
        labels: ["Mon", "Tue", "Wed", "Thu", "Fri"],
        datasets: [{ label: "Sales", data: data.peek() }],
      },
    });
    return () => chart.destroy(); // runs on unmount — no leak
  };

  // Push new data into the existing chart instead of recreating it.
  effect(() => {
    const next = data.get();
    if (!chart) return;
    chart.data.datasets[0].data = next;
    chart.update();
  });

  return html`
    <section>
      <canvas ref=${canvas} width="600" height="300"></canvas>
      <button onclick=${() => data.set(data.get().map(() => Math.round(Math.random() * 30)))}>
        Randomize
      </button>
    </section>
  `;
}

mount(Sales, "#app");

The bridge, explained#

  • ref gives you the element. The callback receives the inserted <canvas>, so the library has a real, connected node to draw into. See Bindings.
  • ref cleanup owns the instance. Returning () => chart.destroy() ties the chart's lifetime to the element's: Zoijs runs it on unmount (or when the node leaves a list), so the library tears down cleanly — no detached canvases, no listeners left behind.
  • effect syncs data, doesn't rebuild. The chart is created once; an effect watches data and calls the library's own update() when it changes. That's the whole point of Zoijs's model — you don't re-render the chart, you mutate it imperatively exactly where it matters, and only when the data changes.
  • Read with peek at creation, get in the effect. The ref reads data.peek() once to seed the chart (no subscription); the effect reads data.get() so it re-runs on change. That split keeps creation and updates cleanly separated.

When the library wants a container, not a canvas#

Some libraries (ApexCharts, ECharts) take a <div> and manage their own SVG/canvas inside it. Identical pattern — ref the <div>, new Chart(el, …), return () => chart.dispose(). Because the library owns that subtree, leave it out of your html template (don't put reactive ${…} inside the chart's container) and let the library manage it.


Next: Animations.