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

No "working" styled-components example #167

Closed
1 task done
iDVB opened this issue Feb 15, 2023 · 22 comments · Fixed by #311
Closed
1 task done

No "working" styled-components example #167

iDVB opened this issue Feb 15, 2023 · 22 comments · Fixed by #311

Comments

@iDVB
Copy link

iDVB commented Feb 15, 2023

What version of Remix are you using?

1.12.0

Are all your remix dependencies & dev-dependencies using the same version?

  • Yes

Steps to Reproduce

official styled-components example

Expected Behavior

That it will render both on first load as well as consecutive loads without err.

Actual Behavior

Exhibits the hydration issues mentioned here.
However, in our experience, the styles match fine on FIRST load. Only on page refresh do them become broken. Even with JS disabled this issue persists, which leads me to believe the issue is strictly an SSR one for the moment.

It does not appear that it will work with any SSR without the aid of babel-plugin-styled-components which itself is not supported in Remix since it doesn't seem we have access to plugins or modifying esbuild config.

As a comparison, Next.js allows you to control webpack option values from a configuration file (next.config.js) so they can implement the needed plugins. A member of the development team says in a PR comment that this is because exposing the configuration values would lock in the compiler's choices and also risk breaking the application.

This limitation seems to be leading some remix users to implement workarounds using a custom module to override esbuild.

Then there are a number of other mentions out there of similar issues that link around and I'm not even quite sure if its relates to the same hydration issue or not, but certainly sounds like it would affect the existing example.

There are even posts about how to do this with NextJS that say there are no issues.... but that seems odd when the example doesnt seem to need babel-plugin-styled-components.

@kentcdodds mentions a possible fix here using patch-package, however then @jmurzy mentions that the needed code is stripped out, rendering that possible fix incomplete.

For added injury, we also need to get Material-UI v4 working, which seems to have exactly the same issue. First load, all the style names match up, on reload, none match. This is also with JS disabled, so it appears again to be an SSR only issue.

Conclusion

None of the methods above seem to work for us, and we're currently not clear if its because we use pnpm or that it's a monorepo, or that we also implement our own custom react ui lib that also uses babel-plugin-styled-components but in any case, none seem to work.

We're VERY much wanting to migrate all our sites to Remix but need to slowly migrate away from styled-components, and not cold-turkey cut it off. So we need a migration path for styled-components with Remix.

@iDVB iDVB changed the title No working styled-components example No "working" styled-components example Feb 15, 2023
@iDVB
Copy link
Author

iDVB commented Feb 17, 2023

I'm still getting these hydration errors:
image

Any guidance is VERY much appreciated.

@Xiphe
Copy link

Xiphe commented Feb 20, 2023

