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

ServerOnly executing on the client #299

Open
danthegoodman1 opened this issue Jan 21, 2024 · 3 comments
Open

ServerOnly executing on the client #299

danthegoodman1 opened this issue Jan 21, 2024 · 3 comments

Comments

@danthegoodman1
Copy link

danthegoodman1 commented Jan 21, 2024

Describe the bug

With a shiki highlighter in a .server.ts file, imported into a Fence.tsx file, the browser console is throwing errors of highlighter.codeToHtml not being defined, and renders mismatching between server and client:

image
image

Your Example Website or App

not public atm

Steps to Reproduce the Bug or Issue

highlighter.server.ts

import * as shiki from "shiki"

export const highlighter = await shiki.getHighlighter({
  theme: "dracula-soft",
})

fetch.tsx

import { Children, ReactNode } from "react"
import { ServerOnly } from "remix-utils/server-only"
import { highlighter } from "./highlighter.server"

export function Fence(props: { children?: ReactNode; language: string }) {
  return (
    <ServerOnly>
      {() => {
        const code = Children.toArray(props.children)[0] as string
        const html = highlighter.codeToHtml(code.trim(), {
          lang: props.language,
        })
        return (
          <div
            dangerouslySetInnerHTML={{
              __html: html,
            }}
          />
        )
      }}
    </ServerOnly>
  )
}

export const fence = {
  render: "Fence",
  attributes: {
    language: {
      type: String,
    },
  },
}

Then render Fence in a route with nothing else (no loaders, etc.)

Expected behavior

Does not try to render on the client

Screenshots or Videos

No response

Platform

  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Safari, Firefox]
  • Version: [e.g. 91.1]

Additional context

Discovery in Remix discord: https://discord.com/channels/770287896669978684/770287896669978687/1198623722815361148

@danthegoodman1
Copy link
Author

And if I use a client fallback that does effectively the same code, I still get a hydration mismatch error

export function FenceClient(props: { children?: ReactNode; language: string }) {
  const code = Children.toArray(props.children)[0] as string
  const [html, setHTML] = useState("")
  async function loadHTML() {
    const res = await fetch("/syntax_highlighter", {
      method: "post",
      headers: {
        "content-type": "application/json",
      },
      body: JSON.stringify({
        code,
        language: props.language,
      } as SyntaxHighlightPayload),
    })
    setHTML(await res.text())
  }

  useEffect(() => {
    loadHTML()
  }, [])

  return (
    <div
      dangerouslySetInnerHTML={{
        __html: html,
      }}
    />
  )
}
export function Fence(props: { children?: ReactNode; language: string }) {
  return (
    <ServerOnly fallback={<FenceClient {...props} />}>
      {() => {
        const code = Children.toArray(props.children)[0] as string
        const html = highlighter.codeToHtml(code.trim(), {
          lang: props.language,
        })
        return (
          <div
            dangerouslySetInnerHTML={{
              __html: html,
            }}
          />
        )
      }}
    </ServerOnly>
  )
}
export interface SyntaxHighlightPayload {
  language: string
  code: string
}

export async function action(args: ActionFunctionArgs) {
  const data = (await args.request.json()) as SyntaxHighlightPayload
  const html = highlighter.codeToHtml(data.code.trim(), {
    lang: data.language,
  })
  return new Response(html)
}

@sergiodxa
Copy link
Owner

I think the problem is that in highlighter.server.ts you're using a top-level await.

But ServerOnly is intended as the inverse of ClientOnly, in ClientOnly the children is rendered in the client and the fallback in the server, in ServerOnly the children is rendered in the server and the fallback in the browser.

If you can create a reproduction repo I could try to see what's happening.

@danthegoodman1
Copy link
Author

I suspect that as well, as I use this in other places successfully like:

<ServerOnly>
            {() => {
              return (
                <TopNav
                  isAdmin={data.user?.isAdmin}
                  redirectTo={data.currentPath}
                  authed={!!data.user}
                  subscribed={!!data.user?.subscription}
                />
              )
            }}
          </ServerOnly>
          <ClientOnly>
            {() => {
              return (
                <TopNav
                  isAdmin={data.user?.isAdmin}
                  redirectTo={window.location.pathname}
                  authed={!!data.user}
                  subscribed={!!data.user?.subscription}
                />
              )
            }}
          </ClientOnly>

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

No branches or pull requests

2 participants