Veronika-Jaghinyan / light-dark-mode-app

Single page React app created with Vite. It's a demo of light and dark modes.
26 stars 10 forks source link

unable to access window object at component level #1

Closed pfoerdie closed 1 year ago

pfoerdie commented 2 years ago

I already commented on your medium post, but this might be a much better place for discussion.

I really like your approach to dark mode in react. I have already searched through several implementations but was never quite convinced. I really wanted to use your solution by myself, but when I tried to integrate it into my application I came accross some issues I was not able to resolve.

My setup does not use vite but nextjs to render the components. In your App.tsx you use the window object to access matchMedia and localStorage. This has worked for you, but because nextjs uses a combination of serverside and clientside rendering I was not able to access the window object at render time, although my code looked really similar.

https://github.com/Veronika-Jaghinyan/light-dark-mode-app/blob/0161be1862b16a2a941f9710e74783ce37069ebe/src/App.tsx#L11-L17

I am kinda new to react and I know it is much to ask, but do you have any sugestions how to accomplish your approach with a nextjs app? I thought maybe the useEffect hook could help, because inside I have access to the window object, but I am not sure how to use the classes and states then. Or is your approach not possible with serverside rendering like in nextjs?

This is my app file btw:

import React from 'react'

import 'bootstrap/dist/css/bootstrap.css'
import '../styles/globals.scss'

import Layout from '../components/Layout'
import { defaultProps, ThemeContext } from '../lib/context'

export default function Website ({ Component, pageProps }) {
  React.useEffect(() => {
    import('bootstrap/dist/js/bootstrap.bundle')
  }, [])

  function prefersColorScheme () {
    if (window.matchMedia) {
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
      return mediaQuery.matches ? 'dark' : 'light'
    }
    return 'light'
  }

  function getDefaultTheme () {
    if (window.localStorage) {
      const theme = window.localStorage.getItem('theme')
      if (theme) return theme
    }
    return prefersColorScheme()
  }

  const [theme, setTheme] = React.useState(getDefaultTheme())

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Layout {...defaultProps} {...pageProps}>
        <Component {...defaultProps} {...pageProps} />
      </Layout>
    </ThemeContext.Provider>
  )
}

And the error is:

Server Error
ReferenceError: window is not defined

This error happened while generating the page.

Source: pages\_app.js (23:4) @ getDefaultTheme

  21 | 
  22 |   function getDefaultTheme () {
> 23 |     if (window.localStorage) {
     |    ^
  24 |       const theme = window.localStorage.getItem('theme')
  25 |       if (theme) return theme
  26 |     }
Veronika-Jaghinyan commented 2 years ago

Thanks for your feedback. I've seen your comment on the Medium and provided a possible solution.

Since you are using server side generator tool the window will not be defined during build time and will throw a reference error. That's why before using window object always check if the typeof window is not equal to undefined: typeof window !== "undefined".

Hope this will help. Feel free to contact me if there will be further issues.

pfoerdie commented 2 years ago

Thank you! That helped already to get further to a solution.

I had some more problems along the way that I was able to resolve, but I have finally come to another problem I don't know how to deal with.

First, because I use module scss I had to rewrite the themify mixin to use a global scope for the theme class:

@mixin themify {

  @each $theme,
  $map in $themes {
    :global(.theme-#{$theme}) & {
      $theme-map: () !global;

      @each $key,
      $submap in $map {
        $value: map-get(map-get($themes, $theme), '#{$key}');
        $theme-map: map-merge($theme-map, ($key: $value, )) !global;
      }

      @content;
      $theme-map: null !global;
    }
  }
}

Some minor issues like not being able to set a background of the body (for obvious reasons). So good so far, but then I had a real problem.

I already replaced all functions that use the window object to make a check before returning and to return the default value otherwise. That resulted in those methods not throwing during serverside rendering and working correctly on clientside. The dark mode was still not rendering and I found the following error message on the console:

Warning: Prop `className` did not match. Server: "theme-light" Client: "theme-dark"
See more info here: https://nextjs.org/docs/messages/react-hydration-error

The suggested solution to this problem is to use the useEffect hook to change the state after hydration:

export default function Website ({ Component, pageProps }) {
  const [theme, setTheme] = React.useState('light')

  React.useEffect(() => {
    import('bootstrap/dist/js/bootstrap.bundle')
    setTheme(getDefaultTheme())
  }, [])

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <div className={'theme-' + theme}>
        <Layout {...defaultProps} {...pageProps}>
          <Component {...defaultProps} {...pageProps} />
        </Layout>
      </div>
    </ThemeContext.Provider>
  )
}

That actually did work and showed the dark mode, but this is not a good solution for me, because the page is already rendered after hydration and setting the dark theme will result in a flashing application everytime I load the page.

Do you have any further ideas how I could resolve the problem?

dygy commented 1 year ago

you could try to keep theme in a cookie instead of local storage, or in a database. So you could pass on the server side to data. Also, you could create a URL-based theme selection. So themes will be located in the query. So you could dispatch theme to props from the server while knowing what the URL of the request is.

I think it's not good to push here for SSR stuff, as Veronika is not doing SSR example. So my opinion, the issue should be closed, as it's not a repository issue.

pfoerdie commented 1 year ago

Thanks, that is a good idea. I don't think that url based theme selection is very user friendly, but a cookie based approach is a viable solution for SSR.

I will close this issue, because it is not a repository issue as you said, but I think it is an important topic that many developers will face. Developing only for CSR is not up to date in my opinion as reacts latest changes move towards server-first rendering.

I was really struggling to find solutions for my problem on the internet. Even libraries like usehooks-ts were not working in SSR. I would have been happy to find a discussion about this topic and possible solutions for implementations. The reason I brought it up here is because I was very fond of Veronikas solution and I really wanted to make it work for SSR too.

dygy commented 1 year ago

i agree that discussion is good, but I guess then it somehow should be enabled by Veronika https://docs.github.com/en/discussions

Also, a good thing is to create an SSR-fork example, but the problem with "issues", is that devs check the section, to understand what kind of problems are today existing in Veronika code, to understand, whether should they reuse it or not.