Gatsby/React navigator

Hello I’m currently into an issue where I want to show a pop up using JS in my react component but I’m encountering an error when building my gatsby. WebpackError: ReferenceError: navigator is not defined. Here is my JS code that I will use on my React component.

JS

var isMobile = {
Android: function () {
    return navigator.userAgent.match(/Android/i)
  },
  iOS: function () {
    return navigator.userAgent.match(/iPhone|iPad|iPod/i)
  },
  Windows: function () {
    return navigator.userAgent.match(/IEMobile/i)
  },
  any: function () {
    return isMobile.Android() || isMobile.iOS() || isMobile.Windows()
  },
}

if (!isMobile.any()) {
  $('body').addClass('is-not-ios')
  $('.show-ios, .show-android').addClass('disabled')
  $('.show-no-device').removeClass('disabled')
}

if (isMobile.Android()) {
  $('body').addClass('is-not-ios')
  $('head').append('<meta name="theme-color" content="#FFFFFF"> />')
  $('.show-android').removeClass('disabled')
  $(
    '.show-ios, .show-no-device, .simulate-android, .simulate-iphones'
  ).addClass('disabled')
}

if (isMobile.iOS()) {
  $('body').addClass('is-ios')
  $('.show-ios').removeClass('disabled')
  $(
    '.show-android, .show-no-device, .simulate-android, .simulate-iphones'
  ).addClass('disabled')
}

if (pwaEnabled === true) {
  //Setting Timeout Before Prompt Shows Again if Dismissed
  var now = new Date()
  var start = new Date(now.getFullYear(), 0, 0)
  var diff = now - start
  var oneDay = 1000 * 60 * 60 * 24
  var day = Math.floor(diff / oneDay)
  var dismissDate = localStorage.getItem('Appkit-PWA-Timeout-Value')

  if (day - dismissDate > pwaRemind) {
    localStorage.removeItem('Appkit-PWA-Prompt')
  }

  //Dismiss Prompt Button
  $('.pwa-dismiss').on('click', function () {
    console.log('User Closed Add to Home / PWA Prompt')
    localStorage.setItem('Appkit-PWA-Prompt', 'install-rejected')
    $('body')
      .find('#menu-install-pwa-android, #menu-install-pwa-ios, .menu-hider')
      .removeClass('menu-active')
    localStorage.setItem('Appkit-PWA-Timeout-Value', day)
  })

  //Detecting Mobile Operating Systems
  var isMobile = {
    Android: function () {
      return navigator.userAgent.match(/Android/i)
    },
    iOS: function () {
      return navigator.userAgent.match(/iPhone|iPad|iPod/i)
    },
    any: function () {
      return isMobile.Android() || isMobile.iOS() || isMobile.Windows()
    },
  }
  var isInWebAppiOS = window.navigator.standalone == true
  var isInWebAppChrome = window.matchMedia('(display-mode: standalone)').matches

  //Trigger Install Prompt for Android
  if (isMobile.Android()) {
    function showInstallPrompt() {
      if ($('#menu-install-pwa-android, .add-to-home').length) {
        if (localStorage.getItem('Appkit-PWA-Prompt') != 'install-rejected') {
          setTimeout(function () {
            $('.add-to-home').addClass(
              'add-to-home-visible add-to-home-android'
            )
            $('#menu-install-pwa-android, .menu-hider').addClass('menu-active')
          }, 4500)
          console.log('Triggering PWA Window for Android')
        } else {
          console.log(
            'PWA Install Rejected. Will Show Again in ' +
              (dismissDate - day + pwaRemind) +
              ' Days'
          )
        }
      } else {
        console.log(
          'The div #menu-install-pwa-android was not found. Please add this div to show the install window'
        )
      }
    }
    let deferredPrompt
    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault()
      deferredPrompt = e
      showInstallPrompt()
    })
    $('.pwa-install').on('click', function (e) {
      deferredPrompt.prompt()
      deferredPrompt.userChoice.then((choiceResult) => {
        if (choiceResult.outcome === 'accepted') {
          //console.log('User accepted the A2HS prompt');
        } else {
          //console.log('User dismissed the A2HS prompt');
        }
        deferredPrompt = null
      })
    })
    window.addEventListener('appinstalled', (evt) => {
      $('#menu-install-pwa-android, .menu-hider').removeClass('menu-active')
    })
  }

  //Trigger Install Guide iOS
  if (isMobile.iOS()) {
    if (!isInWebAppiOS) {
      if ($('#menu-install-pwa-ios, .add-to-home').length) {
        if (localStorage.getItem('Appkit-PWA-Prompt') != 'install-rejected') {
          console.log('Triggering PWA Window for iOS')
          setTimeout(function () {
            $('.add-to-home').addClass('add-to-home-visible add-to-home-ios')
            $('#menu-install-pwa-ios, .menu-hider').addClass('menu-active')
          }, 4500)
        } else {
          console.log(
            'PWA Install Rejected. Will Show Again in ' +
              (dismissDate - day + pwaRemind) +
              ' Days'
          )
        }
      } else {
        console.log(
          'The div #menu-install-pwa-ios was not found. Please add this div to show the install window'
        )
      }
    }
  }
}

