<Helmet>
is a component, so it must be placed inside the return
statement:
return (
<>
<Helmet>
<script src={withPrefix('script.js')} type="text/javascript" />
</Helmet>
<Navbar />
{ children}
<Footer />
</>
)
However, as I usually point in your answers, you need to be careful when dealing with global objects as window
or document
since they may break your code in gatsby build
environment. Those global objects are not available in the code (at the moment they are being requested) during the gatsby build
because it's a process that occurs in the server, where obviously there is no window
. Make some trials and errors to ensure that your code doesn't break.
In addition, you can achieve the same result using a React-friendly approach, using some hook. Otherwise, besides not being a good practice, you are pointing directly to the DOM, blocking React's rehydration, potentially leading with several issues and caveats. In your Navbar
component (where I assume is your .navbar
class) do something like:
// other imports
import { useScrollPosition } from '/path/to/useScrollPosition/useScrollPosition';
const Navbar =()=>{
const [navBarClass, setNavBarClass]=useState("");
const [scroll, setScroll]= useState(0);
useScrollPosition(function setScrollPosition ({ currentPosition: { y:
currentVerticalYPosition } }) {
setScroll(currentVerticalYPosition);
});
useEffect(()=>{
if(scroll < 10)setNavBarClass("active")
else setNavBarClass("")
}, [scroll])
return <nav className={`some-class-name ${navBarClass}`}> your navbar code</div>
}
useScrollPosition
is a custom hook that may look like:
import { useLayoutEffect, useRef } from 'react';
const isBrowser = typeof window !== `undefined`;
const getScrollPosition = ({ element, useWindow }) => {
if (!isBrowser) return { x: 0, y: 0 };
const target = element ? element.current : document.body,
position = target.getBoundingClientRect();
return useWindow
? { x: window.scrollX, y: window.scrollY }
: { x: position.left, y: position.top };
};
export const useScrollPosition = (effect, deps, element, useWindow, wait) => {
const position = useRef(getScrollPosition({ useWindow }));
let throttleTimeout = null;
const callBack = () => {
const currentPosition = getScrollPosition({ element, useWindow });
effect({ previousPosition: position.current, currentPosition: currentPosition });
position.current = currentPosition;
throttleTimeout = null;
};
useLayoutEffect(() => {
const handleScroll = () => {
if (wait && !throttleTimeout) throttleTimeout = setTimeout(callBack, wait);
else callBack();
};
window.addEventListener(`scroll`, handleScroll);
return () => window.removeEventListener(`scroll`, handleScroll);
}, deps);
};
Basically, you are wrapping your logic of calculating the window
stuff inside React's ecosystem, using the states, this won't break your rehydration.
In that way, you are creating a state to hold your nav
class name, initially set as empty (const [navBarClass, setNavBarClass]=useState("")
) and a state that holds the current scroll position (const [scroll, setScroll]= useState(0)
), initially set as 0
.
On the other hand, a useEffect
hook will be triggered every time the scroll
of the window
changes (the user is scrolling), this is controlled by the deps
array ([scroll]
), there, you are holding the logic of setting/removing a new class name if the scroll is bigger than 10 or not.
Since the class name state has changed, your component will be rehydrated again, showing/hiding your class name in real-time. Finally, the logic of calculating the window's parameters are controlled by the custom hook, with its internal logic that doesn't belong to your component.
P.S: a rehydration issue, for example, is when you navigate to one page, and once you go back to the previous page, you don't see some components, because they are not being rendered (rehydrated) due to this issue.
Steps:
Create a file wherever you prefer in your project and name it useScrollPosition.js
.
Paste the following code:
import { useLayoutEffect, useRef } from 'react';
const isBrowser = typeof window !== `undefined`;
const getScrollPosition = ({ element, useWindow }) => {
if (!isBrowser) return { x: 0, y: 0 };
const target = element ? element.current : document.body,
position = target.getBoundingClientRect();
return useWindow
? { x: window.scrollX, y: window.scrollY }
: { x: position.left, y: position.top };
};
export const useScrollPosition = (effect, deps, element, useWindow, wait) => {
const position = useRef(getScrollPosition({ useWindow }));
let throttleTimeout = null;
const callBack = () => {
const currentPosition = getScrollPosition({ element, useWindow });
effect({ previousPosition: position.current, currentPosition: currentPosition });
position.current = currentPosition;
throttleTimeout = null;
};
useLayoutEffect(() => {
const handleScroll = () => {
if (wait && !throttleTimeout) throttleTimeout = setTimeout(callBack, wait);
else callBack();
};
window.addEventListener(`scroll`, handleScroll);
return () => window.removeEventListener(`scroll`, handleScroll);
}, deps);
};
Import it in your desired component as:
import { useScrollPosition } from '/path/to/useScrollPosition/useScrollPosition';
Use it.