styled-components / styled-components-website

The styled-components website and documentation
https://styled-components.com
MIT License
611 stars 439 forks source link

Need a FAQ page added for this #927

Open quantizor opened 1 year ago

quantizor commented 1 year ago

If anyone needs a step by step on how to make styled-components work with Next.js 13 (app router) without any "delay bugs" using client-side rendering, here it is:

Step 1: add the following to your next.config.js file

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

module.exports = nextConfig

Step 2: create the registry.tsx file with the following code:

'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>
  )
}

Step 3: add the 'use client' directive to your layout.tsx file and wrap all the children components on your layout with the StyledComponentsRegistry component.

I've made a tutorial if anyone needs further help: https://www.youtube.com/watch?v=3tgrPm2aKog

Originally posted by @lucasmelz in https://github.com/styled-components/styled-components/issues/3856#issuecomment-1597789823

nyxb commented 1 year ago

hi, i have the problem that my styles only load when i click the DarkmodeToggle button. On first load there is no style and on refresh of the page also not all styles load only when I use the button.

my registry.tsx:


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

my ConfigContext.tsx:

 * This context holds user configuration stuff, like:
 * - Dark or light mode
 * - Sound enabled or disabled
 */
import { ReactNode, createContext, useCallback, useEffect, useMemo, useState } from 'react';

import {
  DARK_COLORS,
  LIGHT_COLORS,
  PREFERS_DARK_KEY,
  PREFERS_DARK_CSS_PROP,
  ColorType,
} from '@constants';
import usePersistedState from '@hooks/usePersistedState';

export const ConfigContext = createContext<{
   colorMode?: string;
   setColorMode?: (value: "light" | "dark") => void;
   soundEnabled?: boolean;
   setSoundEnabled?: (newValue: boolean) => void;
   allowColorTransitions?: boolean;
   disableTabInCodeSnippets?: boolean;
   setDisableTabInCodeSnippets?: (value: boolean) => void;
 } | null>(null);

const SOUND_ENABLED_KEY = 'sound-enabled';