Until the root-cause of this is hopefully resolved with react 18.3 you can try out remix-island or a similar approach to hydrate only parts of the document. It works reliably on my end with styled-components (can't stream them though)

@MichaelDeBoey MichaelDeBoey transferred this issue from remix-run/remix Mar 2, 2023
@rojvv
Copy link

rojvv commented Mar 19, 2023

Unrelated to this issue, but might still be interesting.

I’ve recently created a CSS-in-JS library that builds CSS like Tailwind, and doesn’t require a Babel/esbuild plugin. Specifically for having no issues with Remix.

Read more here: https://github.com/roj1512/classno
Example app: https://github.com/roj1512/classno_remix

@mehranhydary
Copy link

Hi @iDVB @Xiphe - are there any viable paths forward? Running into a hydration issue on Remix as well with a very minimal setup

@Xiphe
Copy link

Xiphe commented Jun 29, 2023

Hi @mehranhydary

Both the remix and the react team are aware of this issue. React is actively working towards a solution as this is affecting any solution that hydrates react into the complete document.

On my end, https://github.com/kiliman/remix-hydration-fix and https://github.com/Xiphe/remix-island present good enough workarounds until the problem will eventually be fixed at it's root cause. You can also try to downgrade react to v17.

@markdalgleish
Copy link
Member

Thanks for raising this issue! I've just merged a PR that updates our Styled Components example to address the issues mentioned here.

If you're still experiencing issues, let us know, or even help us improve the example if there's something we missed.

@Xiphe
Copy link

Xiphe commented Aug 14, 2023

Hi @markdalgleish thanks for taking the time to update the example on this!

Indeed, the change you added to the example should make it so styled components itself don't cause hydration issues.
Unfortunately, from my experience this still isn't really safe to use with react ~18.2.

The problem is that when anything else produces a hydration error, react will wipe away the <style> outlet of styled-components in the head effectively causing any styled component to be unstyled. I haven't validated this with v6 but to my knowledge styled-components is not able to recover from this.

To me that's not a state in which I would ship an app to production.
To my knowledge the only fix is remix-island or waiting and hoping for [email protected].

I understand that there's little to nothing we can do about this from within remix itself (Other then maybe officially supporting rendering into a smaller part of the page) and the update you did on the example is certainly helpful. Still I wouldn't consider this issue fixed.

@seanmcquaid
Copy link

Hey ya'll! Is there any thought on getting an example together with Vite + Styled Components since this seems to be the direction Remix is going?

@mryechkin
Copy link

Would love to know that as well - just starting to explore Remix + Vite, and styled-components support is paramount for us.

@SilencerWeb
Copy link

@seanmcquaid @mryechkin hey guys, have you managed to successfully integrate styled-components with SSR support for Remix + Vite?

@prxg22
Copy link

prxg22 commented Mar 12, 2024

I did using remix-island. It helps managing the <header> rehydration, but as tradeoff, you loose the ability to render the whole document inside your application.

  1. first create the Header component on your app/root.tsx file
// app/root.tsx

export const Head = createHead(() => {
  return (
    <>
      <meta charSet="utf-8" />
      <meta name="viewport" content="width=device-width,initial-scale=1" />
      <Meta />
      <Links />
    </>
  )
})

export default function App() {
  // this will be rendered inside a node
  return (
    <ThemeProvider>
       <Outlet />   
    </ThemeProvider>
  )
}
  1. In your app/entry.server.tsx collect the StyleSheet and inject it with the head in your document before stream it
// app/entry.server.tsx
import { PassThrough } from 'node:stream'

import {
  createReadableStreamFromReadable,
  type EntryContext,
} from '@remix-run/node'
import { RemixServer } from '@remix-run/react'
import { isbot } from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
import { renderHeadToString } from 'remix-island'
import { ServerStyleSheet } from 'styled-components'

import { Head } from './root'

const ABORT_DELAY = 5_000

export function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
) {
  const styleSheet = new ServerStyleSheet()
  return new Promise((resolve, reject) => {
    let shellRendered = false

    const { pipe, abort } = renderToPipeableStream(
      styleSheet.collectStyles(
        <RemixServer
          context={remixContext}
          url={request.url}
          abortDelay={ABORT_DELAY}
        />,
      ),
      {
        onShellReady() {
          shellRendered = true
          responseHeaders.set('Content-Type', 'text/html')
          const head = renderHeadToString({
            request,
            remixContext,
            Head,
          })
          const body = injectStyles(head, styleSheet, pipe)
          const stream = createReadableStreamFromReadable(body)

          resolve(
            new Response(stream, {
              status: responseStatusCode,
              headers: responseHeaders,
            }),
          )
        },
        onShellError(err: unknown) {
          reject(err)
        },
        onError(err: unknown) {
          if (shellRendered) {
            console.error(err)
          }
        },
      },
    )

    setTimeout(abort, ABORT_DELAY)
  })
}

function injectStyles(
  head: string,
  styleSheet: ServerStyleSheet,
  pipe: <Writable extends NodeJS.WritableStream>(
    destination: Writable,
  ) => Writable,
) {
  const body = new PassThrough()
  body.write(
    `<!DOCTYPE html><html><head>${head} ${styleSheet.getStyleTags()}</head><body><div id="root">`,
  )
  pipe(body)
  body.write('</div></body></html>')
  return body
}
  1. Finally hydrate the #root element in your app/entry.client.tsx:
