communitypackages:react-router-ssr

v6.0.0Published 2 weeks ago

React-Router-SSR

Simple isomorphic React SSR for Meteor with subscribed data re-hydration

Supporting the project ❤️

This project, like all of the projects maintained by the Meteor Community Packages org, takes time and hard work to keep updated. If you find this or any of our other packages useful, consider visiting the sponsor section of a repo and sending some love to the dedicated developers that keep your favorite packages up to date.

Install

  1. First install NPM dependencies

    npm install --save react@19 react-dom@19 react-router-dom@6

    React 19 is required. This package renders and hydrates the whole document, and relies on React 19's native document-metadata hoisting for the <head> (see Managing the document head). It will refuse to load on React 18. No react-helmet is needed or supported.

  2. Install communitypackages:react-router-ssr

    meteor add communitypackages:react-router-ssr

Upgrading from v5

v6 is a rewrite around React 19 and whole-document rendering. Coming from v5, there are four changes to make — most are deletions.

  1. Move to React 19. React 18 is no longer supported (the package won't load on it). Bump React and drop two packages v5 required that v6 no longer needs:

    npm install --save react@19 react-dom@19 react-router-dom@6
    npm uninstall react-helmet abort-controller
    

    react-router-dom stays on v6. abort-controller is now bundled by the package, and react-helmet is replaced by native head management (next).

  2. Replace react-helmet with native document metadata. v6 renders and hydrates the whole document and uses React 19's metadata hoisting instead of react-helmet — render <title> / <meta> directly in your components:

    -import { Helmet } from "react-helmet";
    -
     function ProfilePage() {
       return (
         <>
    -      <Helmet>
    -        <title>Profile</title>
    -      </Helmet>
    +      <title>Profile</title>
           {/* … */}
         </>
       );
     }
  3. Remove the mount element and static <head>. v6 renders the entire <html> document, so a <div id="react-target"> or a hand-written <head> in a static HTML file is no longer used — delete them. renderWithSSR no longer takes an options argument; the renderTarget option has been removed:

    -renderWithSSR(AppRoutes, { renderTarget: "react-app" });
    +renderWithSSR(AppRoutes);
  4. Using the Rspack bundler? Add one bit of server config so react-router isn't duplicated during SSR — see Using with the Rspack bundler.

Package Exports 📦

renderWithSSR(routes) - Isomorphic app rendering. Renders and hydrates the whole <html> document, so there is no mount element to configure.

  • routes - A JSX element or array of JSX elements that represent the routes of your app.

    1import { renderWithSSR } from "meteor/communitypackages:react-router-ssr";
    2
    3const AppRoutes = [
    4  { path: "/", element: <Home /> },
    5  { path: "/about", element: <About /> },
    6]
    7
    8renderWithSSR(AppRoutes);

useSubscribeSuspense(name, ...args) - A server enabled version of react-meteor-data's suspendable useSubscribe hook. Arguments are same as Meteor.subscribe.

Usage ⚙️

This package renders and hydrates the entire <html> document — it produces its own <html>, <head>, and <body>. You do not need a mount element (<div id="…">) or a hand-written <head> in a static HTML file; anything you put there is replaced on hydration. Configure the <head> from your components instead — see Managing the document head.

Call renderWithSSR from shared code, such as a /both/main.jsx file, or a file that is imported into your mainModule for both the client and server.

1import { renderWithSSR } from "meteor/communitypackages:react-router-ssr";
2
3import React from "react";
4import DashboardPage from "./imports/ui/pages/dashboard";
5import ProfilePage from "./imports/ui/pages/profile";
6import LoginPage from "./imports/ui/pages/login";
7
8const AppRoutes = [
9  { path: "/", element: <DashboardPage /> },
10  { path: "/profile/:username", element: <ProfilePage /> },
11  { path: "/login", element: <LoginPage /> },
12];
13
14// Alternatively you can use a JSX fragment
15// const AppRoutes = (
16//   <>
17//     <Route path="/" element={<DashboardPage />} />
18//     <Route path="/profile/:username" element={<ProfilePage />} />
19//     <Route path="/login" element={<LoginPage />} />
20//   </>
21// );
22
23renderWithSSR(AppRoutes);

Managing the document head

This package renders the entire <html> document and hydrates it with hydrateRoot(document, …). It deliberately does not render a <title> or any <meta> tags of its own — instead it relies on React 19's native document metadata support: any <title>, <meta>, or <link> you render from a route or component is hoisted into <head> automatically, on the server and on the client. This replaces react-helmet entirely.

Just render the tags where it's convenient — typically at the top of each page component:

1function ProfilePage() {
2  const { username } = useParams();
3
4  return (
5    <>
6      <title>{`${username} · MyApp`}</title>
7      <meta name="description" content={`Profile page for ${username}`} />
8      {/* …page content… */}
9    </>
10  );
11}

During SSR the correct title/meta are streamed into the served HTML, and on client-side navigation React updates them as the matched route changes — no extra library or provider required.

Using with the Rspack bundler

If your app uses Meteor's Rspack bundler (@meteorjs/rspack), one small piece of configuration is required for SSR to work.

Rspack bundles your app's code separately from this Meteor-compiled package, in its own module registry. That means Rspack bundles its own copy of react-router, distinct from the copy this package loads. React Router shares its routing state through React context, and React context only works across a single shared instance — so with two copies the package's <RouterProvider> / <StaticRouterProvider> can't supply context to a <Link> / <NavLink> in your components. This breaks both builds:

  • Server: SSR fails with TypeError: Cannot destructure property 'future' of 'useContext(...)' as it is null.
  • Client: after hydration, <NavLink> throws and React Router renders its error boundary.

Rspack already externalizes react and react-dom for exactly this reason — react-router just isn't in its default list. Add it yourself in rspack.config.js with Meteor's compileWithMeteor helper, which tells Rspack to let Meteor provide these packages at runtime (one shared instance instead of a bundled duplicate). Apply it to both the client and server builds:

1const { defineConfig } = require("@meteorjs/rspack");
2
3module.exports = defineConfig((Meteor) => ({
4  // Externalize react-router so it isn't duplicated across the Meteor/Rspack build
5  // boundary (see above). Needed on both client and server.
6  ...Meteor.compileWithMeteor([
7    "react-router",
8    "react-router-dom",
9    "@remix-run/router",
10  ]),
11}));

This only affects apps using the Rspack bundler. With Meteor's classic bundler your app code and this package share one module registry, so there is nothing to configure.