• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
stevekrouse

stevekrouse

reactRouter7MinimalExample

Full-stack example ReactRouter7 project
Remix of stevekrouse/reactRouter7Example
Public
Like
reactRouter7MinimalExample
Home
Code
5
backend
2
frontend
3
routes
6
shared
1
README.md
Environment variables
Branches
2
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
README.md
Code
/
README.md
Search
…
Viewing readonly version of minimal branch: v15
View latest version
README.md

React Router 7 on Val Town

A full-stack SSR + SPA reference architecture for Val Town, built with React Router 7, Hono, and SQLite. Ships as a community message board demo, but the patterns here are designed to be extracted into a reusable framework/template.

Live demo: https://react-router-7-example.val.run/

Why This Exists

Val Town gives you serverless TypeScript functions, but building a real full-stack app with SSR, client-side navigation, loaders, actions, and hydration requires wiring together several pieces. This project demonstrates one clean way to do it — and is the seed for a potential Val Town Router framework.

Architecture Overview

Rendering mermaid diagram...

Request Lifecycle

Rendering mermaid diagram...

Features

  • Server-Side Rendering — Every page is fully rendered on first visit (SEO-friendly, fast paint)
  • Client-Side Navigation — After hydration, navigation is instant SPA-style
  • Single-Fetch Data Strategy — Client navigation makes one request per route transition, not one per loader
  • Loaders & Actions — Remix-style data loading and form handling, co-located with route components
  • Redirect Handling — Server actions return redirects as JSON for the client dataStrategy to follow
  • SQLite Persistence — Val Town's built-in SQLite with idempotent migrations and seed data
  • Loading States — useNavigation() for global loading indicators

Project Structure

├── backend/
│   ├── index.ts                 # Hono HTTP entry point (SSR + data API + static files)
│   └── database/
│       ├── migrations.ts        # Schema creation + seed data
│       └── queries.ts           # All database queries
├── frontend/
│   ├── index.html               # HTML shell template
│   ├── index.tsx                # Client entry — hydration + dataStrategy
│   └── components/
│       ├── App.tsx              # (unused legacy — see routes/App.tsx)
│       ├── LoadingSpinner.tsx
│       ├── MessageForm.tsx
│       ├── MessageList.tsx
│       ├── SearchForm.tsx
│       └── SearchResults.tsx
├── routes/
│   ├── App.tsx                  # Root layout (Outlet + nav + footer)
│   ├── Home.tsx                 # Home page — topic list + create form
│   ├── Home.loader.ts           # Loads all topics
│   ├── Topic.tsx                # Topic detail — messages + reply form
│   ├── Topic.loader.ts          # Loads topic + messages
│   ├── Topic.action.ts          # Creates a message in a topic
│   ├── TopicMessage.loader.ts   # Direct link to a specific message
│   ├── Topics.action.ts         # Creates a new topic (root action)
│   ├── Search.tsx               # Search results page
│   └── Search.loader.ts         # Runs search query
├── shared/
│   ├── routes.ts                # Route tree (shared between server + client)
│   └── types.ts                 # TypeScript interfaces (Topic, Message, SearchResults)
├── README.md
└── TODO.md

Key Patterns

1. Shared Route Definitions (shared/routes.ts)

The same route tree is used on both server and client. The serverLoader() / serverAction() helpers make this work:

  • On the server: dynamically import the real loader/action module and call it
  • On the client: return a no-op stub (the dataStrategy handles fetching)

This means loader/action code (and its database dependencies) never ships to the browser.

2. Single-Fetch dataStrategy (frontend/index.tsx)

Instead of React Router calling each loader individually on the client, a custom dataStrategy makes one HTTP request with a X-Data-Request: true header. The server runs all matched loaders/actions and returns their data as keyed JSON.

3. SSR with Hydration (backend/index.ts)

On full page loads:

  1. createStaticHandler runs loaders
  2. createStaticRouter + StaticRouterProvider renders to HTML string
  3. HTML is injected into the index.html template
  4. Client picks up with hydrateRoot + createBrowserRouter

4. Action → Redirect → Reload

Form submissions go through React Router actions. On the server, the action returns a redirect(). For data requests, the server converts this to { redirect: "/path" } JSON so the client dataStrategy can follow it, triggering a client-side navigation + loader re-fetch.

URL Structure

URLDescription
/Home — list of topics + create form
/topics/:topicIdTopic detail — messages + reply form
/topics/:topicId/messages/:messageIdDeep link to a specific message
/search?q=querySearch results across topics and messages

Tech Stack