const loadScript = () => window.addEventListener('load', () => isMobile())

export default loadScript

And here is my component where I’ll be using the loadScript

const MasterIndexPage = () => {

useEffect(() => {
loadScript()
  }, [])

<div
      id="menu-video"
      className="menu menu-box-bottom rounded-m"
      data-menu-height="410"
      data-menu-effect="menu-over"
    >
      <div class="responsive-iframe max-iframe">
        <iframe
          src="https://www.youtube.com/embed/qCSBMbUa9jg"
          frameborder="0"
          allowfullscreen
        ></iframe>
      </div>
      <div className="menu-title">
        <p className="color-highlight">Learn</p>
        <h1>How to install</h1>
        <a href="#" className="close-menu">
          <i className="fa fa-times-circle"></i>
        </a>
      </div>
      <div className="content mt-n2">
        <p>Install Sparkle</p>
        <a
          href="#"
          className="close-menu btn btn-full btn-m shadow-l rounded-s text-uppercase font-600 bg-green-dark mt-n2"
        >
          Done
        </a>
      </div>
    </div>
}

Can anyone help me on what is the problem? This is my expected output when I’m using only full JS.

enter image description here

This will prompt whenever I load the page.

Answer

I’m encountering an error when building my gatsby

Summarizing and simplifying, gatsby develop is interpreted directly by the client (browser) using a web socket (that’s why you have instant refresh) and there’s a window or navigator object, while gatsby build is handled by the Node server, where obviously there’s no window, document or other global objects (like navigator) because they are not even defined yet.

It’s a quite common and straight-forward issue in Gatsby that can be easily bypassed by adding the following condition:

typeof window !== `undefined`

If the window object is defined, means that you are not in the build-time process so you can access it.

useEffect(() => {
  if(typeof window !== 'undefined') loadScript()
  }, [])

Or by adding it in each navigator statement:

Android: function () {
    return typeof window !== 'undefined' && navigator.userAgent.match(/Android/i)
  },
  iOS: function () {
    return typeof window !== 'undefined' && 
 navigator.userAgent.match(/iPhone|iPad|iPod/i)
  },
  Windows: function () {
    return typeof window !== 'undefined' && 
 navigator.userAgent.match(/IEMobile/i)
  },
  any: function () {
    return typeof window !== 'undefined' &&  (isMobile.Android() || isMobile.iOS() || isMobile.Windows())
  },
}

Of course, tweak it as you wish.

This workaround (typeof window !== 'undefined') applies to all references to window, document, navigator, and other global objects unavailable in the SSR everywhere in your code.


Outside the scope of the question. It’s extremely not recommended to avoid pointing the DOM like you are doing (with jQuery) in:

$('body').addClass('is-not-ios')

Really, don’t do it.

With React, you are creating and manipulating a virtual DOM (vDOM) to avoid high-performance actions like the ones that manipulate the real DOM are. With React, you can use hooks (useRef) or other workarounds to point to those elements within a React scope and environment.

Your approach will lead you to hydration issues because you are performing actions outside the scope of React. This means that you may face some rendering issues of some elements, especially when moving back and forward or when triggering some display actions.


Do you know what can be my best approach in this kind of problem?

Yes, avoid using jQuery.

You can achieve the same behavior by creating a useState hook that changes its value depending on your userAgent logic to add a class.

For example:

const MasterIndexPage = props =>{
   const [userAgent, setUserAgent]=useState("");

   const detectUserAgent = ()=>{
    if(navigator.userAgent.match(/Android/i)) setUserAgent("isAndroid");
   // and so on for the rest
   
   }

   useEffect(() => {
     detectUserAgent()
  }, [])
 
   return <main className={`${userAgent === "isAndroid" ? "someClassName" : ``}`}>
     <div
      id="menu-video"
      className="menu menu-box-bottom rounded-m"
      data-menu-height="410"
      data-menu-effect="menu-over"
    >
      <div class="responsive-iframe max-iframe">
        <iframe
          src="https://www.youtube.com/embed/qCSBMbUa9jg"
          frameborder="0"
          allowfullscreen
        ></iframe>
      </div>
      <div className="menu-title">
        <p className="color-highlight">Learn</p>
        <h1>How to install</h1>
        <a href="#" className="close-menu">
          <i className="fa fa-times-circle"></i>
        </a>
      </div>
      <div className="content mt-n2">
        <p>Install Sparkle</p>
        <a
          href="#"
          className="close-menu btn btn-full btn-m shadow-l rounded-s text-uppercase font-600 bg-green-dark mt-n2"
        >
          Done
        </a>
      </div>
    </div>
    <main>
}

To avoid extending the answer more than it actually is I’ve only added the Android workaround but the same logic applies to the rest. You can even add a function call inside the className to return the userAgent validation to avoid ternary conditions like className={someFunctionThatReturnsTheClassname()}. You can, of course, extend and isolate all this logic into separate functions even in a custom hook that fetches and returns the userAgent value.