Params

Params demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import { Component, get } from '@expressive/react';
import { Link, Route, Router } from '@expressive/router';
import type { ReactNode } from 'react';

const PAGES = ['intro', 'install', 'api'];

let SEEN = 0;

export default () => (
  <Router>
    <Route as={Frame}>
      <Route as={Index} />
      <Route to="docs" as={Section}>
        <Route to=":page" as={Page} />
      </Route>
    </Route>
  </Router>
);

const Frame = (props: { children?: ReactNode }) => (
  <div className="container">
    <h1>Params</h1>
    <p>
      A <code>:page</code> segment captures whatever sits in that position, and the
      page reads it back off its own route. Handing <code>goto</code> an object
      swaps a param in place instead of composing a path.
    </p>
    <nav className="nav">
      <Link to="/">Home</Link>
      <Link to="/docs/intro">Docs</Link>
    </nav>
    <div className="view">{props.children}</div>
    <small>
      Same-pattern navigation reconciles rather than remounts, so the instance
      number below holds while you switch pages - and only changes when you leave
      the section and come back. The section chrome persists for the same reason.
    </small>
  </div>
);

const Index = () => <p>Pick Docs above to enter a section with a param.</p>;

const Section = (props: { children?: ReactNode }) => (
  <div className="section">
    <header>docs section</header>
    {props.children}
  </div>
);

class Page extends Component {
  route = get(Route);
  instance = ++SEEN;

  render() {
    const { route, instance } = this;

    return (
      <>
        <h2>{route.match?.page}</h2>

        <div className="row">
          {PAGES.map((page) => (
            <button key={page} onClick={() => route.goto({ page })}>
              {page}
            </button>
          ))}
        </div>

        <small>page instance #{instance}</small>
      </>
    );
  }
}

App.css

.nav {
  display: flex;
  gap: var(--s3);
  width: 100%;
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--accent-soft);
}

.nav a {
  cursor: pointer;
}

.view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  width: 100%;
}

.section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.section header {
  align-self: flex-start;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.section h2 {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--t-xl);
  color: var(--accent);
}

.row {
  display: flex;
  gap: var(--s2);
}

.missing {
  margin: 0;
  color: var(--muted);
}