A custom React Server-Side Rendering (SSR) application with streaming rendering, code splitting, and hot module replacement.
- π Server-Side Rendering (SSR) - Powered by React 18's streaming capabilities
- π¦ Code Splitting - Dynamic imports with @loadable/component
- π₯ Hot Module Replacement (HMR) - Real-time updates in development
- β‘ Webpack 5 - Modern build toolchain
- π― TypeScript - Full type support
- π Koa.js - Lightweight server framework
- π¨ CSS Support - CSS files and style extraction
- π§ React Router v6 (SSR) - Server-rendered routing
- π§ͺ React Query (SSR) - Data prefetch & hydration
- React 18
- TypeScript
- React Router v6
- @loadable/component (Code Splitting)
- @tanstack/react-query (Data fetching & SSR hydration)
- Koa.js
- @koa/router
- @loadable/server
- Webpack 5
- Babel
- CSS Loader & Mini CSS Extract Plugin
# Clone the repository
git clone https://github.com/liyincode/react-custom-ssr.git
cd react-custom-ssr
# Install dependencies (pnpm recommended)
pnpm install
# or use npm
npm install# Start development server
npm run dev
# or
pnpm dev
# Start mock API server in another terminal
npm run mockThe development server will start two services:
- SSR Server: http://localhost:3000 (Main application)
- HMR Server: http://localhost:8099 (Hot reload assets)
# Build for production
npm run build
# or
pnpm build# Clean build directory
npm run clean
# or
pnpm cleanreact-custom-ssr/
βββ app/ # Application code
β βββ client/ # Client entry
β β βββ index.tsx # Client hydration (BrowserRouter + React Query)
β βββ server/ # Server code
β βββ index.tsx # SSR server (Koa + @loadable/server)
β βββ app.tsx # Route matching + React Query prefetch + dehydrate
β βββ html.ts # HTML template generation (inject dehydrated state)
β βββ stream.ts # Streaming render logic
βββ config/ # Build configuration
β βββ constants.js # Constants configuration
β βββ webpack.config.js # Webpack base config
β βββ webpack.dev.js # Development config
β βββ webpack.prod.js # Production config
βββ scripts/ # Script files
β βββ dev.js # Development server startup script
βββ src/ # Source code
β βββ index.tsx # App component (useRoutes + Koa context provider)
β βββ index.css # App styles
β βββ routes.tsx # Route definitions with optional prefetch metadata
β βββ Home.tsx # Example page using useQuery
β βββ Post.tsx # Example dynamic route using useQuery
β βββ api.ts # API client (mock server base URL)
βββ build/ # Build output directory
βββ client/ # Client build files
βββ server.js # Server build file
βββ loadable-stats.json # Code splitting stats
The project includes three TypeScript configuration files:
tsconfig.json- Base configurationtsconfig.client.json- Client-specific configurationtsconfig.server.json- Server-specific configuration
- Development: Enables HMR, proxies static assets to dev server
- Production: Code minification, CSS extraction, optimized bundling
NODE_ENV- Runtime environment (development/production)PORT- Server port (default: 3000)
The application uses React 18's renderToPipeableStream API for streaming rendering, providing better user experience and performance.
Component-level code splitting implemented through @loadable/component to reduce initial bundle size.
Development environment supports hot module replacement, allowing you to see code changes without page refresh.
- Define routes in
src/routes.tsx, and optionally attachqueryKeyandloadDatafor SSR prefetching.queryKeycan be a function that receives dynamic params.
// src/routes.tsx
import { Params, RouteObject } from "react-router-dom";
import { QueryKey } from "@tanstack/react-query";
import loadable from "@loadable/component";
import { api } from "./api";
const Home = loadable(() => import("./Home"), { ssr: true });
const Post = loadable(() => import("./Post"), { ssr: true });
type PrefetchRouteObject = RouteObject & {
queryKey?: QueryKey | ((params: Params<string>) => QueryKey);
loadData?: (params: Params<string>) => Promise<unknown>;
};
export const routes: PrefetchRouteObject[] = [
{ path: "/", element: <Home />, queryKey: ["home-data"], loadData: () => api.getHomeData() },
{ path: "/post/:id", element: <Post />, queryKey: (p) => ["post", p.id!], loadData: (p) => api.getPostById(p.id!) },
];- On the server, matched routes are prefetched and dehydrated, then injected into HTML.
// app/server/app.tsx (excerpt)
import { dehydrate, QueryClient, QueryClientProvider, HydrationBoundary } from "@tanstack/react-query";
import { matchRoutes } from "react-router-dom";
import { StaticRouter } from "react-router-dom/server";
import { routes } from "@/routes";
const queryClient = new QueryClient();
const matches = matchRoutes(routes, ctx.req.url ?? "");
// prefetch for each route that declares queryKey + loadData ...
const dehydratedState = dehydrate(queryClient);- On the client, hydrate React Query state and the app.
// app/client/index.tsx (excerpt)
const dehydratedState = JSON.parse(document.getElementById("__REACT_QUERY_STATE__")?.textContent || "{}");
hydrateRoot(root, (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<App />
</HydrationBoundary>
</QueryClientProvider>
</BrowserRouter>
));- A lightweight mock server is provided via
json-serverfor demo data fetching. - Start it with
npm run mock(default:http://localhost:8007). - API client lives in
src/api.ts.
MIT License