Gatsby - IntersectionObserver is not defined - reactjs

I am trying to build my gatsby project but I am unable due to the IntersectionObserver not being recognized. I use the intersectionObserver inside an InView component:
import React, { useRef, useState, useEffect } from 'react'
const InView = ({ children }) => {
const [boundingClientY, setBoundingClientY] = useState(null)
const [direction, setDirection] = useState(null)
const [element, setElement] = useState(null)
const [inView, setInView] = useState(false)
const observer = useRef(new IntersectionObserver((entries) => {
const first = entries[0]
const { boundingClientRect } = first
first.isIntersecting && setInView(true)
!first.isIntersecting && setInView(false)
boundingClientRect.y > boundingClientY && setDirection('down')
boundingClientRect.y < boundingClientY && setDirection('up')
boundingClientY && setBoundingClientY(first.boundingClientRect.y)
}))
useEffect(() => {
const currentElement = element
const currentObserver = observer.current
currentElement && currentObserver.observe(currentElement)
// console.log(currentObserver)
return () => {
currentElement && currentObserver.unobserve(currentElement)
};
}, [element])
const styles = {
opacity: inView ? 1 : 0,
transform: `
translateY(${!inView ?
direction === 'up' ? '-20px' : '20px'
: 0})
rotateY(${!inView ? '35deg' : 0})
scale(${inView ? 1 : 0.9})
`,
transition: 'all 0.4s ease-out 0.2s'
}
return (
<div ref={setElement} style={styles}>
{children}
</div>
)
}
export default InView
I have a wrapper for the root element to enable a global state and have tried importing the polyfill inside gatsby-browser.js:
import React from 'react'
import GlobalContextProvider from './src/components/context/globalContextProvider'
export const wrapRootElement = ({ element }) => {
return (
<GlobalContextProvider>
{element}
</GlobalContextProvider>
)
}
export const onClientEntry = async () => {
if (typeof IntersectionObserver === `undefined`) {
await import(`intersection-observer`);
}
}

This is an error on build, right ($ gatsby build)? If that's the case this has nothing to do with browser support.
It is the fact that IntersectionObserver is a browser API and you should not use browser APIs during server side rendering. Instead you try to utilize them after components have mounted. To solve this initialize your observer in useEffect() instead of useRef() as you currently do.
...
const observer = useRef();
useEffect(() => {
observer.current = new IntersectionObserver({ ... });
}, []); // do this only once, on mount
...

I got my jest test to pass by placing this before the creation of "new IntersectionObserver"
if (!window.IntersectionObserver) return

declare a let variable = null. this also works in NextJS
...
let observer = null
useEffect(()=> {
observer = new IntersectionObserver(callback,optional);
},[])

IntersectionObserver API is a browser API and it can't be executed during Gatbsy build process. Therefore, you should check if your code is running in browser:
let observer = null;
if (typeof window !== "undefined"){ // The code inside brackets will be executed ONLY in browser
observer = new IntersectionObserver(/* ... */);
// ...
}

Related

Why is there a delay when I do useState and useEffect to update a variable?

I saw quite a few posts about a delay between setting the state for a component, however, I'm running into this issue in a custom hook that I built. Basically, the classNames that I'm returning are being applied, just with a delay after the component first renders. I've tried using callback functions and useEffect with no luck. Does anyone have any ideas about why there is a small delay?
import * as React from 'react';
import classNames from 'classnames';
import debounce from 'lodash/debounce';
const useScrollStyling = ref => {
const [isScrolledToBottom, setIsScrolledToBottom] = React.useState(false);
const [isScrolledToTop, setIsScrolledToTop] = React.useState(true);
const [isOverflowScrollingEnabled, setIsOverflowScrollingEnabled] = React.useState(false);
const { current } = ref;
React.useEffect(() => {
if (current) {
const { clientHeight, scrollHeight } = current;
setIsOverflowScrollingEnabled(scrollHeight > clientHeight);
}
}, [current]);
const handleScroll = ({ target }) => {
const { scrollHeight, scrollTop, clientHeight } = target;
const isScrolledBottom = scrollHeight - Math.ceil(scrollTop) === clientHeight;
const isScrolledTop = scrollTop === 0;
setIsScrolledToBottom(isScrolledBottom);
setIsScrolledToTop(isScrolledTop);
};
return {
handleScroll: React.useMemo(() => debounce(handleScroll, 100), []),
scrollShadowClasses: classNames({
'is-scrolled-top': isOverflowScrollingEnabled && isScrolledToTop,
'is-scrolled-bottom': isScrolledToBottom,
'is-scrolled': !isScrolledToTop && !isScrolledToBottom,
}),
};
};
export default useScrollStyling;

