Gatsby build fails because window is undefined in a React hook - reactjs

I have a custom hook which checks the window width to conditionally render some UI elements. It works ok during development, but fails on Gatsby build.
Here is the code for my hook:
export const useViewport = () => {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
const handleWindowResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleWindowResize);
return () => window.removeEventListener("resize", handleWindowResize);
}, []);
// Return the width so we can use it in our components
return { width };
}
Then in my component I use it like this:
const { width } = useViewport();
const breakpoint = 767
/** in JSX **/
{
width > breakpoint ? <Menu {...props} /> : <Drawer {...props} />
}
According to Gatsby docs window is not available during the build process. I've tried to if (typeof window !== 'undefined') condition to the hook, but I get the following error:
Cannot destructure property 'width' of 'Object(...)(. ..)' as it is undefined
I've also tried to wrap const { width } = useViewport() in React.useEffect hook, but then I get an error that width in my JSX is undefined.
How can I fix this problem?

See few solutions here
Specially this one:
You'd need to adjust the hook itself. Defining default values in the outside scope and using them as default state should do the trick:

Bit late to the party, but saw this whilst searching for something else.
Window is not avaliable during SSR/SSG so you need to wrap any usage of window in an iffy.
if (typeof window !== 'undefined') {
// Do what you need with any calls to window
}

Related

How to set window resize event listener value to React State?

This issue is very simple but I probably overlook very little point. Window screen size is listening by PostLayout component. When window width is less than 768px, I expect that isDesktopSize is false. I tried everything like using arrow function in setIsDesktopSize, using text inside of true or false for state value, using callback method etc... but it's not working.
PostLayout shared below:
import React, {useState,useEffect, useCallback} from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(false)
}else{
setIsDesktopSize(true)
}
}
useEffect(() => {
window.addEventListener('resize', autoResize)
autoResize();
}, [])
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
console log is shared below:
Desktop: true
627
This could probably be extracted into a custom hook. There's a few things you'd want to address:
Right now you default the state to true, but when the component loads, that may not be correct. This is probably why you see an incorrect console log on the first execution of the effect. Calculating the initial state to be accurate could save you some jank/double rendering.
You aren't disconnecting the resize listener when the component unmounts, which could result in an error attempting to set state on the component after it has unmounted.
Here's an example of a custom hook that addresses those:
function testIsDesktop() {
if (typeof window === 'undefined') {
return true;
}
return window.innerWidth >= 768;
}
function useIsDesktopSize() {
// Initialize the desktop size to an accurate value on initial state set
const [isDesktopSize, setIsDesktopSize] = useState(testIsDesktop);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
function autoResize() {
setIsDesktopSize(testIsDesktop());
}
window.addEventListener('resize', autoResize);
// This is likely unnecessary, as the initial state should capture
// the size, however if a resize occurs between initial state set by
// React and before the event listener is attached, this
// will just make sure it captures that.
autoResize();
// Return a function to disconnect the event listener
return () => window.removeEventListener('resize', autoResize);
}, [])
return isDesktopSize;
}
Then to use this, your other component would look like this (assuming your custom hook is just in this same file -- though it may be useful to extract it to a separate file and import it):
import React, { useState } from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const isDesktopSize = useIsDesktopSize();
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
EDIT: I modified this slightly so it should theoretically work with a server-side renderer, which will assume a desktop size.
Try this, you are setting isDesktopSizze to 'mobile', which is === true
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(true)
}else{
setIsDesktopSize(false)
}
}
I didn't find such a package on npm and I thought it would be nice to create one: https://www.npmjs.com/package/use-device-detect. I think it will help someone :)

conditional rendering of component in Next js

