How to detect the device on React SSR App with Next.js?
I think you should do it by using getInitialProps in your page, as it runs both on the server and on the client, and getting the device type by first detecting if you are just getting the request for the webpage (so you are still on the server), or if you are re-rendering (so you are on the client).
// index.js
IndexPage.getInitialProps = ({ req }) => {
let userAgent;
if (req) { // if you are on the server and you get a 'req' property from your context
userAgent = req.headers['user-agent'] // get the user-agent from the headers
} else {
userAgent = navigator.userAgent // if you are on the client you can access the navigator from the window object
}
}
Now you can use a regex to see if the device is a mobile or a desktop.
// still in getInitialProps
let isMobile = Boolean(userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
))
return { isMobile }
Now you can access the isMobile prop that will return either true or false
const IndexPage = ({ isMobile }) => {
return (
<div>
{isMobile ? (<h1>I am on mobile!</h1>) : (<h1>I am on desktop! </h1>)}
</div>
)
}
I got this answer from this article here I hope that was helpful to you
UPDATE
Since Next 9.5.0, getInitialProps
is going to be replaced by getStaticProps
and getServerSideProps
. While getStaticProps
is for fetching static data, which will be used to create an html page at build time, getServerSideProps
generates the page dynamically on each request, and receives the context
object with the req
prop just like getInitialProps
. The difference is that getServerSideProps
is not going to know navigator
, because it is only server side. The usage is also a little bit different, as you have to export an async function, and not declare a method on the component. It would work this way:
const HomePage = ({ deviceType }) => {
let componentToRender
if (deviceType === 'mobile') {
componentToRender = <MobileComponent />
} else {
componentToRender = <DesktopComponent />
}
return componentToRender
}
export async function getServerSideProps(context) {
const UA = context.req.headers['user-agent'];
const isMobile = Boolean(UA.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
))
return {
props: {
deviceType: isMobile ? 'mobile' : 'desktop'
}
}
}
export default HomePage
Please note that since getServerSideProps
and getStaticProps
are mutually exclusive, you would need to give up the SSG advantages given by getStaticProps
in order to know the device type of the user. I would suggest not to use getServerSideProps for this purpose if you need just to handle a couple of styiling details. If the structure of the page is much different depending on the device type than maybe it is worth it
Load only the JS files needed dynamically
You can load components dynamically with next/dynamic, and only the appropriate component will be loaded.
You can use react-detect-device or is-mobile and in my case. In this scenario, I created separate layout for mobile and desktop, and load the appropriate component base on device.
import dynamic from 'next/dynamic';
const mobile = require('is-mobile');
const ShowMobile = dynamic(() => mobile() ? import('./ShowMobile.mobile') : import('./ShowMobile'), { ssr: false })
const TestPage = () => {
return <ShowMobile />
}
export default TestPage
You can view the codesandbox . Only the required component.JS will be loaded.
Edit:
How different is the above from conditionally loading component? e.g.
isMobile ? <MobileComponent /> : <NonMobileComponent />
The first solution will not load the JS file, while in second solution, both JS files will be loaded. So you save one round trip.
With current Next.js (v 9.5+) I accomplished that using next/dynamic
and react-detect-device
.
For instance, on my header
component:
...
import dynamic from 'next/dynamic';
...
const MobileMenuHandler = dynamic(() => import('./mobileMenuHandler'), {
ssr: false,
});
return (
...
<MobileMenuHandler
isMobileMenuOpen={isMobileMenuOpen}
setIsMobileMenuOpen={setIsMobileMenuOpen}
/>
)
...
Then on MobileMenuHandler
, which is only called on the client:
import { isMobile } from 'react-device-detect';
...
return(
{isMobile && !isMobileMenuOpen ? (
<Menu
onClick={() => setIsMobileMenuOpen(true)}
className={classes.menuIcon}
/>
) : null}
)
With that, the react-detect-device
is only active on the client side and can give a proper reading.
See Next.js docs.
LATEST UPDATE:
So if you don't mind doing it client side you can use the dynamic importing as suggested by a few people below. This will be for use cases where you use static page generation.
i created a component which passes all the react-device-detect
exports as props (it would be wise to filter out only the needed exports because then does not treeshake)
// Device/Device.tsx
import { ReactNode } from 'react'
import * as rdd from 'react-device-detect'
interface DeviceProps {
children: (props: typeof rdd) => ReactNode
}
export default function Device(props: DeviceProps) {
return <div className="device-layout-component">{props.children(rdd)}</div>
}
// Device/index.ts
import dynamic from 'next/dynamic'
const Device = dynamic(() => import('./Device'), { ssr: false })
export default Device
and then when you want to make use of the component you can just do
const Example = () => {
return (
<Device>
{({ isMobile }) => {
if (isMobile) return <div>My Mobile View</div>
return <div>My Desktop View</div>
}}
</Device>
)
}
Personally I just use a hook to do this, although the initial props method is better.
import { useEffect } from 'react'
const getMobileDetect = (userAgent: NavigatorID['userAgent']) => {
const isAndroid = () => Boolean(userAgent.match(/Android/i))
const isIos = () => Boolean(userAgent.match(/iPhone|iPad|iPod/i))
const isOpera = () => Boolean(userAgent.match(/Opera Mini/i))
const isWindows = () => Boolean(userAgent.match(/IEMobile/i))
const isSSR = () => Boolean(userAgent.match(/SSR/i))
const isMobile = () => Boolean(isAndroid() || isIos() || isOpera() || isWindows())
const isDesktop = () => Boolean(!isMobile() && !isSSR())
return {
isMobile,
isDesktop,
isAndroid,
isIos,
isSSR,
}
}
const useMobileDetect = () => {
useEffect(() => {}, [])
const userAgent = typeof navigator === 'undefined' ? 'SSR' : navigator.userAgent
return getMobileDetect(userAgent)
}
export default useMobileDetect
I had the problem that scroll animation was annoying on mobile devices so I made a device based enabled scroll animation component;
import React, { ReactNode } from 'react'
import ScrollAnimation, { ScrollAnimationProps } from 'react-animate-on-scroll'
import useMobileDetect from 'src/utils/useMobileDetect'
interface DeviceScrollAnimation extends ScrollAnimationProps {
device: 'mobile' | 'desktop'
children: ReactNode
}
export default function DeviceScrollAnimation({ device, animateIn, animateOut, initiallyVisible, ...props }: DeviceScrollAnimation) {
const currentDevice = useMobileDetect()
const flag = device === 'mobile' ? currentDevice.isMobile() : device === 'desktop' ? currentDevice.isDesktop() : true
return (
<ScrollAnimation
animateIn={flag ? animateIn : 'none'}
animateOut={flag ? animateOut : 'none'}
initiallyVisible={flag ? initiallyVisible : true}
{...props}
/>
)
}
UPDATE:
so after further going down the rabbit hole, the best solution i came up with is using the react-device-detect in a useEffect, if you further inspect the device detect you will notice that it exports const's that are set via the ua-parser-js
lib
export const UA = new UAParser();
export const browser = UA.getBrowser();
export const cpu = UA.getCPU();
export const device = UA.getDevice();
export const engine = UA.getEngine();
export const os = UA.getOS();
export const ua = UA.getUA();
export const setUA = (uaStr) => UA.setUA(uaStr);
This results in the initial device being the server which causes false detection.
I forked the repo and created and added a ssr-selector which requires you to pass in a user-agent. which could be done using the initial props
UPDATE:
Because of Ipads not giving a correct or rather well enough defined user-agent, see this issue, I decided to create a hook to better detect the device
import { useEffect, useState } from 'react'
function isTouchDevice() {
if (typeof window === 'undefined') return false
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
function mq(query) {
return typeof window !== 'undefined' && window.matchMedia(query).matches
}
// @ts-ignore
if ('ontouchstart' in window || (window?.DocumentTouch && document instanceof DocumentTouch)) return true
const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('') // include the 'heartz' - https://git.io/vznFH
return mq(query)
}
export default function useIsTouchDevice() {
const [isTouch, setIsTouch] = useState(false)
useEffect(() => {
const { isAndroid, isIPad13, isIPhone13, isWinPhone, isMobileSafari, isTablet } = require('react-device-detect')
setIsTouch(isTouch || isAndroid || isIPad13 || isIPhone13 || isWinPhone || isMobileSafari || isTablet || isTouchDevice())
}, [])
return isTouch
Because I require the package each time I call that hook, the UA info is updated, it also fixes to SSR out of sync warnings.