LayerTechnologyPurpose
ServerHonoHTTP routing, static files, SSR orchestration
RoutingReact Router 7SSR + SPA routing, loaders, actions
UIReact 18Components + hydration
StylingTwindTailwind-in-JS (CDN)
DatabaseVal Town SQLiteProject-scoped persistence via std/sqlite@14-main
RuntimeVal Town (Deno)Serverless hosting

Towards a Framework / Template

This project is designed to be the starting point for a reusable Val Town fullstack framework. Here's what could be extracted:

What's Already Framework-Ready

  • serverLoader() / serverAction() pattern — Generic helpers that work with any route tree
  • dataStrategy implementation — The single-fetch client strategy is app-agnostic
  • SSR pipeline in backend/index.ts — The Hono → StaticHandler → render → hydrate flow is generic
  • X-Data-Request protocol — Simple convention for distinguishing SSR vs data-only requests

Suggestions for Extraction

Rendering mermaid diagram...
  1. createValTownRouter(routes, options) — A single function that returns a Hono app with SSR, static file serving, and the data-request protocol baked in. Users just supply routes and get a working full-stack app:

    Create val
    import { createValTownRouter } from "https://esm.town/v/std/valtown-router"; import { routes } from "./shared/routes.ts"; export default createValTownRouter(routes, { title: "My App" });
  2. Centralized deps.ts version pinning — React, React Router, and React DOM versions are repeated across many files via esm.sh URLs (currently 7+ files all reference react@18.2.0). A framework could export these from a single manifest:

    Create val
    // deps.ts — one place to update versions export { default as React } from "https://esm.sh/react@18.2.0"; export { hydrateRoot } from "https://esm.sh/react-dom@18.2.0/client"; export { createBrowserRouter, ... } from "https://esm.sh/react-router@7.5.0?deps=react@18.2.0,react-dom@18.2.0";
  3. createRoutes() helper — A typed helper that wraps the serverLoader/serverAction pattern so users don't need to understand the server/client bifurcation:

    Create val
    createRoutes([ { path: "/", Component: App, children: [ { index: true, Component: Home, loader: "./Home.loader.ts" }, { path: "topics/:id", Component: Topic, loader: "./Topic.loader.ts", action: "./Topic.action.ts" }, ]} ])
  4. File-based route discovery — The routes/ directory already follows Remix-style naming conventions (Home.tsx, Home.loader.ts, Topic.action.ts). A framework could auto-discover routes from the filesystem, eliminating the manual shared/routes.ts file entirely:

    routes/
      _layout.tsx        → root layout (App)
      _index.tsx         → / (Home)
      _index.loader.ts   → Home loader
      topics.$id.tsx     → /topics/:id (Topic)
      topics.$id.loader.ts
      topics.$id.action.ts
      search.tsx         → /search
      search.loader.ts
    
  5. HTML template generation — Instead of a manual index.html, the framework could generate the shell with the right scripts, twind, and catch handler included. No more forgetting to add <script src="https://esm.town/v/std/catch"></script>.

  6. Project-scoped SQLite by default ✅ Done! — Migrated from @stevekrouse/sqlite (user-scoped) to std/sqlite@14-main (project-scoped). Each fork now gets its own isolated database automatically.

  7. Middleware hooks — Auth, logging, error boundaries, CORS, etc. as composable Hono middleware that plugs into the framework:

    Create val
    export default createValTownRouter(routes, { middleware: [authMiddleware(), corsMiddleware()], errorBoundary: MyErrorBoundary, });
  8. CLI / init template — A createValTownApp script or a Val Town project template that scaffolds the full directory structure with a working starter app in one click.

Open Questions

  • How much of the Hono boilerplate can be hidden vs left explicit for power users?
  • What's the right story for CSS — keep Twind CDN, support CSS modules, or Tailwind build?
  • Should the framework ship as a single Val Town val (importable library) or as an npm package on esm.sh?
  • Could hydration data be embedded in the HTML (like Remix does) instead of requiring a separate client fetch on first load?

Getting Started

  1. Fork this val on Val Town
  2. The database auto-initializes with seed data on first request
  3. Visit the HTTP endpoint to see the app
  4. Modify routes in shared/routes.ts, add components in routes/, and queries in backend/database/

Dependencies

All dependencies are loaded via esm.sh with pinned versions:

  • react@18.2.0
  • react-dom@18.2.0
  • react-router@7.5.0
  • hono@4.1.0
  • @valtown/std/utils (for readFile / serveFile)
  • @std/sqlite@14-main (Val Town project-scoped SQLite)
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.