I used to render a component depending on the screen width by writing something like this.
function App(props) {
const [mobile, setMobile] = useState(() => window.innerWidth < 576 ? true : false)
return (
<div>
{
mobile ? <ComponentA /> : <ComponentB />
}
</div >
);
}
But now that I'm using Next.js this gives me several errors due to the window.innerWidth reference.
How could I do this?
Thanks in advance.
You are getting a reference error because you cannot access the window object in useState. Instead, you have to set the initial value in useState to undefined or null and use useEffect where window can be referenced to call setMobile(window.innerWidth < 576 ? true : false). finally, in your render method, you can check whether mobile state is set using setMobile (i.e., not undefined or null) and use the defined mobile state value (either true or false) to conditionally render your ComponentA or ComponentB. Also, you need to add window.addEventListener('resize', handleResize) when your App component is mounted and remove it when it is unmounted, which you also do in useEffect since that is where you get reference to window. Otherwise, resizing the browser will not trigger an update to mobile state. Here is a working example:
import React, { useState, useEffect } from 'react'
function App() {
const [mobile, setMobile] = useState(undefined)
useEffect(() => {
const updateMobile = () => {
setMobile(window.innerWidth < 576 ? true : false)
}
updateMobile()
window.addEventListener('resize', updateMobile)
return () => {
window.removeEventListener('resize', updateMobile)
}
}, [])
return typeof mobile !== 'undefined' ? (
mobile ? (
<ComponentA />
) : (
<ComponentB />
)
) : null
}
Assuming you're seeing something along the lines of ReferenceError: window is not defined:
ReferenceError is thrown when a non-existent variable is referenced.
This is occurring because, in NextJS, components are often initially rendered server-side, using NodeJS, before being handed over for clients to consume. Additionally, in NodeJS, there is no such thing as window — hence, window is not defined.
Fortunately, typeof can be used in such cases to safely check variables before attempting to use them (see this SO answer for additional info).
See below for a practical example.
const [mobile, setMobile] = useState(() => {
if (typeof window === 'undefined') return false
return window.innerWidth < 576
})

How to detect window size in Next.js SSR using react hook?

I am building an app using Next.js and react-dates.
I have two component DateRangePicker component and DayPickerRangeController component.
I want to render DateRangePicker when the window's width is bigger than size 1180px, if the size is smaller than this I want to render DayPickerRangeController instead.
Here is the code:
windowSize > 1180 ?
<DateRangePicker
startDatePlaceholderText="Start"
startDate={startDate}
startDateId="startDate"
onDatesChange={handleOnDateChange}
endDate={endDate}
endDateId="endDate"
focusedInput={focus}
transitionDuration={0}
onFocusChange={(focusedInput) => {
if (!focusedInput) {
setFocus("startDate")
} else {
setFocus(focusedInput)
}
}}
/> :
<DayPickerRangeController
isOutsideRange={day => isInclusivelyBeforeDay(day, moment().add(-1, 'days'))}
startDate={startDate}
onDatesChange={handleOnDateChange}
endDate={endDate}
focusedInput={focus}
onFocusChange={(focusedInput) => {
if (!focusedInput) {
setFocus("startDate")
} else {
setFocus(focusedInput)
}
}}
/>
}
I normally use react hook with window object to detect window screen width like this
But I found that this way is not available when ssr because ssr rendering does not have window object.
Is there an alternative way I can get window size safely regardless of ssr?
You can avoid calling your detection function in ssr by adding this code:
// make sure your function is being called in client side only
if (typeof window !== 'undefined') {
// detect window screen width function
}
full example from your link:
import { useState, useEffect } from 'react';
// Usage
function App() {
const size = useWindowSize();
return (
<div>
{size.width}px / {size.height}px
</div>
);
}
// Hook
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
// only execute all the code below in client side
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
NB: Updated as Sergey Dubovik comment, we dont need to validate windows since useEffect run in client side
While Darryl RN has provided an absolutely correct answer. I'd like to make a small remark: You don't really need to check for the existence of the window object inside useEffect because useEffect only runs client-side and never server-side, and the window object is always available on the client-side.
useEffect(()=> {
window.addEventListener('resize', ()=> {
console.log(window.innerHeight, window.innerWidth)
})
}, [])
here's the solution i'm using: a small npm package found here use-window-size
once installed and imported, all you need to do is use it like this:
const { innerWidth, innerHeight, outerHeight, outerWidth } = useWindowSize();
return (
<div>Window width: {innerWidth}</div>
)

