Back to Fluentui

React Router 7/Remix setup

apps/public-docsite-v9/src/Concepts/SSR/Remix.mdx

4.40.2-hotfix26.3 KB
Original Source

import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Concepts/Developer/Server-Side Rendering/React Router 7 and Remix setup" />

React Router 7/Remix setup

Installation

  1. Create a new React Router 7/Remix project or skip this step if you already have one:
bash
npx create-remix@latest fluentui-remix

# or
npx create-react-router@latest fluentui-react-router
  1. Install dependencies:
bash
# Install Fluent UI core packages
npm i @fluentui/react-components @fluentui/react-icons

# Install required Vite plugins
npm i vite-plugin-cjs-interop @griffel/vite-plugin -D

Configuration

  1. Update vite.config.ts:
ts
// Import Vite plugins
import { cjsInterop } from 'vite-plugin-cjs-interop';
import griffel from '@griffel/vite-plugin';

export default defineConfig(({ command }) => ({
  plugins: [
    reactRouter(), // or remix(),
    tsconfigPaths(),

    // Add CJS interop plugin for Fluent UI packages until they are ESM compatible
    cjsInterop({
      dependencies: ['@fluentui/react-components'],
    }),
    // Add Griffel plugin for production optimization
    command === 'build' && griffel(),
  ],
  // Required for Fluent UI icons in SSR
  ssr: {
    noExternal: ['@fluentui/react-icons'],
  },
}));
  1. Modify app/root.tsx to add Fluent UI providers:
tsx
// 1. Import Fluent UI dependencies
import { FluentProvider, webLightTheme } from '@fluentui/react-components';

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
        <meta name="fluentui-insertion-point" content="fluentui-insertion-point" />
      </head>
      <body>
        <FluentProvider theme={webLightTheme}>{children}</FluentProvider>
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}
  1. Set up SSR:
  • Reveal app/entry.client.tsx and app/entry.server.tsx files if not already present:
bash
npx react-router reveal

# or

npx remix reveal
  • Update the entry.client.tsx to wrap the router with both <RendererProvider> and <SSRProvider>:
tsx
import { createDOMRenderer, RendererProvider, SSRProvider } from '@fluentui/react-components';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { HydratedRouter } from 'react-router/dom';

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <RendererProvider renderer={createDOMRenderer()}>
        <SSRProvider>
          <HydratedRouter />
        </SSRProvider>
      </RendererProvider>
    </StrictMode>,
  );
});
  • and then update the entry.server.tsx:
tsx
// 1. Import required Fluent UI SSR utilities
import { createDOMRenderer, RendererProvider, renderToStyleElements, SSRProvider } from '@fluentui/react-components';

// 2. Define constants for style injection
const FLUENT_UI_INSERTION_POINT_TAG = `<meta name="fluentui-insertion-point" content="fluentui-insertion-point"/>`;
const FLUENT_UI_INSERTION_TAG_REGEX = new RegExp(FLUENT_UI_INSERTION_POINT_TAG.replaceAll(' ', '(\\s)*'));

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
) {
  // 3. Create Fluent UI renderer
  const renderer = createDOMRenderer();

  // ...

  return new Promise((resolve, reject) => {
    let shellRendered = false;
    // 4. Track style extraction state
    let isStyleExtracted = false;

    const { pipe, abort } = renderToPipeableStream(
      // 5. Wrap RemixServer with Fluent UI providers
      <RendererProvider renderer={renderer}>
        <SSRProvider>
          <ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />
        </SSRProvider>
      </RendererProvider>,
      {
        [callbackName]: () => {
          shellRendered = true;
          const body = new PassThrough({
            // 6. Transform stream to inject Fluent UI styles
            transform(chunk, _, callback) {
              const str = chunk.toString();
              const style = renderToStaticMarkup(<>{renderToStyleElements(renderer)}</>);

              if (!isStyleExtracted && FLUENT_UI_INSERTION_TAG_REGEX.test(str)) {
                chunk = str.replace(FLUENT_UI_INSERTION_TAG_REGEX, `${FLUENT_UI_INSERTION_POINT_TAG}${style}`);
                isStyleExtracted = true;
              }

              callback(null, chunk);
            },
          });
          // ...
        }
      }
  });
}

Usage Example

Create or update app/routes/_index.tsx:

tsx
import { Button, Card, Title1, Body1 } from '@fluentui/react-components';
import { BookmarkRegular } from '@fluentui/react-icons';

export default function Index() {
  return (
    <Card style={{ maxWidth: '400px', margin: '20px' }}>
      <Title1>Fluent UI + Remix</Title1>
      <Body1>Welcome to your new app!</Body1>
      <Button appearance="primary" icon={<BookmarkRegular />}>
        Click me
      </Button>
    </Card>
  );
}

Troubleshooting

Common Issues

  1. SSR Hydration Mismatch
Text content does not match server-rendered HTML

Fix: Check style injection in entry.server.tsx.

  1. Icons Not Rendering in SSR
Error: No "exports" main defined in node_modules/@fluentui/react-icons/package.json

Fix: Add to vite.config.ts:

ts
ssr: {
  noExternal: ["@fluentui/react-icons"],
}
  1. Module Resolution Errors
Cannot use import statement outside a module

Fix: Add to vite.config.ts:

ts
cjsInterop({
  dependencies: ["@fluentui/react-components"],
}),
  1. Development Mode Warning
@fluentui/react-provider: There are conflicting ids in your DOM.
Please make sure that you configured your application properly.
Configuration guide: https://aka.ms/fluentui-conflicting-ids

This warning occurs in development due to React's StrictMode double rendering. It can be safely ignored as it doesn't affect production builds.

Production Build Optimization

For production builds, install and configure @griffel/vite-plugin to enable build time pre-computing and transforming styles.