Search code examples
reactjsnext.jsstyled-componentsrollup

NextJS and component-library using styled-components, not hydrating properly


I am trying to build a component library for a new nextJS application. The nextJS app has the "app router".

Styled-components is working fine when I use it, integrated directly in the nextJS code, the issue arrises when I want to use the component library, I have on the side.

I did everything recommended by nextJS, I set the following in my next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  compiler: {
    styledComponents: {
      // Enabled by default.
      cssProp: true,
    },
  },
}

module.exports = nextConfig

I also included the "StyledComponentsRegistry" as per the nextJS definition.

I have configured the library, such that it is build using rollup.

First I tried keeping styled-components as a peerDependency, and added it as "external" in rollup. That did not work(It worked fine in the browser, since the client code works fine, but it gave me an error in the console, when generating the page. It basically gave me the following error, as webpack was not able to include the styled-components library styled_components__webpack_imported_module_0__ is not a function)

So instead I tried including styled-components as a direct dependency, and I modified the StyledComponentsRegistry, to also include the library's version of styled-components. Like the following:

'use client';
 
import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
import { StyledComponentsServerStyleSheet, StyledComponentsStyleSheetManager } from 'skafte-cms';
 
export default function StyledComponentsRegistry({
  children,
}) {
  // 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());
  const [cmsStyledComponentsStyleSheet] = useState(() => new StyledComponentsServerStyleSheet());
 
  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    const cmsStyles = cmsStyledComponentsStyleSheet.getStyleElement();
    cmsStyledComponentsStyleSheet.instance.clearTag();
    return <>{styles}{cmsStyles}</>;
  });
 
  if (typeof window !== 'undefined') return <>{children}</>;
 
  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      <StyledComponentsStyleSheetManager sheet={cmsStyledComponentsStyleSheet.instance}>
        {children}
      </StyledComponentsStyleSheetManager>
    </StyleSheetManager>
  );
}

This did work, but when I load the page, I get an error in the console, telling me the styled-component is not being hydrated properly(It only maintains it's class that is based on the file-location, not the short class-identifier responsible for the styling)

Any help is appreciated


Solution

  • To fully use styled-components with NextJs you need:

    • Configure next.config.js or add the configuration from Babel (whichever is more preferable in your case)
    module.exports = {
      compiler: {
        styledComponents: {
          // Enable display of the component name along with the generated className (needed for debugging).
          displayName: true,
          // Enable SSR support
          ssr: true,
          // Optional
          fileName: false,
        },
      },
    }
    
    // babel.config.js
    
    module.exports = {
      plugins: [
        [
          "babel-plugin-styled-components",
          {
            displayName: true,
            fileName: false,
            ssr: true,
          },
        ],
      ],
    };
    
    • Install the styled-components package in your library as peerDependency (yarn add styled-components --peer).
    • In the Rollup configuration, use the rollup-plugin-peer-deps-external plugin, which will automatically exclude from the bundle the dependencies designated as peerDependency in package.json of your library.
    • Add the Babel integration plugin to the Rollup configuration and create a babel.config.js file in the root of the library with content similar to what was demonstrated above.
    • This is because the configuration you set up at the application level only applies to components created within the application itself. Code from external libraries will not be handled properly. Therefore, you need to add the new Babel configuration directly to the library, so that when you build Rollup, the babel-plugin-styled-components plugin will make the necessary changes.

    Using the babel-plugin-styled-components plugin for the library is a must, because it will add the withConfig function with the settings needed for easy debugging during development and SSR support so there are no hydration problems.

    For example (output bundle):

    var StyledView = styled.div.withConfig({
      displayName: "StyledView",
      componentId: "sc-10aexlr-0"
    })({
      padding: 10,
      border: "1px solid teal"
    });