FrançaisPlayground

Routing

@fluixi/start gives you file-based routing: files in src/routes/ become routes.

src/routes/
  index.tsx          →  /
  about.tsx          →  /about
  users/
    layout.tsx       →  wraps everything under /users
    index.tsx        →  /users
    [id].tsx         →  /users/:id
  blog/
    [...slug].tsx    →  /blog/* (catch-all)
  (auth)/
    login.tsx        →  /login   — the group is not in the URL

Four conventions, and that is the whole file layer:

File Meaning
index.tsx the directory's own path
[id].tsx one dynamic segment, matched as :id
[...slug].tsx catch-all — matches the rest of the path
layout.tsx wraps the routes beside and below it
(name)/ grouping only — organises files without adding a URL segment

A route group (auth) is flattened: (auth)/login.tsx serves /login, not /auth/login. It exists so a set of routes can share a folder — and, if you add one, a layout.tsx — without that folder appearing in the URL.

Dynamic segments and params

[param] captures a segment. useParams returns an accessor, because navigating between two matches of the same route changes the params without unmounting the component:

import { useParams } from '@fluixi/start/router';

export default function Post() {
  const params = useParams<{ slug: string }>();
  return <h1>{params().slug}</h1>;
}

Related hooks, for narrower needs:

const id = useParam('id');                  // one param, as an accessor
const own = useLevelParams();               // only the params this layer introduced
const matched = useMatch(() => '/users/:id'); // is this pattern currently matched?

Nested layouts

A layout.tsx wraps its child routes around an <Outlet/>:

import { Outlet } from '@fluixi/start/router';

export default function UsersLayout() {
  return (
    <div class="users">
      <Sidebar />
      <Outlet />
    </div>
  );
}

A layout stays mounted while you navigate between the routes inside it, so state it holds — a scroll position, an open panel, a fetched list — survives the transition. That is the main reason to reach for one.

Navigation

<Link> navigates on the client; a plain <a href> does a full page load.

import { Link } from '@fluixi/start/router';

<Link href="/about">About</Link>
<Link href="/users/42" replace>Replace history entry</Link>
<Link href="/docs" activeClass="on">Docs</Link>

activeClass is applied while the link matches the current path, so a nav highlight needs no extra wiring. active overrides that decision when you need something other than an exact path match. A is an alias for Link.

For programmatic navigation:

import { useNavigate } from '@fluixi/start/router';

const navigate = useNavigate();
navigate('/dashboard');
navigate('/login', { replace: true });

Location and query string

useLocation() returns a live object — read properties directly, no call:

const location = useLocation();
location.pathname;   // reactive

useSearchParams is a getter/setter pair. Setting a key to null removes it:

const [params, setParams] = useSearchParams();

params().page;                        // "2"
setParams({ page: '3' });             // ?page=3
setParams({ page: null });            // removes it
setParams({ page: '1' }, { replace: true });

Loading data

A route file can export routeData, and the nearest one is available through useRouteData:

export function routeData({ params }) {
  return getUser(params().id);   // params is an accessor, not a plain object
}

export default function User() {
  const user = useRouteData<User>();
  return <h1>{user()?.name}</h1>;
}

useRouteData returns an accessor that suspends under <Suspense> while loading, so a boundary above it shows the fallback rather than a flash of empty state.

For data not tied to a route, createAsync wraps a fetcher into a resource:

import { createAsync, cache } from '@fluixi/start/router';

const getUser = cache((id: string) => fetchUser(id), 'user');

const user = createAsync(() => getUser(props.id));

cache gives the function a stable key so identical calls deduplicate, results transfer from server to client during hydration instead of being fetched twice, and revalidate('user') can invalidate them.

Mutations

action wraps a mutation so its progress is observable:

import { action, useSubmission, Form } from '@fluixi/start/router';

const save = action(async (data: FormData) => {
  await updateProfile(data);
}, 'save-profile');

function Profile() {
  const submission = useSubmission(save);

  return (
    <Form action={save}>
      <input name="name" />
      <button disabled={!!submission()?.pending}>Save</button>
    </Form>
  );
}

Use <Form>, not a plain <form action={save}> — the action is a function, and an action attribute has to be a URL. <Form> builds the FormData, calls the action without a page navigation, and still emits method="post" with the action's real URL in the markup, so a submit works before JavaScript loads.

useSubmission reports the in-flight state — pending, result, error — along with retry and clear, which is what a form needs to disable its button and show a failure without tracking that by hand.

Next: Server functions.