Check if an element is in the viewport but with mapped refs - ReactJS

I was wondering if anybody could help.
I've been looking into lots of solutions to check when elements are displayed in the viewpoint and am currently trying to integrate this method into my project - https://www.webtips.dev/webtips/react-hooks/element-in-viewport, this method uses refs however the content I'm wishing to have on the page isn't static but rather mapped. I've found this method to dynamically use refs - https://dev.to/nicm42/react-refs-in-a-loop-1jk4 however I think I'm doing it incorrectly as I'm getting the following error:
TypeError: Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'.
Any help at all is most appreciated.
Many thanks, code below.
Component.js
import { useRef, createRef } from 'react'
import useIntersection from './useIntersection.js'
const MyComponent = ({ someStateData }) => {
const ref= useRef([]);
ref.current = someStateData.map((title, i) => ref.current[i] ?? createRef());
const inViewport = useIntersection(ref, '0px');
if (inViewport) {
console.log('in viewport:', ref.current);
}
{someStateData.map((title, i) => {
return (
<div key={title.id} id={title.id} ref={ref.current[i]}>
{{ title.id }}
</div>
)
})}
}
useIntersection.js
import { useState, useEffect } from 'react'
const useIntersection = (element, rootMargin) => {
const [isVisible, setState] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setState(entry.isIntersecting);
}, { rootMargin }
);
element && observer.observe(element);
return () => observer.unobserve(element);
}, []);
return isVisible;
};
export default useIntersection
ref.current has a collection of refs
const ref= useRef([]);
ref.current = someStateData.map((title, i) => ref.current[i] ?? createRef());
The observe function needs an Element to observe but the code calls with the whole ref
const inViewport = useIntersection(ref, '0px');
const useIntersection = (element, rootMargin) => {
...
...
observer.observe(element);
quoting #backtick "is an array of objects with current properties", the proper call should be something like
observer.observe(ref.current[i].current)

react hook useEffect infinite loop

Below is my code snipet.
When i receieve my prop and try to useSate, i recieve this infine loop even after following number of solutions.
const App = ({ center }) => {
const position = [-1.29008, 36.81987];
const [mapCenter, setMapCenter] = useState();
useEffect(() => {
if (center && center.length > 0) setMapCenter(center);
else setMapCenter(position);
}, [center, position]);
return (<div> </div>)
}
export default App;
The issue is that you are defining position array in functional component and its reference gets changed on each re-render and hence the useEffect executed again.
You can move declaration of position out of component since its a constant like
const position = [-1.29008, 36.81987];
const App = ({ center }) => {
const [mapCenter, setMapCenter] = useState();
useEffect(() => {
if (center && center.length > 0) setMapCenter(center);
else setMapCenter(position);
}, [center, position]);
return (<div> </div>)
}
export default App;
or remove the dependency of position from useEffect
const App = ({ center }) => {
const position = [-1.29008, 36.81987];
const [mapCenter, setMapCenter] = useState();
useEffect(() => {
if (center && center.length > 0) setMapCenter(center);
else setMapCenter(position);
}, [center]);
return (<div> </div>)
}
export default App;
remove the dependency of position from useEffect

How to test useRef with Jest and react-testing-library?

I'm using create-react-app, Jest and react-testing-library for the configuration of the chatbot project.
I have a functional component that uses useRef hook. When a new message comes useEffect hook is triggered and cause scrolling event by looking a ref's current property.
const ChatBot = () => {
const chatBotMessagesRef = useRef(null)
const chatBotContext = useContext(ChatBotContext)
const { chat, typing } = chatBotContext
useEffect(() => {
if (typeof chatMessagesRef.current.scrollTo !== 'undefined' && chat && chat.length > 0) {
chatBotMessagesRef.current.scrollTo({
top: chatMessagesRef.current.scrollHeight,
behavior: 'smooth'
})
}
// eslint-disable-next-line
}, [chat, typing])
return (
<>
<ChatBotHeader />
<div className='chatbot' ref={chatBotMessagesRef}>
{chat && chat.map((message, index) => {
return <ChatBotBoard answers={message.answers} key={index} currentIndex={index + 1} />
})}
{typing &&
<ServerMessage message='' typing isLiveChat={false} />
}
</div>
</>
)
}
I want to be able to test whether is scrollTo function triggered when a new chat item or typing comes, do you have any ideas? I couldn't find a way to test useRef.
You can move your useEffect out of your component and pass a ref as a parameter to it. Something like
const useScrollTo = (chatMessagesRef, chat) => {
useEffect(() => {
if (typeof chatMessagesRef.current.scrollTo !== 'undefined' && chat && chat.length > 0) {
chatBotMessagesRef.current.scrollTo({
top: chatMessagesRef.current.scrollHeight,
behavior: 'smooth'
})
}
}, [chat])
}
Now in your component
import useScrollTo from '../..'; // whatever is your path
const MyComponent = () => {
const chatBotMessagesRef = useRef(null);
const { chat } = useContext(ChatBotContext);
useScrollTo(chatBotMessagesRef, chat);
// your render..
}
Your useScrollTo test:
import useScrollTo from '../..'; // whatever is your path
import { renderHook } from '#testing-library/react-hooks'
it('should scroll', () => {
const ref = {
current: {
scrollTo: jest.fn()
}
}
const chat = ['message1', 'message2']
renderHook(() => useScrollTo(ref, chat))
expect(ref.current.scrollTo).toHaveBeenCalledTimes(1)
})

When to use hooks? is worth it that example?

I have write a hook to check if browser is IE, so that I can reutilize the logic instead of write it in each component..
const useIsIE = () => {
const [isIE, setIsIE] = useState(false);
useEffect(() => {
const ua = navigator.userAgent;
const isIe = ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
setIsIE(isIe);
}, []);
return isIE;
}
export default useIsIE;
Is it worth it to use that hook?
Im not sure if is good idea because that way, Im storing a state and a effect for each hook call (bad performane?) when I can simply use a function like that:
export default () => ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
What do you think? is worth it use that hook or not?
If not, when should I use hooks and when not?
ty
No. Not worth using the hook.
You'd need to use a hook when you need to tab into React's underlying state or lifecycle mechanisms.
Your browser will probably NEVER change during a session so just creating a simple utility function/module would suffice.
I would recommend to set your browser checks in constants and not functions, your browser will never change.
...
export const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor);
export const isIOSChrome = /CriOS/.test(userAgent);
export const isMac = (navigator.platform.toUpperCase().indexOf('MAC') >= 0);
export const isIOS = /iphone|ipad|ipod/.test(userAgent.toLowerCase());
...
This is a simple hook that checks if a element has been scrolled a certain amount of pixels
const useTop = (scrollable) => {
const [show, set] = useState(false);
useEffect(() => {
const scroll = () => {
const { scrollTop } = scrollable;
set(scrollTop >= 50);
};
const throttledScroll = throttle(scroll, 200);
scrollable.addEventListener('scroll', throttledScroll, false);
return () => {
scrollable.removeEventListener('scroll', throttledScroll, false);
};
}, [show]);
return show;
};
Then you can use it in a 'To Top' button to make it visible
...
import { tween } from 'shifty';
import useTop from '../../hooks/useTop';
// scrollRef is your scrollable container ref (getElementById)
const Top = ({ scrollRef }) => {
const t = scrollRef ? useTop(scrollRef) : false;
return (
<div
className={`to-top ${t ? 'show' : ''}`}
onClick={() => {
const { scrollTop } = scrollRef;
tween({
from: { x: scrollTop },
to: { x: 0 },
duration: 800,
easing: 'easeInOutQuart',
step: (state) => {
scrollRef.scrollTop = state.x;
},
});
}}
role="button"
>
<span><ChevronUp size={18} /></span>
</div>
);
};

Resources