import { RemixBrowser } from '@remix-run/react'
import { StrictMode, startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'

startTransition(() => {
  hydrateRoot(
    document.getElementById('root')!,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>,
  )
})

@prxg22
Copy link

prxg22 commented Mar 13, 2024

I know @Xiphe had already recommended the remix-island, but I manage to stream the responses, so I found valuable to share.

@Xiphe
Copy link

Xiphe commented Apr 22, 2024

Awesome! Thanks for sharing @prxg22

@wetteyve
Copy link

You rock @prxg22 🪨 ! Thank you so much for sharing ❤️

@AronH99
Copy link

AronH99 commented Aug 14, 2024

Still having hydration issues though, seems like its' related with an external library i am using because everything seems to work fine, anyone having any ideas?
My vite config ssr has the external packages included which could cause problems so not really sure what else it can be.

@wetteyve
Copy link

Still having hydration issues though, seems like its' related with an external library i am using because everything seems to work fine, anyone having any ideas? My vite config ssr has the external packages included which could cause problems so not really sure what else it can be.

can you share the current code of your root.tsx & entry.server.tsx ? i also had some issues first and than could resolve it by adapting the injectStyles() and App() functions

@AronH99
Copy link

AronH99 commented Aug 14, 2024

@wetteyve
The following is what i got for root.tsx:

import { LoaderFunctionArgs } from '@remix-run/node'
import './global.css'
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  json,
  useLoaderData,
} from '@remix-run/react'
import React from 'react'
import { useChangeLanguage } from 'remix-i18next/react'
import { createHead } from 'remix-island'
import { serverClient } from './apollo'
import i18nServer from './i18n.server'

export const Head = createHead(() => {
  return (
    <>
      <meta charSet="utf-8" />
      <meta name="viewport" content="width=device-width,initial-scale=1" />
      <Meta />
      <Links />
    </>
  )
})

export default function App() {
  const { someData } = useLoaderData<typeof loader>()
  useChangeLanguage(locale)

  return (
    <>
      <Outlet context={{ someData }} />
      <ScrollRestoration />
      <Scripts />
    </>
  )
}

export async function loader({ request }: LoaderFunctionArgs) {
  const locale = await i18nServer.getLocale(request)

  const { data } = await serverClient.query({
   ....
  })

  const brand = data?.brand
  return json({ locale, brand })
}

and this is my entry.server.tsx file:

/* eslint-disable no-console */
import { PassThrough } from 'node:stream'
import { ApolloProvider } from '@apollo/client'
import { getDataFromTree } from '@apollo/client/react/ssr'
import type { EntryContext } from '@remix-run/node'
import { createReadableStreamFromReadable } from '@remix-run/node'
import { RemixServer } from '@remix-run/react'
import { MultiBrandThemes } from '@test'
import { createInstance } from 'i18next'
import { isbot } from 'isbot'
import type { ReactElement } from 'react'
import { renderToPipeableStream } from 'react-dom/server'
import { I18nextProvider, initReactI18next } from 'react-i18next'
import { renderHeadToString } from 'remix-island'
import { ServerStyleSheet, ThemeProvider } from 'styled-components'
import { serverClient } from './apollo'
import { StepProvider } from './components'
import * as i18n from './i18n'
import i18nServer from './i18n.server'
import { Head } from './root'

const ABORT_DELAY = 5_000

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return isbot(request.headers.get('user-agent'))
    ? handleBotRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      )
    : handleBrowserRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      )
}

async function handleBotRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const styleSheet = new ServerStyleSheet()

  const instance = createInstance()
  const lng = await i18nServer.getLocale(request)
  const ns = i18nServer.getRouteNamespaces(remixContext)

  await instance.use(initReactI18next).init({
    ...i18n,
    lng,
    ns,
  })

  return new Promise(async (resolve, reject) => {
    let shellRendered = false
    const { pipe, abort } = renderToPipeableStream(
      styleSheet.collectStyles(
        await wrapRemixServerWithContexts(
          <RemixServer
            context={remixContext}
            url={request.url}
            abortDelay={ABORT_DELAY}
          />,
          instance
        )
      ),
      {
        onAllReady() {
          shellRendered = true
          responseHeaders.set('Content-Type', 'text/html')
          const head = renderHeadToString({ request, remixContext, Head })
          const body = injectStyles(head, styleSheet, pipe)
          const stream = createReadableStreamFromReadable(body)

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            })
          )
        },
        onShellError(error: unknown) {
          reject(error)
        },
        onError(error: unknown) {
          responseStatusCode = 500
          if (shellRendered) {
            console.error(error)
          }
        },
      }
    )

    setTimeout(abort, ABORT_DELAY)
  })
}

