Reputation: 223
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.
This will prompt whenever I load the page.
Upvotes: 0
Views: 1686
Reputation: 29320
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.
Upvotes: 4