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#
refgives you the element. The callback receives the inserted<canvas>, so the library has a real, connected node to draw into. See Bindings.refcleanup 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.effectsyncs data, doesn't rebuild. The chart is created once; aneffectwatchesdataand calls the library's ownupdate()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
peekat creation,getin the effect. Therefreadsdata.peek()once to seed the chart (no subscription); theeffectreadsdata.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.