Styles take time to load - Next.js

When I enter my portfolio it loads the unstyled html page, and only after a few seconds the styles load. How can I resolve this?

NOTE: I'm using Styled-Components

  1. When I enter the page: enter image description here

  2. After a few seconds: enter image description here

I tried looking for styled components compatibility with next.js but couldn't find anything about this bug

Upvotes: 9

Views: 11548

Answers (4)

Bart Janeczko
Bart Janeczko

Reputation: 131

If you are using Next 13 or Next 14 with App Router solution with _document.js won't work. Then you need to create a global registry component to collect all CSS style rules during a render.

Add registry.tsx in /app folder

'use client';

import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import {
  ServerStyleSheet,
  StyleSheetManager,
} from 'styled-components';

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  // Only create stylesheet once with lazy initial state
  // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
  const [styledComponentsStyleSheet] = useState(
    () => new ServerStyleSheet()
  );

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== 'undefined') return <>{children}</>;

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}

Then import it and use in your app/layout.tsx.

import StyledComponentsRegistry from './lib/registry'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  )
}

Example: https://github.com/vercel/app-playground/tree/main/app/styling/styled-components

Upvotes: 13

codajoao
codajoao

Reputation: 279

Answering @BogdanOnu’s question, in Next.js 13+ with TypeScript, you can handle this with the following example:

import { ReactElement } from "react";
import { ServerStyleSheet } from "styled-components";
import Document, { Html, Head, Main, NextScript, DocumentContext, DocumentInitialProps } from "next/document";

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      } as any;
    } finally {
      sheet.seal();
    }
  }
  render(): ReactElement {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

Upvotes: 0

Makinde Sodiq
Makinde Sodiq

Reputation: 39

Since Next.js supports server-side rendering by default, the CSS-in-JS approach used by styled-components may not work as expected. By default, Next.js pre-renders all pages on the server before they are sent to the client, which means that styles generated by styled-components may not be available in the initial HTML response.

To address this issue, you can use the ServerStyleSheet class from styled-components to collect all the styles used in your application and include them in the server-rendered HTML response by adding the following into your _document.js

import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

function MyDocument(props) {
  return (
    <Html>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

MyDocument.getInitialProps = async (ctx) => {
  const sheet = new ServerStyleSheet();
  const originalRenderPage = ctx.renderPage;

  try {
    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) =>
          sheet.collectStyles(<App {...props} />),
      });

    const initialProps = await Document.getInitialProps(ctx);
    return {
      ...initialProps,
      styles: (
        <>
          {initialProps.styles}
          {sheet.getStyleElement()}
        </>
      ),
    };
  } finally {
    sheet.seal();
  }
};

export default MyDocument;

Upvotes: 3

ivanatias
ivanatias

Reputation: 4033

Being a CSS-in-JS styling solution, styled-components is geared for client-side rendering, it normally assumes that it's executing in a browser and so it produces CSS styles as a result of JavaScript execution and injects them directly into the document. In this case, since Next.js pre-renders all pages by default, you need to have the CSS styles in the server-side rendered HTML to avoid the flash of unstyled content on first render.

You can do so following these steps:

If you are using Next.js 12+ (with SWC compiler):

Modify next.config.js:

/** @type {import('next').NextConfig} */

const nextConfig = {
  // ...rest of options
  compiler: {
    styledComponents: true,
  },
}

module.exports = nextConfig

Create a custom _document.js file on pages folder and add:

import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: [initialProps.styles, sheet.getStyleElement()],
      }
    } finally {
      sheet.seal()
    }
  }
}

Upvotes: 15

Related Questions