React Hook does not work properly on the first render in gatsby production mode

I have the following Problem:
I have a gatsby website that uses emotion for css in js. I use emotion theming to implement a dark mode. The dark mode works as expected when I run gatsby develop, but does not work if I run it with gatsby build && gatsby serve. More specifically the dark mode works only after switching to light and back again.
I have to following top level component which handles the Theme:
const Layout = ({ children }) => {
const [isDark, setIsDark] = useState(() => getInitialIsDark())
useEffect(() => {
if (typeof window !== "undefined") {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark])
return (
<ThemeProvider theme={isDark ? themeDark : themeLight}>
<ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>
</ThemeProvider>
)
}
The getInitalIsDark function checks a localStorage value, the OS color scheme, and defaults to false. If I run the application, and activate the dark mode the localStorage value is set. If i do now reload the Application the getInitialIsDark method returns true, but the UI Renders the light Theme. Switching back and forth between light and dark works as expected, just the initial load does not work.
If I replace the getInitialIsDark with true loading the darkMode works as expected, but the lightMode is broken. The only way I got this to work is to automatically rerender after loading on time using the following code.
const Layout = ({ children }) => {
const [isDark, setIsDark] = useState(false)
const [isReady, setIsReady] = useState(false)
useEffect(() => {
if (typeof window !== "undefined" && isReady) {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark, isReady])
useEffect(() => setIsReady(true), [])
useEffect(() => {
const useDark = getInitialIsDark()
console.log("init is dark " + useDark)
setIsDark(useDark)
}, [])
return (
<ThemeProvider theme={isDark ? themeDark : themeLight}>
{isReady ? (<ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>) : <div/>}
</ThemeProvider>
)
}
But this causes an ugly flicker on page load.
What am I doing wrong with the hook in the first approach, that the initial value is not working as I expect.
Did you try to set your initial state like this?
const [isDark, setIsDark] = useState(getInitialIsDark())
Notice that I am not wrapping getInitialIsDark() in an additional function:
useState(() => getInitialIsDark())
You will probably crash your build because localStorage is not defined at buildtime. You might need to check if that exists inside getInitialIsDark.
Hope this helps!
#PedroFilipe is correct, useState(() => getInitialIsDark()) is not the way to invoke the checking function on start-up. The expression () => getInitialIsDark() is truthy, so depending on how <ThemedLayout isDark={isDark}> uses the prop it might work by accident, but useState will not evaluate the fuction passed in (as far as I know).
When using an initial value const [myValue, setMyValue] = useState(someInitialValue) the value seen in myValue can be laggy. I'm not sure why, but it seems to be a common cause of problems with hooks.
If the component always renders multiple times (e.g something else is async) the problem does not appear because in the second render the variable will have the expected value.
To be sure you check localstorage on startup, you need an additional useEffect() which explicitly calls your function.
useEffect(() => {
setIsDark(getInitialIsDark());
}, [getInitialIsDark]); //dependency only needed to satisfy linter, essentially runs on mount.
Although most useEffect examples use an anonymous function, you might find more understandable to use named functions (following the clean-code principle of using function names for documentation)
useEffect(function checkOnMount() {
setIsDark(getInitialIsDark());
}, [getInitialIsDark]);
useEffect(function persistOnChange() {
if (typeof window !== "undefined" && isReady) {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark])
I had a similar issue where some styles weren't taking effect because they were being applied to through classes which were set on mount (like you only on production build, everything worked fine in develop).
I ended up switching the hydrate function React was using from ReactDOM.hydrate to ReactDOM.render and the issue disappeared.
// gatsby-browser.js
export const replaceHydrateFunction = () => (element, container, callback) => {
ReactDOM.render(element, container, callback);
};
This is what worked for me, try this and let me know if it works out.
First
In src/components/ i've created a component navigation.js
export default class Navigation extends Component {
static contextType = ThemeContext // eslint-disable-line
render() {
const theme = this.context
return (
<nav className={'nav scroll' : 'nav'}>
<div className="nav-container">
<button
className="dark-switcher"
onClick={theme.toggleDark}
title="Toggle Dark Mode"
>
</button>
</div>
</nav>
)
}
}
Second
Created a gatsby-browser.js
import React from 'react'
import { ThemeProvider } from './src/context/ThemeContext'
export const wrapRootElement = ({ element }) => <ThemeProvider>{element}</ThemeProvider>
Third
I've created a ThemeContext.js file in src/context/
import React, { Component } from 'react'
const defaultState = {
dark: false,
notFound: false,
toggleDark: () => {},
}
const ThemeContext = React.createContext(defaultState)
class ThemeProvider extends Component {
state = {
dark: false,
notFound: false,
}
componentDidMount() {
const lsDark = JSON.parse(localStorage.getItem('dark'))
if (lsDark) {
this.setState({ dark: lsDark })
}
}
componentDidUpdate(prevState) {
const { dark } = this.state
if (prevState.dark !== dark) {
localStorage.setItem('dark', JSON.stringify(dark))
}
}
toggleDark = () => {
this.setState(prevState => ({ dark: !prevState.dark }))
}
setNotFound = () => {
this.setState({ notFound: true })
}
setFound = () => {
this.setState({ notFound: false })
}
render() {
const { children } = this.props
const { dark, notFound } = this.state
return (
<ThemeContext.Provider
value={{
dark,
notFound,
setFound: this.setFound,
setNotFound: this.setNotFound,
toggleDark: this.toggleDark,
}}
>
{children}
</ThemeContext.Provider>
)
}
}
export default ThemeContext
export { ThemeProvider }
This should work for you here is the reference I followed from the official Gatsby site

React SSR, Proper way of handling page scroll position

My goal is to apply different className depending on the user's scroll position. Well, I need to change the background color of the navbar if the user's scroll position is > 0. I came up with a working solution that works in all cases except the one if the user loads a page and initial scroll position is not 0 (scrolled and then reloaded the page).
What I did is I created a custom hook which looks like this:
import { useState, useEffect } from 'react';
export default () => {
const [scrollPosition, setScrollPosition] = useState(
typeof window !== 'undefined' ? window.scrollY : 0,
);
useEffect(() => {
const setScollPositionCallback = () => setScrollPosition(window.scrollY);
if (typeof window !== 'undefined') {
window.addEventListener('scroll', setScollPositionCallback);
}
return () => {
if (typeof window !== 'undefined') {
window.removeEventListener('scroll', setScollPositionCallback);
}
};
}, []);
return scrollPosition;
};
And then I use this hook in my Navbar component:
...
const scrollPosition = useScrollPosition();
...
<Navbar
color={scrollPosition < 1 ? 'transparent' : 'white'}
...
/>
As I described, everything works well if the user loads the page at the 0 scrollY. When it's not, I get the warning Warning: Prop className did not match, which is expected, because scrollY is always 0 on the server side and then scrolling doesn't work, because Navbar keeps ssr class.
What is the proper way of handling it?
I've found a solution. The reason why it was happening is, due to this line in the hook:
const [scrollPosition, setScrollPosition] = useState(
typeof window !== 'undefined' ? window.scrollY : 0,
);
scroll position was equal to 0 all the time on ssr, but when loaded on the client side, it was set to actual scrollY at the beginning.
So what I did is I set initial scrollPosition to 0 on both client and server side by modifying the line below to:
const [scrollPosition, setScrollPosition] = useState(0);
and the added one more effect that works on client side only, which sets scrollPosition:
useEffect(() => {
if (typeof window !== 'undefined' && window.scrollY !== 0) {
setScrollPosition(window.scrollY);
}
}, []);

Resources