cursery.nixDocs

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:

src/app/default.nix
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:

src/page.nix
{
  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.