Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
2 of 5 tasks
r007 opened this issue Nov 21, 2024 · 1 comment
Open
2 of 5 tasks

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

r007 opened this issue Nov 21, 2024 · 1 comment

Comments

@r007
Copy link

r007 commented Nov 21, 2024

Area

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

What kind of documentation issue are you reporting?

  • Reporting a typo
  • Reporting a documentation bug
  • Documentation improvement
  • Documentation feedback

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
  1. 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)
  })
}
  1. 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)
}
  1. 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

  • Check that there isn't already an issue that requests the same feature to avoid creating a duplicate.
@sopranopillow
Copy link
Contributor

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants