apps/public-docsite-v9/src/Concepts/SSR/Remix.mdx
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Concepts/Developer/Server-Side Rendering/React Router 7 and Remix setup" />npx create-remix@latest fluentui-remix
# or
npx create-react-router@latest fluentui-react-router
# 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
vite.config.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'],
},
}));
app/root.tsx to add Fluent UI providers:// 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>
);
}
app/entry.client.tsx and app/entry.server.tsx files if not already present:npx react-router reveal
# or
npx remix reveal
entry.client.tsx to wrap the router with both <RendererProvider> and <SSRProvider>: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>,
);
});
entry.server.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);
},
});
// ...
}
}
});
}
Create or update app/routes/_index.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>
);
}
Text content does not match server-rendered HTML
Fix: Check style injection in entry.server.tsx.
Error: No "exports" main defined in node_modules/@fluentui/react-icons/package.json
Fix: Add to vite.config.ts:
ssr: {
noExternal: ["@fluentui/react-icons"],
}
Cannot use import statement outside a module
Fix: Add to vite.config.ts:
cjsInterop({
dependencies: ["@fluentui/react-components"],
}),
@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.
For production builds, install and configure @griffel/vite-plugin to enable build time pre-computing and transforming styles.