async function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const styleSheet = new ServerStyleSheet()

  const instance = createInstance()
  const lng = await i18nServer.getLocale(request)
  const ns = i18nServer.getRouteNamespaces(remixContext)

  await instance.use(initReactI18next).init({
    ...i18n,
    lng,
    ns,
  })

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

    const { pipe, abort } = renderToPipeableStream(
      styleSheet.collectStyles(
        await wrapRemixServerWithContexts(
          <RemixServer
            context={remixContext}
            url={request.url}
            abortDelay={ABORT_DELAY}
          />,
          instance
        )
      ),
      {
        onShellReady() {
          shellRendered = true
          responseHeaders.set('Content-Type', 'text/html')
          const head = renderHeadToString({ request, remixContext, Head })
          const body = injectStyles(head, styleSheet, pipe)
          const stream = createReadableStreamFromReadable(body)

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            })
          )
        },
        onShellError(error: unknown) {
          reject(error)
        },
        onError(error: unknown) {
          responseStatusCode = 500
          if (shellRendered) {
            console.error(error)
          }
        },
      }
    )

    setTimeout(abort, ABORT_DELAY)
  })
}

async function wrapRemixServerWithContexts(
  remixServer: ReactElement,
  instance: any
) {
  const brandCode = import.meta.env.VITE_BRAND_CODE
  const theme = *insert imported/used theme here or initiate default one*

  const app = (
    <I18nextProvider i18n={instance}>
      <StepProvider>
        <ApolloProvider client={serverClient}>
          <ThemeProvider theme={theme}>{remixServer}</ThemeProvider>
        </ApolloProvider>
      </StepProvider>
    </I18nextProvider>
  )

  await getDataFromTree(app)
  const initialState = serverClient.extract()

  const appWithData = (
    <>
      {app}
      <script
        dangerouslySetInnerHTML={{
          __html: `window.__APOLLO_STATE__=${JSON.stringify(
            initialState
          ).replace(/</g, '\\u003c')}`,
        }}
      />
    </>
  )
  return appWithData
}

const injectStyles = (
  head: string,
  styleSheet: ServerStyleSheet,
  pipe: <Writable extends NodeJS.WritableStream>(
    destination: Writable
  ) => Writable,
  lang = 'nlbe'
) => {
  const body = new PassThrough()
  body.write(
    `<!DOCTYPE html><html lang=${lang}><head>${head} ${styleSheet.getStyleTags()}</head><body><div id="root">`
  )
  pipe(body)
  body.write('</div></body></html>')
  return body
}

As u can see i am also working with i18n, apollo and a custom provider

@wetteyve
Copy link

@AronH99 Ok these files look good from my point of view. I have also wrapped my remixApp in additional providers and added a script tag, everything works without hydration errors. I'm sorry I can't help you any further.

When looking for hydration errors, it sometimes helps me to look at the HTML diffs as described here: https://www.jacobparis.com/content/remix-hydration-errors#-tip-diff-the-html-to-find-the-exact-cause

@AronH99
Copy link

AronH99 commented Aug 15, 2024

@wetteyve thanks for the tip, it's most likely because of the external import of a component library but can't seem to solve it, i will look a little more though

@wilk
Copy link

wilk commented Aug 20, 2024

This solution worked for me: styled-components/styled-components#4275

@ArnabChatterjee20k
Copy link

Still getting the issue @prxg22

TypeError: handleDocumentRequestFunction is not a function
    at handleDocumentRequest (/home/arnab/Desktop/PickPalette-PaletteFromImage/node_modules/@remix-run/server-runtime/dist/server.js:340:18)
    at requestHandler (/home/arnab/Desktop/PickPalette-PaletteFromImage/node_modules/@remix-run/server-runtime/dist/server.js:160:18)

@HcroakerDev
Copy link

I was able to solve it, take a look at my comment here: #375 (comment)

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

Successfully merging a pull request may close this issue.