microsoft / fluentui

Fluent UI web represents a collection of utilities, React components, and web components for building web applications.
https://react.fluentui.dev
Other
18.58k stars 2.74k forks source link

[Docs]: Add a recipe for server-side rendering with Remix framework #33317

Open r007 opened 5 days ago

r007 commented 5 days ago

Area

React Components (https://react.fluentui.dev)

What kind of documentation issue are you reporting?

Is there a specific documentation page you are reporting?

Developer --> Server-Side Rendering page

Description

Hi guys,

Can you please add my recipe for server-side rendering with remix-run framework to the documentation? It supports everything without issues. This is the only fully working solution on github. Most other repos contain an outdated code.

Demo / Starter template

https://github.com/r007/remix-fluentui-v9

Setting up Vite config for Remix

To make it work need to unwrap default imports from Fluent UI during SSR. Install CJS interop plugin for Vite:

# Using Yarn
yard add -D vite vite-plugin-cjs-interop

# Using NPM
npm install -D vite vite-plugin-cjs-interop

Then open up vite.config.ts and paste this code:

import {resolve} from 'path'
import {vitePlugin as remix} from '@remix-run/dev'
import {defineConfig} from 'vite'
import {cjsInterop} from 'vite-plugin-cjs-interop'

export default defineConfig({
  ssr: {
    noExternal: ['@fluentui/react-icons']
  },
  plugins: [
    remix({
      future: {
        v3_fetcherPersist: true,
        v3_relativeSplatPath: true,
        v3_throwAbortReason: true,
        v3_singleFetch: true,
        v3_lazyRouteDiscovery: true
      }
    }),
    cjsInterop({
      // List of CJS dependencies that require interop
      dependencies: [
        '@fluentui/react-components',
        '@fluentui/react-nav-preview',
        '@fluentui/react-list-preview',
        '@fluentui/react-virtualizer',
        '@fluentui/react-motion-components-preview'
      ]
    })
  ],
  resolve: {
    alias: {
      '~': resolve(__dirname, './app')
    }
  },
  server: {
    port: 3000
  }
})

Setting up Fluent UI

1) Install the dependencies

# Using Yarn
yarn add @fluentui/react-components isbot

# Using NPM
npm install @fluentui/react-components isbot

2) Modify the entry.server.tsx file under your app folder with the following content:

import type {EntryContext} from '@remix-run/node'
import {PassThrough} from 'node:stream'
import {RemixServer} from '@remix-run/react'
import {createReadableStreamFromReadable} from '@remix-run/node'
import {renderToStaticMarkup, renderToPipeableStream} from 'react-dom/server'
import {
  createDOMRenderer,
  RendererProvider,
  renderToStyleElements,
  SSRProvider
} from '@fluentui/react-components'
import {isbot} from 'isbot'

const ABORT_DELAY = 5000

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const renderer = createDOMRenderer()
  const callbackName = isbot(request.headers.get('user-agent'))
    ? 'onAllReady'
    : 'onShellReady'

  return new Promise((resolve, reject) => {
    let shellRendered = false
    let isStyleExtracted = false

    const {pipe, abort} = renderToPipeableStream(
      <RendererProvider renderer={renderer}>
        <SSRProvider>
          <RemixServer context={remixContext} url={request.url} />
        </SSRProvider>
      </RendererProvider>,
      {
        [callbackName]: () => {
          shellRendered = true
          const body = new PassThrough({
            transform(chunk, _, callback) {
              const str: string = chunk.toString()
              // Converting Fluent UI styles to style elements. 👇
              const style = renderToStaticMarkup(
                <>{renderToStyleElements(renderer)}</>
              )

              if (!isStyleExtracted) {
                if (str.includes('__STYLES__')) {
                  // Apply Fluent UI styles to markup.
                  chunk = str.replace('__STYLES__', style)
                  isStyleExtracted = true
                }
              }

              callback(null, chunk)
            }
          })
          const stream = createReadableStreamFromReadable(body)

          responseHeaders.set('Content-Type', 'text/html')

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode
            })
          )

          pipe(body)
        },
        onShellError(error: unknown) {
          reject(error)
        },
        onError(error: unknown) {
          responseStatusCode = 500
          if (shellRendered) {
            console.error(error)
          }
        }
      }
    )

    setTimeout(abort, ABORT_DELAY)
  })
}

3) Modify the entry.client.tsx file under your app folder:

/**
 * By default, Remix will handle hydrating your app on the client for you.
 * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
 * For more information, see https://remix.run/file-conventions/entry.client
 */

import {RemixBrowser} from '@remix-run/react'
import {startTransition, StrictMode} from 'react'
import {hydrateRoot} from 'react-dom/client'

const hydrate = async () => {
  await startTransition(() => {
    hydrateRoot(
      document,
      <StrictMode>
        <RemixBrowser />
      </StrictMode>
    )
  })
}

if (window.requestIdleCallback) {
  window.requestIdleCallback(hydrate)
} else {
  window.setTimeout(hydrate, 1)
}

4) Finally add your code to the root.tsx file in your app folder:

import type {MetaFunction} from '@remix-run/node'
import {Links, Meta, Outlet, Scripts, ScrollRestoration} from '@remix-run/react'
import {FluentProvider, webLightTheme} from '@fluentui/react-components'

export const meta: MetaFunction = () => [
  {title: 'Create Remix App'},
  {
    name: 'description',
    content: 'A sample app to demonstrate ssr rendering in remix'
  }
]

const isBrowser = () => {
  return (
    typeof window !== 'undefined' &&
    window.document &&
    window.document.createElement
  )
}

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 />
        {!isBrowser() && '__STYLES__'}
      </head>
      <body>
        {/* 👇 Apply fluent theme to children */}
        <FluentProvider theme={webLightTheme}>{children}</FluentProvider>
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  )
}

export default function App() {
  return <Outlet />
}

Validations

sopranopillow commented 4 hours ago

Hi @r007, if you would like you could contribute by adding the recipe to https://github.com/microsoft/fluentui/tree/master/packages/react-components/recipes/src/recipes. otherwise I'll look into adding this in the coming weeks. Thanks so much for the detailed explanation!