export const ConfigProvider = ({ children }: {children: ReactNode}) => {
  let initialColorValue = 'light';
  let initialSoundEnabled = true;
  let initialAllowColorTransitions = false;

  const [colorMode, rawSetColorMode] = useState(
    initialColorValue
  );
  const [soundEnabled, rawSetSoundEnabled] = useState(
    initialSoundEnabled
  );
  const [
    disableTabInCodeSnippets,
    setDisableTabInCodeSnippets,
  ] = usePersistedState(true, 'tab-in-code-snippets');

  const [
    allowColorTransitions,
    setAllowColorTransitions,
  ] = useState(initialAllowColorTransitions);

  useEffect(() => {
    // Immediately after mount, trigger a re-render IF the values
    // in localStorage don't match the values in the statically-
    // generated HTML
    let root = window.document.documentElement;

    const localColorValue =
      root.style.getPropertyValue(PREFERS_DARK_CSS_PROP) === 'true'
        ? 'dark'
        : 'light';
    const localSoundEnabled =
      window.localStorage?.getItem(SOUND_ENABLED_KEY) === 'false'
        ? false
        : true;

    if (localColorValue !== initialColorValue) {
      rawSetColorMode(localColorValue);
    }

    if (localSoundEnabled !== initialSoundEnabled) {
      rawSetSoundEnabled(localSoundEnabled);
    }
  }, []);

  const setColorMode = useCallback(
    (value: 'light' | 'dark') => {
      if (!allowColorTransitions) {
        setAllowColorTransitions(true);
      }

      let root = window.document.documentElement;

      root.setAttribute('data-color-mode', value);

      const prefersDark = value === 'dark';
      root.style.setProperty(PREFERS_DARK_CSS_PROP, String(prefersDark));
      const newColors = prefersDark ? DARK_COLORS : LIGHT_COLORS;

      root.style.setProperty('--color-text', newColors.text!);
      root.style.setProperty(
        '--color-background',
        newColors.background!
      );
      root.style.setProperty(
        '--color-blurred-background',
        newColors.blurredBackground!
      );
      root.style.setProperty('--color-primary', newColors.primary!);
      root.style.setProperty(
        '--color-secondary',
        newColors.secondary!
      );
      root.style.setProperty('--color-tertiary', newColors.tertiary!);
      root.style.setProperty(
        '--color-decorative',
        newColors.decorative!
      );
      root.style.setProperty('--color-muted', newColors.muted!);
      root.style.setProperty(
        '--color-muted-background',
        newColors.mutedBackground!
      );
      root.style.setProperty('--color-info', newColors.info || '');
      root.style.setProperty('--color-success', newColors.success!);
      root.style.setProperty(
        '--color-success-background',
        newColors.successBackground!
      );
      root.style.setProperty('--color-error', newColors.error!);
      root.style.setProperty(
        '--color-error-background',
        newColors.errorBackground!
      );
      root.style.setProperty('--color-alert', newColors.alert!);
      root.style.setProperty(
        '--color-alert-background',
        newColors.alertBackground!
      );
      root.style.setProperty('--color-venn-0', newColors.venn![0]);
      root.style.setProperty('--color-venn-1', newColors.venn![1]);
      root.style.setProperty('--color-gray-100', newColors.gray![100]);
      root.style.setProperty('--color-gray-200', newColors.gray![200]);
      root.style.setProperty('--color-gray-300', newColors.gray![300]);
      root.style.setProperty('--color-gray-400', newColors.gray![400]);
      root.style.setProperty('--color-gray-500', newColors.gray![500]);
      root.style.setProperty('--color-gray-600', newColors.gray![600]);
      root.style.setProperty('--color-gray-700', newColors.gray![700]);
      root.style.setProperty('--color-gray-900', newColors.gray![900]);
      root.style.setProperty(
        '--color-gray-1000',
        newColors.gray![1000]
      );
      root.style.setProperty(
        '--color-subtle-background',
        newColors.subtleBackground || ''
      );
      root.style.setProperty(
        '--color-subtle-floating',
        newColors.subtleFloating || ''
      );
      root.style.setProperty(
        '--color-homepage-light',
        newColors.homepageLight || ''
      );
      root.style.setProperty(
        '--color-homepage-dark',
        newColors.homepageDark || ''
      );
      root.style.setProperty(
        '--color-homepage-bg',
        newColors.homepageBg || ''
      );

      root.style.setProperty('--syntax-bg', newColors.syntax!.bg);
      root.style.setProperty(
        '--syntax-highlight',
        newColors.syntax!.highlight
      );
      root.style.setProperty('--syntax-txt', newColors.syntax!.txt);
      root.style.setProperty(
        '--syntax-comment',
        newColors.syntax!.comment
      );
      root.style.setProperty('--syntax-prop', newColors.syntax!.prop);
      root.style.setProperty('--syntax-bool', newColors.syntax!.bool);
      root.style.setProperty('--syntax-val', newColors.syntax!.val);
      root.style.setProperty('--syntax-str', newColors.syntax!.str);
      root.style.setProperty('--syntax-name', newColors.syntax!.name);
      root.style.setProperty('--syntax-del', newColors.syntax!.del);
      root.style.setProperty(
        '--syntax-regex',
        newColors.syntax!.regex
      );
      root.style.setProperty('--syntax-fn', newColors.syntax!.fn);

      rawSetColorMode(value);

      localStorage.setItem(PREFERS_DARK_KEY, String(prefersDark));
    },
    [allowColorTransitions]
  );

  // Listen for changes in the media query
  useEffect(() => {
    const QUERY = '(prefers-color-scheme: dark)';

    const mediaQueryList = window.matchMedia(QUERY);

    const listener = (event: MediaQueryListEvent) => {
      const isDark = event.matches;
      setColorMode(isDark ? 'dark' : 'light');
    };

    if (mediaQueryList.addEventListener) {
      mediaQueryList.addEventListener('change', listener);
    } else {
      mediaQueryList.addListener(listener);
    }

    return () => {
      if (mediaQueryList.removeEventListener) {
        mediaQueryList.removeEventListener('change', listener);
      } else {
        mediaQueryList.removeListener(listener);
      }
    };
  }, []);

  const value = useMemo(() => {
    const setSoundEnabled = (newValue: boolean) => {
      window.localStorage?.setItem(SOUND_ENABLED_KEY, String(newValue));
      rawSetSoundEnabled(newValue);
    };

    return {
      colorMode,
      setColorMode,
      soundEnabled,
      setSoundEnabled,
      allowColorTransitions,
      disableTabInCodeSnippets,
      setDisableTabInCodeSnippets,
    };
  }, [
    colorMode,
    setColorMode,
    rawSetColorMode,
    soundEnabled,
    rawSetSoundEnabled,
    allowColorTransitions,
    disableTabInCodeSnippets,
    setDisableTabInCodeSnippets,
  ]);

  return (
    <ConfigContext.Provider value={value}>
      {children}
    </ConfigContext.Provider>
  );
};

my layout.tsx:

