I am trying to load a JavaScript code on my gatsby app. Inside my static folder on my root folder I created a code called script.js
. Here’s the snippet inside:
window.addEventListener("scroll", function(e) { const navBar = document.querySelector(".navbar"); if (this.scrollY > 10) { navBar.classList.add("active"); } else { navBar.classList.remove("active"); } })
Then on my Layout
component, I tried to use helmet to include this:
import React, { useEffect } from "react" import { withPrefix, Link } from "gatsby" import Helmet from "react-helmet" import Navbar from '../components/Navbar' import Footer from '../components/Footer' const Layout = ({ children }) => { <Helmet> <script src={withPrefix('script.js')} type="text/javascript" /> </Helmet> let AOS; useEffect(() => { const AOS = require("aos"); AOS.init({ once: true, }); }, []); useEffect(() => { if (AOS) { AOS.refresh(); } }); return ( <> <Navbar /> { children} <Footer /> </> ) } export default Layout
But this returns this error:
error Expected an assignment or function call and instead saw an expression no-unused-expressions
I am not sure if I should place my script inside an anonymous function to make this call but how do I fix this thing?
UPDATE:
So as @Ferran said I need to use script code on my app as hook. Not sure if I did this right, but here are the steps I made.
Inside my Navbar.js
I created a useState hook that will handle resize window function:
import React, { useEffect, useState } from "react" import { Link } from 'gatsby' import useWindowSize from '../../static/script.js' const Navbar = () => { const [navBarClass, setNavBarClass] = useState("") const { height } = useWindowSize() useEffect(()=>{ if(height > 10)setNavBarClass("active") }, [height]) return ( <header className="header sticky-header"> <nav className={`navbar navbar-expand-lg fixed-top py-3 ${navBarClass}`}> <div class="container container-wide"> <Link to="/"><img src={MainLogo} alt="" /></Link> <button type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" className="navbar-toggler navbar-toggler-right"><i className="fa fa-bars"></i></button> <div id="navbarSupportedContent" className="collapse navbar-collapse"> <ul className="navbar-nav ms-auto"> <li className="nav-item active"><a href="#" class="nav-link text-uppercase font-weight-bold">Home <span class="sr-only">(current)</span></a></li> <li className="nav-item"><a href="#" class="nav-link text-uppercase font-weight-bold">About</a></li> <li className="nav-item"><a href="#" class="nav-link text-uppercase font-weight-bold">Gallery</a></li> <li className="nav-item"><a href="#" class="nav-link text-uppercase font-weight-bold">Portfolio</a></li> <li className="nav-item"><a href="#" class="nav-link text-uppercase font-weight-bold">Contact</a></li> </ul> </div> </div> </nav> </header> ) } export default Navbar
Then inside my static
folder in the root outside the src
folder I place the same exact code:
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(() => { // 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; }
Then back to Navbar.js
I imported it as a component:
import useWindowSize from '../../static/script.js'
Am I doing this right?
Advertisement
Answer
<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.