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

Need a FAQ page added for this #927

Open
quantizor opened this issue Jun 20, 2023 · 3 comments · May be fixed by #971
Open

Need a FAQ page added for this #927

quantizor opened this issue Jun 20, 2023 · 3 comments · May be fixed by #971

Comments

@quantizor
Copy link
Contributor

quantizor commented Jun 20, 2023

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 styled-components/styled-components#3856 (comment)

@nyxb
Copy link

nyxb commented Jul 16, 2023

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
Copy link

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
Copy link
Contributor Author

PR welcome!

@Cyanhead Cyanhead linked a pull request Jun 11, 2024 that will close this issue
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.

3 participants