Skip to content

Migrate from Remix

Migrate from Remix / React Router#

Remix-style loaders, actions, and progressive forms map closely onto Otok. The biggest difference: Otok is HTML-first with soft navigation, not a full client-side router.

Concept mapping#

Remix Otok
loader loader
action action
useLoaderData data page prop
useActionData actionData page prop
<Form method="post"> native <form method="post">
nested routes / outlets nested _layout.tsx + {children}
client-side routing soft navigation HTML swaps
Resource routes Hono API routes via configure / createOtokHandler

Actions and PRG#

import { redirect, validationError, type OtokActionContext, type OtokPageProps } from "@kamod-ch/otok/server";

export async function action({ formData }: OtokActionContext) {
  const name = String(formData?.get("name") ?? "").trim();
  if (!name) {
    validationError({ fieldErrors: { name: "Required" }, values: { name } });
  }
  await save(name);
  redirect("/projects", 303);
}

export default function Page({ actionData }: OtokPageProps) {
  const errors = (actionData as { fieldErrors?: Record<string, string[]> } | undefined)?.fieldErrors;
  return (
    <form method="post">
      <input name="name" aria-invalid={Boolean(errors?.name)} />
      <button>Save</button>
    </form>
  );
}

Soft navigation (optional SPA feel)#

createOtokClient({ registry: islandModules, softNav: { forms: true } });

Mark layout chrome that must update per route with data-otok-swap.

What not to expect#

  • No React ecosystem (Preact components)
  • No useFetcher — use forms, fetch, or islands
  • No nested pathless client routers

Next steps#