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

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

Related

Why I'm getting Uncaught TypeError: Cannot read properties of null (reading 'add') on deployment?

I'm working on a editor app using fabric.js.On localhost, When i click on Add Circle it works fine while on deployment it is causing cannot read properties of null.
Here is the code:
I'm using react context api by which i add objects in canvas and it displays on screen.
FabricCircle.js
import { fabric } from 'fabric';
import ContextCanvas from '../../../context/ContextCanvas';
import { Button } from '#chakra-ui/react';
const FabricTextBox = () => {
const [canvas] = useContext(ContextCanvas);
function addTextBox() {
const textbox = new fabric.Textbox('Click on the Rectangle to move it.', {
fontSize: 20,
left: 50,
top: 100,
width: 200,
fill: 'black',
color: 'white',
cornerColor: 'blue',
});
canvas.add(textbox);
canvas.requestRenderAll();
}
return (
<>
<Button
type="button"
colorScheme="blue"
onClick={addTextBox}
variant={'ghost'}
_hover={{}}
_focus={{}}
_active={{}}
textColor={'white'}
fontWeight={'light'}
>
Text Field
</Button>
</>
);
};
export default FabricTextBox;
FabricCanvas.js
import React, { useContext, useLayoutEffect } from 'react';
import { fabric } from 'fabric';
import ContextCanvas from '../../context/ContextCanvas';
const FabricCanvas = () => {
const [canvas, initCanvas] = useContext(ContextCanvas);
useLayoutEffect(() => {
return () => {
initCanvas(new fabric.Canvas('c'));
};
}, []);
return (
<>
<canvas
id="c"
width={window.innerWidth}
height={window.innerHeight}
/>
</>
)
}
export default FabricCanvas;
ContextCanvas.js
import { fabric } from 'fabric';
const ContextCanvas = createContext();
export function CanvasProvider({ children }) {
const [canvas, setCanvas] = useState(null);
const initCanvas = c => {
setCanvas(c);
c.renderAll();
};
return (
<ContextCanvas.Provider value={[canvas, initCanvas]}>
{children}
</ContextCanvas.Provider>
);
}
export default ContextCanvas;
I think the error is related to this line in FabricCircle.js
canvas.add(textbox);
^^^^
because your canvas object is null in production.
Assuming you use React.StrictMode
Using React.StrictMode
With Strict Mode starting in React 18, whenever a component mounts in development, React will simulate immediately unmounting and remounting the component:
Strict mode flow (read more)
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
* React simulates effects being destroyed on a mounted component.
* Layout effects are destroyed. [THIS]
* Effects are destroyed.
* React simulates effects being re-created on a mounted component.
* Layout effects are created
* Effect setup code runs
The step marked with [THIS] is what makes you feel all right in the local environment (but it's an illusion... See why in the next section).
With that been said:
Your canvas is initialized null inside useState and in the useLayoutEffect, you are calling the initCanvas method inside the cleanup function (so it will only be called when it's too late in production, while in development with StrictMode act like an init-function although it's a cleanup function).
useLayoutEffect(() => {
// Your code should be here
return () => { // The returned function is the cleanup function
// This is executed only when disposing the component.
initCanvas(new fabric.Canvas('c'));
// But when strict mode is active
// the component is disposed and re-mounted immidiatly.
};
}, []);
This is why the local environment works and the production environment doesn't.
Solution
Try updating your useLayoutEffect like this:
useLayoutEffect(() => {
initCanvas(new fabric.Canvas('c'));
}, []);
Conclusion
You should not initialize your state inside a cleanup function.
In this case, React.StrictMode behavior prevents you from realizing the error (without strict mode, it wouldn't even work in development).
Since you were initializing the canvas inside the cleanup function, the canvas never get initialized in time (without the strict mode), remaining null, as the error you receive states.

What happens to default server-side values in Remix?

I'm using the following hook (taken from Get viewport/window height in ReactJS). I've adapted the hook so it can be rendered on the server (by gating access to window inside a conditional).
import { useState, useEffect } from 'react';
function getWindowDimensions() {
if (typeof window !== 'undefined') {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height
};
}
return { width: -1, height: -1 }
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowDimensions;
}
I've noticed something interesting / confusing:
The hook does return the default values of {width: -1, height: -1} when it is rendered on the server
These values are seemingly observed by parent component. Example:
export default function FooComponent() {
const { width, height} = useWindowDimensions();
// this invariant fails
invariant(width > -1 && height > -1);
}
However, if I insert a console.log(width, height), below the invariant(...) line, I never see -1, -1 in the browser console.
So it looks like the invariant is only triggered on the server and console.log is only run on the client. Can someone explain what's going on here? I.e, why does the component not observe default server side values?
Is it invariant from https://github.com/zertosh/invariant ?
A note on the readme says that if process.env.NODE_ENV is not production it'll throw even if condition is true.
And for component that should only render client side, there is a cool package by SergioDXA (a top remix contributor) : https://github.com/sergiodxa/remix-utils#clientonly

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

Ensure entrance css transition with React component on render

I am trying to build a barebones css transition wrapper in React, where a boolean property controls an HTML class that toggles css properties that are set to transition. For the use case in question, we also want the component to be unmounted (return null) before the entrance transition and after the exit transition.
To do this, I use two boolean state variables: one that controls the mounting and one that control the HTML class. When props.in goes from false to true, I set mounted to true. Now the trick: if the class is set immediate to "in" when it's first rendered, the transition does not occur. We need the component to be rendered with class "out" first and then change the class to "in".
A setTimeout works but is pretty arbitrary and not strictly tied to the React lifecycle. I've found that even a timeout of 10ms can sometimes fail to produce the effect. It's a crapshoot.
I had thought that using useEffect with mounted as the dependency would work because the component would be rendered and the effect would occur after:
useEffect(if (mounted) { () => setClass("in"); }, [mounted]);
(see full code in context below)
but this fails to produce the transition. I believe this is because React batches operations and chooses when to render to the real DOM, and most of the time doesn't do so until after the effect has also occurred.
How can I guarantee that my class value is change only after, but immediately after, the component is rendered after mounted gets set to true?
Simplified React component:
function Transition(props) {
const [inStyle, setInStyle] = useState(props.in);
const [mounted, setMounted] = useState(props.in);
function transitionAfterMount() {
// // This can work if React happens to render after mounted get set but before
// // the effect; but this is inconsistent. How to wait until after render?
setInStyle(true);
// // this works, but is arbitrary, pits UI delay against robustness, and is not
// // tied to the React lifecycle
// setTimeout(() => setInStyle(true), 35);
}
function unmountAfterTransition() {
setTimeout(() => setMounted(false), props.duration);
}
// mount on props.in, or start exit transition on !props.in
useEffect(() => {
props.in ? setMounted(true) : setInStyle(false);
}, [props.in]);
// initiate transition after mount
useEffect(() => {
if (mounted) { transitionAfterMount(); }
}, [mounted]);
// unmount after transition
useEffect(() => {
if (!props.in) { unmountAfterTransition(); }
}, [props.in]);
if (!mounted) { return false; }
return (
<div className={"transition " + inStyle ? "in" : "out"}>
{props.children}
</div>
)
}
Example styles:
.in: {
opacity: 1;
}
.out: {
opacity: 0;
}
.transition {
transition-property: opacity;
transition-duration: 1s;
}
And usage
function Main() {
const [show, setShow] = useState(false);
return (
<>
<div onClick={() => setShow(!show)}>Toggle</div>
<Transition in={show} duration={1000}>
Hello, world.
</Transition>
<div>This helps us see when the above component is unmounted</div>
</>
);
}
Found the solution looking outside of React. Using window.requestAnimationFrame allows an action to be take after the next DOM paint.
function transitionAfterMount() {
// hack: setTimeout(() => setInStyle(true), 35);
// not hack:
window.requestAnimationFrame(() => setInStyle(true));
}

Gatsby build fails because window is undefined in a React hook

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
}

Resources