React SSR, Proper way of handling page scroll position - reactjs

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);
}
}, []);

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.

React Infinite Loading hook, previous trigger

Im trying to make a hook similar to Waypoint.
I simply want to load items and then when the waypoint is out of screen, allow it to load more items if the waypoint is reached.
I can't seem to figure out the logic to have this work properly.
Currently it see the observer state that its on the screen. then it fetches data rapidly.
I think this is because the hook starts at false everytime. Im not sure how to make it true so the data can load. Followed by the opposite when its reached again.
Any ideas.
Here's the hook:
import { useEffect, useState, useRef, RefObject } from 'react';
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
useEffect(() => {
observerRef.current.observe(ref.current);
return () => {
observerRef.current.disconnect();
};
}, [ref]);
return isOnScreen;
}
Here's the use of it:
import React, { useRef } from 'react';
import { WithT } from 'i18next';
import useOnScreen from 'utils/useOnScreen';
interface IInboxListProps extends WithT {
messages: any;
fetchData: () => void;
searchTerm: string;
chatID: string | null;
}
const InboxList: React.FC<IInboxListProps> = ({ messages, fetchData, searchTerm, chatID}) => {
const elementRef = useRef(null);
const isOnScreen = useOnScreen(elementRef);
if (isOnScreen) {
fetchData();
}
const renderItem = () => {
return (
<div className='item unread' key={chatID}>
Item
</div>
);
};
const renderMsgList = ({ messages }) => {
return (
<>
{messages.map(() => {
return renderItem();
})}
</>
);
};
let messagesCopy = [...messages];
//filter results
if (searchTerm !== '') {
messagesCopy = messages.filter(msg => msg.user.toLocaleLowerCase().startsWith(searchTerm.toLocaleLowerCase()));
}
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={elementRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
export default InboxList;
Let's inspect this piece of code
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
We have the following meanings:
.isIntersecting is TRUE --> The element became visible
.isIntersecting is FALSE --> The element disappeared
and
isOnScreen is TRUE --> The element was at least once visible
isOnScreen is FALSE--> The element was never visible
When using a xor (!==) you specify that it:
Was never visible and just became visible
this happens 1 time just after the first intersection
Was visible once and now disappeared
this happens n times each time the element is out of the screen
What you want to do is to get more items each time the element intersects
export default function useOnScreen(ref: RefObject<HTMLElement>, onIntersect: function) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
setIsOnScreen(entry.isIntersecting);
});
}, []);
useEffect(()=?{
if(isOnScreen){
onIntersect();
}
},[isOnScreen,onIntersect])
...
}
and then use it like:
const refetch= useCallback(()=>{
fetchData();
},[fetchData]);
const isOnScreen = useOnScreen(elementRef, refetch);
or simply:
const isOnScreen = useOnScreen(elementRef, fetchData);
If fetchData changes reference for some reason, you might want to use the following instead:
const refetch= useRef(fetchData);
const isOnScreen = useOnScreen(elementRef, refetch);
Remember that useOnScreen has to call it like onIntersect.current()
In InboxList component, what we are saying by this code
if (isOnScreen) {
fetchData();
}
is that, every time InboxList renders, if waypoint is on screen, then initiate the fetch, regardless of whether previous fetch is still in progress.
Note that InboxList could get re-rendered, possibly multiple times, while the fetch is going on, due to many reasons e.g. parent component re-rendering. Every re-rendering will initiate new fetch as long as waypoint is on screen.
To prevent this, we need to keep track of ongoing fetch, something like typical isLoading state variable. Then initiate new fetch only if isLoading === false && isOnScreen.
Alternatively, if it is guaranteed that every fetch will push the waypoint off screen, then we can initiate the fetch only when waypoint is coming on screen, i.e. isOnScreen is changing to true from false :
useEffect(() => {
if (isOnScreen) {
fetchData();
}
}, [isOnScreen]);
However, this will not function correctly if our assumption, that the waypoint goes out of screen on every fetch, does not hold good. This could happen because
pageSize of fetch small and display area can accommodate more
elements
data received from a fetch is getting filtered out due to
client side filtering e.g. searchTerm.
As my assumption. Also you can try this way.
const observeRef = useRef(null);
const [isOnScreen, setIsOnScreen] = useState(false);
const [prevY, setPrevY] = useState(0);
useEffect(()=>{
fetchData();
var option = {
root : null,
rootmargin : "0px",
threshold : 1.0 };
const observer = new IntersectionObserver(
handleObserver(),
option
);
const handleObserver = (entities, observer) => {
const y = observeRef.current.boundingClientRect.y;
if (prevY > y) {
fetchData();
}
setPrevY(y);
}
},[prevY]);
In this case we not focus chat message. we only focus below the chat<div className="item element. when div element trigger by scroll bar the fetchData() calling again and again..
Explain :
In this case we need to use IntersectionObserver for read the element position. we need to pass two parameter for IntersectionObserver.
-first off all in the hanlderObserver you can see boundingClientRect.y. the boundingClientRect method read the element postion. In this case we need only y axis because use y.
when the scrollbar reach div element, y value changed. and then fetchData() is trigger again.
root : This is the root to use for the intersection. rootMargin : Just like a margin property, which is used to provide the margin value to the root either in pixel or in percent (%) . threshold : The number which is used to trigger the callback once the intersection’s area changes to be greater than or equal to the value we have provided in this example .
finally you can add loading status for loading data.
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={observeRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
I hope its correct, i'm not sure. may it's helpful someone. thank you..

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

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
}

Scroll to y location within react element with useRef

I have an component on my page that scrolls, but the rest doesn't. I want to scroll to a certain Y location within this element, when I change a useState value using a different component.
const scrollMe = useRef();
useEffect(
() => {
if (scrollMe === true) {
scrollPanel.scrollTo(0, 300)
}
},
[ scrollMe ]
);
The scrollable component is like so:
<div ref={scrollMe} onScroll={(e) => handleScroll(e)}>A mapped array</div>
The scroll function is as follows:
const handleScroll = (e) => {
if (e.target.scrollTop !== 0) {
setTopBarPosition(true);
} else {
setTopBarPosition(false);
}
};
Setting the topBarPosition affects
<div className={`topBar ${topBarPosition === true ? 'topBarToTop' : ''}`}>
<TopBar/>
</div>
And the css class topBarToTop moves the topBar to the top of the page:
.topBarToTop {
top: 30px;
}
How come I just get scrollMe.scrollTo is not a function ?
I made a codesandbox that illustrates what I'm trying to do:
https://codesandbox.io/s/react-hooks-counter-demo-izho9
You need to make some small changes, Please follow the code comments:
useEffect(() => {
// swap the conditions to check for childRef first
if (childRef && topPosition) {
// use to get the size (width, height) of an element and its position
// (x, y, top, left, right, bottom) relative to the viewport.
const { y } = childRef.current.getBoundingClientRect()
// You have to call `current` from the Ref
// Add the `scrollTo()` second parameter (which is left)
childRef.current.scrollTo(y, 0)
// Initially it was set to `false` and we change it to `true` when click on scroll
// button then we change it back to `false` when re-render
setTopPosition(false)
}
}, [topPosition, childRef])
More about getBoundingClientRect()

Resources