# The Zoijs ecosystem Zoijs core is deliberately tiny — nine functions, no router, no data layer, no head manager. Everything else lives in **small, optional packages** you add only when you need them. Use none of them and you still have a complete framework; add them one at a time as your app grows. This page explains what each package does, how they fit together, and walks through one app — the **Task Board** demo — that uses five of them together. (For an app that exercises eight packages, see the [admin dashboard](../../examples/admin/).) ## The packages | Package | Adds | Public API | |---|---|---| | [`@zoijs/core`](../README.md) | The framework | `html`, `mount`, `createState`, `computed`, `each`, `effect`, `boundary`, `configure`, `onCleanup` | | [`@zoijs/router`](../../router/README.md) | Client-side routing | `createRouter` → `view`, `link`, `go`, `path`, `query` | | [`@zoijs/resource`](../../resource/README.md) | Reading async data | `resource(fetcher)` → `data`, `loading`, `error`, `refresh` | | [`@zoijs/action`](../../action/README.md) | Writing async data | `action(fn)` → `run`, `pending`, `error`, `done`, `result`, `reset` | | [`@zoijs/head`](../../head/README.md) | Page title & meta | `title`, `description`, `meta` | | [`@zoijs/storage`](../../storage/README.md) | Persisting state | `storage(key, initial)` → `get`, `set`, `peek` (localStorage-backed) | | [`@zoijs/forms`](../../forms/README.md) | Form state + validation | `form(initial, options?)` → `values`, `value`, `set`, `errors`, `touch`, `validate`, … | | [`@zoijs/i18n`](../../i18n/README.md) | Internationalization | `createI18n(options)` → `t`, `locale`, `setLocale`, `n`, `d`, `list`, `add` | | [`@zoijs/ssr`](../../ssr/README.md) | Server rendering | `renderToString(component, { hydratable })`, `hydrate`, `serialize` (SSR + static prerender + data hand-off) | | [`@zoijs/testing`](../../testing/README.md) | DOM testing helpers (dev) | `render`, `screen`, `fireEvent`, `waitFor`, `cleanup`, `mockRouter` | | [`@zoijs/devtools`](../../devtools/README.md) | Reactive-graph inspector (dev) | `inspect()`, `createInspector()` | | [`@zoijs/eslint-plugin`](../../eslint-plugin/README.md) | Lint rules (dev) | `require-reactive-binding` (auto-fixable) + a11y rules (`alt-text`, `no-positive-tabindex`, `no-static-element-interactions`) | > New to Zoijs? Scaffold a ready-to-run app with `npm create zoijs@latest my-app` > (no build step). The optional packages below add capabilities to an app you > already have. Install only what you use: ```bash npm install @zoijs/core npm install @zoijs/router # routing npm install @zoijs/resource # reading data npm install @zoijs/action # writing data npm install @zoijs/head # title & meta npm install @zoijs/storage # persistence (localStorage) npm install @zoijs/forms # form state + validation npm install @zoijs/i18n # internationalization npm install @zoijs/ssr # server-side rendering npm install -D @zoijs/testing # DOM testing helpers (dev dependency) npm install -D @zoijs/devtools # reactive-graph inspector (dev dependency) npm install -D @zoijs/eslint-plugin # lint rule for the reactive-binding rule (dev dependency) ``` Or load them with no install via an import map / CDN (pin the version): ```js import { html, mount } from "https://esm.sh/@zoijs/core@1"; import { createRouter } from "https://esm.sh/@zoijs/router@0.2"; ``` Every package is: - **Optional** — install only what you use; the core never depends on them. - **Tiny** — each is one small file with a handful of functions. - **Built on the core's public API** — they use `createState`, `onCleanup`, `html`, and `mount` just like your own code. There is no private core access and **no core changes** were needed to add any of them. ## How they work together They share one idea: **reactive readers you call inside bindings.** `count.get()`, `user.loading()`, `save.pending()`, `router.path()` — they're all the same shape, so once you've learned one you've learned them all. Wrap any of them in an arrow (`${() => ...}`) and the DOM updates when it changes. And they share the core's **ownership model**: when a component (or a routed page) unmounts, its `onCleanup` runs. That single mechanism is why: - the **router** can swap pages and the old page's effects are disposed, - **head** restores the previous title automatically, - **resource**/**action** ignore a response that arrives after unmount. No provider wraps your app. No context is threaded through. No lifecycle methods. A page is a function; it sets up what it needs; the core cleans it up. ## The Task Board app A small task manager that uses **five** packages — core, router, resource, action, and head. Find it at [`examples/task-board/`](../../examples/task-board/). **Pages:** Home, Tasks (list + filter + counts), New task (form), Task details, About, and a Not-Found route. ### How each package is used - **`@zoijs/router`** — the route map is a plain object (`"/tasks/:id": TaskDetails`). `router.link(...)` renders the nav, `router.view()` renders the current page, and `router.go("/tasks")` redirects after a successful create. - **`@zoijs/head`** — every page calls `title(...)` and `description(...)` in its first lines, so the browser tab updates as you navigate. - **`@zoijs/resource`** — the Tasks page does `resource(() => api.listTasks())`; the details page does `resource(() => api.getTask(params.id))`. Loading and error states come for free. - **`@zoijs/action`** — creating, toggling, and deleting are `action(...)`s. The Create button shows `pending()`, the form shows `error()`, and on success the page calls the resource's `refresh()` (or navigates away). - **`@zoijs/core`** — `each()` renders the task list, `computed()` derives the "X total · Y done" counts, and `createState()` holds the All/Active/Done filter. ### A telling detail: no cache needed When you create a task and land back on the Tasks page, the router **re-mounts** that page, so its `resource` loads fresh data — the new task is just there. No query cache, no invalidation, no store subscription. The framework's own mount/unmount is the "cache invalidation." That's the whole philosophy: lean on the platform and the core, don't build a system. ### Run it From the repository root: ```bash npx serve -l 7310 . # open http://localhost:7310/examples/task-board/ ``` No install, no build — `index.html` uses an **import map** to point the `@zoijs/*` names at local source. > The demo is served at a sub-path here, so it passes > `{ base: "/examples/task-board" }` to the router — that's why the first load > lands on Home and every link works under the sub-path. Deployed at your site's > root, drop the `base`. ## How this avoids React/Vue/Angular-style complexity | You don't write… | Instead | |---|---| | JSX + a compiler | Real HTML in `html\`...\`` template literals | | A bundler/build config | A `