What happens to default server-side values in Remix? - reactjs

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

Related

window-based react hook makes sidebar flicker

I'm using this react hook in a next.js app.
It is supposed to return the width and the state of my sidebar
export const useSidebarWidth = () => {
const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_WIDTH);
const handleResize = () => {
if (window.innerWidth < SIDEBAR_BREAKPOINT) {
setSidebarWidth(SIDEBAR_WIDTH);
} else {
setSidebarWidth(SIDEBAR_WIDTH_EXPANDED);
}
};
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const isExpanded = useMemo(() => {
return sidebarWidth === SIDEBAR_WIDTH_EXPANDED;
}, [sidebarWidth]);
return {
sidebarWidth: isExpanded ? SIDEBAR_WIDTH_EXPANDED : SIDEBAR_WIDTH,
isExpanded,
};
};
Unfortunately, when navigating through my app, the sidebar flickers: for a short moment, it takes its small width, and immediately after, it expands. This happens every time I move to another page.
How can I make sure that this doesn't happen ?
Depending on your build configuration, window.innerwidth may be initially set to 0 for the first frame and then be updated next render. For example, I tried re-creating this issue and got the same result in a CRA application but not in a Vite application.
Either way, one possible solution is to use window.outerWidth, which is usually the same as window.innerWidth for most end-users. A possible implementation in your case would be:
const width = window.innerWidth === 0 ? window.outerWidth : window.innerWidth;
if (width < SIDEBAR_BREAKPOINT) {
setSidebarWidth(SIDEBAR_WIDTH);
} else {
setSidebarWidth(SIDEBAR_WIDTH_EXPANDED);
}
This is obviously not a perfect solution, but I think it's better than what you have, as window.outerWidth is a more accurate estimate of window.innerWidth than 0 is.

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

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

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
}

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