import GlobalStyle from '@components/GlobalStyle'
import { Inter } from 'next/font/google'
import { ThemeProvider } from 'styled-components'
import { THEME } from '@constants'
import StyledComponentsRegistry from '@lib/registry'
import { ConfigProvider } from '@components/ConfigContext'

const inter = Inter({ subsets: ['latin'] })

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <StyledComponentsRegistry>
        <ConfigProvider>
          <ThemeProvider theme={THEME}>
            <GlobalStyle allowColorTransitions={true} />
            <body className={inter.className}>{children}</body>
          </ThemeProvider>
        </ConfigProvider>
      </StyledComponentsRegistry>
    </html>
  )
}

my GlobalStyle.tsx:

import React from 'react'
import { createGlobalStyle } from 'styled-components'
import '@nyxb/reset.css'
import { COLOR_SWAP_TRANSITION_DURATION } from '@constants';

export interface GlobalStylesProps {
   allowColorTransitions?: boolean;
}

const GlobalStyles = createGlobalStyle<GlobalStylesProps>`
  /* Global styles */

  body {
    color: var(--color-text);
    background: var(--color-background);

    transition: ${(p) => {
      if (!p.allowColorTransitions) {
        return null;
      }

      return `color ${COLOR_SWAP_TRANSITION_DURATION}ms, background ${COLOR_SWAP_TRANSITION_DURATION}ms`;
    }};
  }

  a:focus {
    outline: 5px auto var(--color-primary);
  }

  body, input, button, select, option {
    font-family: var(--font-family);
    font-weight: var(--font-weight-light);
  }

  h1, h2, h3, h4, h5, h6, strong {
    font-weight: var(--font-weight-bold);
  }

  code {
    font-size: 0.95em;
  }

  ::selection {
  background-color: var(--selection-background-color, var(--color-primary));
  color: var(--selection-text-color, white);
}

  /* Scrollbar and selection styles */
  ::selection {
    background-color: var(--selection-background-color, var(--color-primary));
    color: var(--selection-text-color, white);
    background-image: none !important;
    -webkit-text-fill-color: var(--selection-text-color) !important;
    -moz-text-fill-color: var(--selection-text-color) !important;
    background-image: none !important;
    background-clip: revert !important;
    -webkit-background-clip: revert !important;
    text-shadow: none !important;
  }

  @media (orientation: landscape) {
    ::-webkit-scrollbar {
      width: var(--scrollbar-width, 12px);
      height: var(--scrollbar-height, 12px);
      background-color: var(--scrollbar-background-color, var(--color-gray-100));
    }
    ::-webkit-scrollbar-track {
      border-radius: 3px;
      background-color: var(--scrollbar-background-color, transparent);
    }
    ::-webkit-scrollbar-thumb {
      border-radius: 5px;
      background-color: var(--scrollbar-color, var(--color-gray-700));
      border: 2px solid var(--scrollbar-background-color, var(--color-gray-100));
    }
  }

  /* CSS Variables */
  :root {
    --font-weight-bold: 600;
    --font-weight-medium: 500;
    --font-weight-light: 400;

    --font-family: 'Wotfard', 'Wotfard-fallback', sans-serif;
    --font-family-mono: 'League Mono', 'Fira Mono', monospace;
    --font-family-spicy: 'Sriracha', 'Wotfard', Futura, -apple-system, sans-serif;

    /* HACK:
      Reach UI tests for loaded styles, but I'm providing my own.
      This is to avoid a noisy warning in dev.
    */
   --reach-dialog: 1;
  }

  .video-js .vjs-big-play-button {
    top: 0 !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    margin: auto !important;
    border: 1px solid rgba(255, 255, 255, 0.25) !important;
    background-color: rgba(0, 0, 0, 0.4) !important;
  }

  .video-js .vjs-play-progress:before {
    top: -0.6em !important;
  }

  .vjs-slider-horizontal .vjs-volume-level:before {

    top: -0.6em !important;
  }
`;

const GlobalStylesWrapper = (props: any) => {
  return <GlobalStyles {...props} />;
};

export default React.memo(GlobalStylesWrapper);

when loading for the first time or after a reload it looks like this:

image

if i click the button now:

image

if I click it again for light mode:

image

I've been trying for weeks now, haven't found anything in the nice and nextjs forum didn't give me any answers and no one replied to my post. I just do not understand what I am doing wrong. How do I make one of the styles exist on first load?

Cyanhead commented 3 months ago

Hi @quantizor, it looks like this is still not implemented on the site (this is all we have on Next.js https://styled-components.com/docs/advanced#next.js). Is there a new fix, or do I stick to the steps above for a PR?

quantizor commented 3 months ago

PR welcome!