React Router 7+ with Server-Side Rendering (SSR)

React Router is a standard library for routing in React applications that enables dynamic routing with server-side rendering capabilities. With React Router v7+, you have a powerful and flexible system for managing navigation and rendering in both server and client environments for your applications.

Understanding React Router 7+ with SSR

React Router v7 introduces a refined approach to routing with several key SSR-focused features:

  • Loaders: Data fetching functions that run on the server before a route renders

  • Actions: Server-side functions that process form submissions and data mutations

  • Data APIs: Methods to access loaded data throughout your application

  • Hydration: Seamless transition from server-rendered content to client interactivity

  • Route Configuration: Type-safe route definitions that support nested layouts

Flagship implementation with SSR

To successfully implement Flagship in a React Router 7+ SSR application, follow these steps:

  • Fetch Flagship data on the server using loader functions

  • Pass initial flag data and visitor information to the client

  • Set up the FlagshipProvider at the root level with hydration support

  • Access flags in components using hooks

  • Send analytics hits from client components

1. Set up the Flagship Provider with SSR support

Create a custom provider component that initializes Flagship with SSR hydration:

// app/helpers/FsProvider.tsx
import FlagshipProvider, {
  LogLevel,
  type SerializedFlagMetadata,
  type VisitorData,
} from "@flagship.io/react-sdk";

export function FsProvider({
  children,
  visitorData,
  initialFlagsData,
}: {
  children: React.ReactNode;
  visitorData?: VisitorData;
  initialFlagsData?: SerializedFlagMetadata[];
}) {
  return (
    <FlagshipProvider
      envId={import.meta.env.VITE_ENV_ID}
      apiKey={import.meta.env.VITE_API_KEY}
      logLevel={LogLevel.DEBUG}
      initialFlagsData={initialFlagsData} // Hydrates client with server-fetched flags
      visitorData={visitorData || null}
    >
      {children}
    </FlagshipProvider>
  );
}

2. Fetch Flagship data in a server loader function

Use React Router's server loader function to fetch initial flag data before server rendering:

export async function loader({ params }: Route.LoaderArgs) {
  // This runs on the server during SSR
  const visitorData = {
    visitorId: "visitorId", // In production, generate or retrieve a unique ID
    context: {
      key: "value",
    },
    hasConsented: true,
  };
  
  // Initialize Flagship and fetch flags on the server
  const visitor = await getFsVisitorData(visitorData);
  
  // Return data that will be available to the client
  return {
    fsInitialFlagsData: visitor?.getFlags().toJSON(), // Serialized flags for hydration
    visitorData, // Pass visitor data to maintain consistency
  };
}

Create a helper function to initialize the Flagship SDK for server and client:

// app/helpers/flagship.ts

import { Flagship, DecisionMode, LogLevel } from "@flagship.io/react-sdk";

export async function startFlagshipSDK() {
  // Works in both server and client environments
  return await Flagship.start(
    import.meta.env.VITE_ENV_ID,
    import.meta.env.VITE_API_KEY,
    {
      logLevel: LogLevel.DEBUG,
      fetchNow: false,
      decisionMode: DecisionMode.DECISION_API,
    }
  );
}

export async function getFsVisitorData(visitorData) {
  // Start the SDK and get the Flagship instance
  const flagship = await startFlagshipSDK();

  // Create a visitor
  const visitor = flagship.newVisitor({
    visitorId: visitorData.visitorId,
    context: visitorData.context,
    hasConsented: visitorData.hasConsented,
  });

  // Fetch flag values for the visitor
  await visitor.fetchFlags();

  // Return visitor instance
  return visitor;
}

3. Create a layout component with SSR-aware FlagshipProvider

Set up your root layout component that handles hydration from server to client:

// app/root.tsx

export function Layout({ children }: { children: React.ReactNode }) {
  // Get initial flags data and visitor data from the server loader
  const data = useRouteLoaderData<typeof loader>("root");
  
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <FsProvider
          initialFlagsData={data?.fsInitialFlagsData} // Server-fetched flags for hydration
          visitorData={data?.visitorData} // Visitor data from server
        >
          <div className={"container"}>
            <div className="nav">
              <Nav />
            </div>
            <main className={"main"}>{children}</main>
          </div>

          <ScrollRestoration />
          <Scripts />
        </FsProvider>
      </body>
    </html>
  );
}

4. Access flags in server and client components

There are two main ways to access flags in your components:

Option 1: Use the useFsFlag hook in your page components

// app/routes/anotherPage.tsx

import { useFsFlag } from "@flagship.io/react-sdk";

export default function AnotherPage() {
  // Get the flag `btnColor` using useFsFlag hook (client-side)
  const flag = useFsFlag("btnColor");
  const flagValue = flag.getValue("#dc3545");

  return (
    <>
      <h1>This is another page</h1>
      <p>flag key: btnColor</p>
      <p>flag value: {flagValue}</p>
      <button style={{ background: flagValue }}>Click me</button>
    </>
  );
}

Option 2: Create dedicated components for flag access

For more complex use cases, you can create dedicated components:

// app/components/MyFlagComponent.tsx

import { useFsFlag } from "@flagship.io/react-sdk";

export function MyFlagComponent() {
    const myFlag = useFsFlag("my_flag_key");
    return <p>flag value: {myFlag.getValue("default-value")}</p>
}

Use client components in your routes:

// app/routes/home.tsx

import { MyFlagComponent } from "~/components/MyFlagComponent";
import { MyButtonSendHit } from "~/components/MyButtonSendHit";

// This can be a server component that includes client components
export default function Home() {
  return (
    <>
      <h1>Example of Flagship implementation with React Router SSR</h1>
      <p>flag key: my_flag_key</p>
      {/* Client component that accesses Flagship */}
      <MyFlagComponent />
      <MyButtonSendHit />
    </>
  );
}

5. Send analytics hits from client components

To send analytics hits from your client components, use the Flagship SDK:

// app/components/MyButtonSendHit.tsx

import { useFlagship, HitType, EventCategory } from "@flagship.io/react-sdk";

export function MyButtonSendHit() {
  const fs = useFlagship();

  const onSendHitClick = () => {
    fs.sendHits({
      type: HitType.EVENT,
      category: EventCategory.ACTION_TRACKING,
      action: "click button",
    });
  };

  return (
    <button
      style={{ width: 100, height: 50 }}
      onClick={() => {
        onSendHitClick();
      }}
    >
      Send hits
    </button>
  );
}

Server-side considerations for SSR

When using React Router 7+ with SSR, there are several important considerations:

  1. Flag consistency: The flags fetched on the server must match the initial state on the client to prevent flickering.

  2. Visitor identity: Generate or retrieve consistent visitor IDs on the server that persist to the client.

  3. Environment variables: Ensure environment variables are accessible in both server and client contexts.

  4. Performance optimization: Fetch flag data as early as possible in the server request lifecycle.

This approach ensures a smooth transition from server-rendered content to client-side interactivity without any flag flickering or inconsistencies while handling potential errors gracefully.

Do you have any feedback or suggestions? Would you like to see another tutorial? Contact us with your thoughts [email protected].

You can view the full code for this example on GitHub

Last updated

Was this helpful?