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

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

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.

Two React useEffect hooks accessing the window onscroll?

I am new to React dev so this may be something simple I am missing with hooks.
Using a template, I have used a header bar which shrinks in height if you scroll down in the page far enough (i.e it is only at max height if you scroll to the top).
I have been customising a sidebar to go along with the headerbar, and I'm trying to get the items within it to also move up when the bottom of the headerbar moves up.
The app bar uses a pre-made function:
import { useState, useEffect } from 'react';
// ----------------------------------------------------------------------
export default function useOffSetTop(top: number) {
const [offsetTop, setOffSetTop] = useState(false);
const isTop = top || 100;
useEffect(() => {
window.onscroll = () => {
if (window.pageYOffset > isTop) {
setOffSetTop(true);
} else {
setOffSetTop(false);
}
};
return () => {
window.onscroll = null;
};
}, [isTop]);
return offsetTop;
}
Then you can just import it, assign a constant bool to useOffSetTop(HEADER.DASHBOARD_DESKTOP_HEIGHT) and base the layout on the state of that const.
In the app bar it controls the height, so in the nav bar I made it control he height of an empty .
It does work, but the app bar stops working.
I do have hot-reload on and if I make a change to the app bar it starts working but the nav bar stops working.
I guess it is just because whichever loads last is the one which binds something to window.onscroll and the other is wiped.
I am just wondering how I could change this function or restructure the code so that this could be imported by multiple components on the same page - possibly without having to just import it higher up and pass the true/false value down through the components?
The issue is that you are actually "overriding" the onScroll function (or replacing it) instead of listening for the event.
by doing this
window.onScroll = null;
you are effectively overriding the onScroll function to do nothing.
Best to listen for the onscroll event.
import { useState, useEffect } from 'react';
export default function useOffSetTop(top: number) {
const [offsetTop, setOffSetTop] = useState(false);
const isTop = top || 100;
const handleOnScroll = () => {
if (window.pageYOffset > isTop) {
setOffSetTop(true);
} else {
setOffSetTop(false);
}
}
useEffect(() => {
window.addEventListener('scroll', handleOnScroll )
return () => {
window.removeEventListener('scroll', handleOnScroll)
};
}, [isTop, handleOnScroll]);
return offsetTop;
}

strange behaviour in my code using react hooks

This might sound silly, but I'm trying to understand my own code and I wanted to see your input here. I'm using useRef() to click a HTML element on user changing the screen. For some reason the ref.current passes the if condition I created so it only executes if this isn't null. Not sure why? I managed to make it work but I had to add an additional if statement to the onresize function, could someone explain why this is the case.
import React, { useRef, useEffect } from 'react';
import classes from './Backdrop.css';
const backdrop = (props) => {
const backDropRef = useRef(null);
useEffect(() => {
if (backDropRef.current !== null && props.show) {
document.body.onresize = () => {
if (backDropRef.current !== null) {
backDropRef.current.click();
}
};
}
}, [backDropRef.current, props.show]);
let classForBackdrop = classes.Backdrop;
if (props.toolTipShow) {
classForBackdrop = classes.BackdropForToolTip;
}
return props.show ? (
<div
ref={backDropRef}
className={classForBackdrop}
onClick={props.clicked}
id="back-drop"
></div>
) : null;
};
export default backdrop;
I will break into parts:
on true at if conditional it attaches onresize listener that has backDropRef.current.click();
backDropRef.current points to your div;
once show prop turns to false div is removed from DOM;
backDropRef.current lost div reference, and now is null;
After all this, you need the if condition because otherwise any resize event triggered when show is false there is no <div>, backDropRef.current is null and you try to call click on a null value which throws an error.

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

Resources