Search code examples
javascriptstyled-components

Implement different themes using styled components


I decided to switch to styled-components and now I'm really struggling to make my dark/light theme work again. Before I used only css and relied upon css variables. I looked many tutorials and example for styled-components but the theme is always stored and changed on app/top component, while I preferably need it stored in the config component and rendered on another.

How could I do this without necessarily changing the structure ?

import * as sc from "./styles";

function App() {
  return (
    <>
      <sc.globalStyles />
      <sc.title>My app</sc.title>
      <Configuration />
    </>
  );
}

function Configuration() {
  const [config, setConfig] = useState(
    retrieveFromStorage("configuration") ?? {
      //other things
      useDarkTheme: true,
    }
  )

  useEffect(() => setToStorage(config, "configuration"), [config]);

  const handleConfig = ({ target: { type, name, value, checked } }) => {
    setConfig(prev => ({
      ...prev,
      [name]: type === "select-one" ? value : checked,
    }));
  };

  return (
    <>
      <sc.options>
        <summary title="set your config">Options:</summary>
        {/*other things*/}
      </sc.options>
      <Theme useDarkTheme={config.useDarkTheme} handleInput={handleConfig} />
    </>
  );
}

function Theme({ useDarkTheme, handleInput }) {
  React.useEffect(
    () => (useDarkTheme ? console.log("should be dark") : console.log("should be light")),
    [useDarkTheme]
  );

  return (
    <sc.theme>
      ☀️
      <sc.toogleSwitch>
        <sc.toogleTheme
          type="checkbox"
          name="useDarkTheme"
          id="toogle"
          defaultChecked={useDarkTheme}
          onChange={handleInput}
        />
        <sc.themeLabel htmlFor="toogle" />
      </sc.toogleSwitch>
      🌒
    </sc.theme>
  );
}

thanks!


Solution

  • Are you familiarized with React's Context API? From styled-components docs:

    styled-components has full theming support by exporting a <ThemeProvider> wrapper component. This component provides a theme to all React components underneath itself via the context API.

    Let's see how we could implement a ThemeProvider with styled-components.

    1. First we need to create a Context to encapsulate our theming logic

    We should also create a "custom hook" to ease access to our context throughout our app.

    theme-context.jsx

    import React from 'react'
    
    export const ThemeContext = React.createContext({
      // our theme object
      theme: {},
      // our color modes ('dark' || 'light')
      colorMode: '',
      // a method to toggle our theme from `dark` to `light` and vice-versa
      setColorMode: () => null,
    })
    
    // export our custom hook for quick access to our context
    export function useTheme() {
      return React.useContext(ThemeContext)
    }
    
    2. Now we need to extend styled-components native <ThemeProvider> to create our own ThemeProvider

    Since we'll need access to our themes, I'll add two very contrived theme objects (for the sake of simplicity) as well.

    theme-provider.jsx

    import React from 'react'
    import { ThemeProvider as StyledProvider } from 'styled-components'
    import { ThemeContext } from './theme-context'
    
    // our theme objects
    const lightTheme = { colorMode: 'light', bg: '#fff', text: '#000' }
    const darkTheme = { colorMode: 'dark', bg: '#000', text: '#fff' }
    
    // our iterable theme "store"
    const myThemes = [lightTheme, darkTheme]
    
    // our default color mode
    const defaultColorMode = 'light'
    
    const ThemeProvider = ({ children, ...props }) => {
      // get fallback values from the parent ThemeProvider (if exists)
      const {
        theme: fallbackTheme,
        colorMode: fallbackColorMode,
      } = useTheme()
    
      // initialize our state
      const theme = props.theme ?? fallbackTheme
      const [colorMode, setColorMode] = React.useState(
        props.colorMode ?? fallbackColorMode ?? defaultColorMode,
      )
    
      // memoize the current theme
      const resolvedTheme = React.useMemo(() => {
        const theme = myThemes.find(t => t.colorMode === colorMode)
        if (theme) return theme
        return lightTheme
      }, [theme, myThemes, colorMode])
    
      // update our state if props change
      React.useEffect(() => {
        setColorMode(props.colorMode ?? fallbackColorMode ?? defaultColorMode)
      }, [props.colorMode, fallbackColorMode])
    
     return (
        <ThemeContext.Provider
          value={{
            theme: resolvedTheme,
            colorMode,
            setColorMode,
          }}
        >
          <StyledProvider theme={resolvedTheme}>{children}</StyledProvider>
        </ThemeContext.Provider>
      )
    }
    
    export default ThemeProvider
    
    3. Our final step is wrapping up our main <App /> component within our ThemeProvider

    app.jsx

    import React from 'react'
    import ThemeProvider from './theme-provider'
    
    const App = () => {
      const [themeType, setThemeType] = React.useState('light')
      const switchThemes = () => {
        setThemeType(last => (last === 'dark' ? 'light' : 'dark'))
      }
    
      return (
        <ThemeProvider colorMode={themeType}>
          <MySwitch onClick={switchThemes} />
        </ThemeProvider>
      )
    }
    

    And that's it. We should now be able to toggle our theme by clicking on MySwitch. Hope that helps!

    Let me know how it goes? Cheers