How to detect the device on React SSR App with Next.js? How to detect the device on React SSR App with Next.js? reactjs reactjs

How to detect the device on React SSR App with Next.js?


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.tsximport { 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.tsimport 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.


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.jsIndexPage.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 getInitialPropslet 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 hereI 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.