Using external script (in JavaScript) in Gatsby JS

Tags: , , ,



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?

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.



Source: stackoverflow