App structure
A page has two Nix entry points: the app, which runs in the browser, and the server entry, which renders the HTML around it. Both are named in the Vite plugin's pages option.
The app#
An app is a directory. Its default.nix returns an attrset:
let inherit (import <web/dom>) html; inherit (import <web/style>) css; io = import <web/io>; in { # Global CSS for the page. styles = "body { font-family: system-ui; }"; # The initial state. init = 0; # The next state, given the current one and an action. update = count: action: if action.type == "increment" then count + 1 else count; # The children to render, given the state. render = count: [ (html.h1 { class = css "color: teal;"; } "Clicked ${toString count} times") (html.button { on.click = event: io.dispatch "increment" null; } "Increment") ]; }
Every attribute is optional, but init and update come as a pair. Without them, the app has no state.
The app is every .nix file in its directory and below. Split it up with relative imports, like import ./header.nix.
Actions#
Handlers return effects. io.dispatch sends an action, { type; payload; }, to update, and the app re-renders with the new state.
update can return an effect instead of a state, to call out to the host. See Host IO.
The server entry#
The server entry is a function that returns the page's HTML. It runs at build time, and on each request in dev:
{ children, assets, scripts, }: let inherit (import <web/dom>) html; in html.html { lang = "en"; } [ (html.head [ (html.title "Counter") assets ]) (html.body [ children scripts ]) ]
Its inputs:
children: the app's initial render, in the element it mounts into.assets: what the app needs in the head, like its styles.scripts: the script that mounts the app.
Each is empty when the page doesn't use it.