Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

on a web application I want to display two different Menu, one for the Mobile, one for the Desktop browser. I use Next.js application with server-side rendering and the library react-device-detect.

Here is the CodeSandox link.

import Link from "next/link";
import { BrowserView, MobileView } from "react-device-detect";

export default () => (
  <div>
    Hello World.{" "}
    <Link href="/about">
      <a>About</a>
    </Link>
    <BrowserView>
      <h1> This is rendered only in browser </h1>
    </BrowserView>
    <MobileView>
      <h1> This is rendered only on mobile </h1>
    </MobileView>
  </div>
);

If you open this in a browser and switch to mobile view and look the console you get this error:

Warning: Text content did not match. Server: " This is rendered only in browser " Client: " This is rendered only on mobile "

This happen because the rendering by the server detects a browser and on the client, he is a mobile device. The only workaround I found is to generate both and use the CSS like this:

.activeOnMobile {
  @media screen and (min-width: 800px) {
    display: none;
  }
}

.activeOnDesktop {
  @media screen and (max-width: 800px) {
    display: none;
  }
}

Instead of the library but I don't really like this method. Does someone know the good practice to handle devices type on an SSR app directly in the react code?

like image 615
Benjamin Sx Avatar asked Dec 26 '19 22:12

Benjamin Sx


5 Answers

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.

like image 116
Paul van Dyk Avatar answered Oct 10 '22 09:10

Paul van Dyk


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

like image 32
Dylanbob211 Avatar answered Oct 10 '22 09:10

Dylanbob211


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.

like image 3
BernardA Avatar answered Oct 10 '22 08:10

BernardA


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.

like image 3
Someone Special Avatar answered Oct 10 '22 10:10

Someone Special


When I was working on one of my next.js projects, I came across a similar situation. I have got some ideas from the answers. And I did solve it with the following approach.

Firstly, I made custom hook using react-device-detect

//hooks/useDevice.ts
import { isDesktop, isMobile } from 'react-device-detect';

interface DeviceDetection {
  isMobile: boolean;
  isDesktop: boolean;
}

const useDevice = (): DeviceDetection => ({
  isMobile,
  isDesktop
});

export default useDevice;

Secondly, I made a component which uses of custom hook

//Device/Device.tsx
import { ReactElement } from 'react';

import useDevice from '@/hooks/useDevice';

export interface DeviceProps {
  desktop?: boolean;
  mobile?: boolean;
  children: ReactElement;
}

export const Device = ({ desktop, mobile, children }: DeviceProps): ReactElement | null => {
  const { isMobile } = useDevice();

  return (isMobile && mobile) || (!isMobile && desktop) ? children : null;
};

Thirdly, I import the component dynamically using next.js next/dynamic

//Device/index.tsx
import dynamic from 'next/dynamic';
    
import type { DeviceProps } from './Device';
    
export const Device = dynamic<DeviceProps>(() => import('./Device').then((mod) => mod.Device), {
      ssr: false
    });

Finally, I used it following way in pages.

//pages/my-page.tsx
import { Device } from '@/components/Device';
<Device desktop>
    <my-component>Desktop</my-component>
 </Device>
<Device mobile>
    <my-component>Mobile</my-component>
 </Device>
like image 1
BinodNepali Avatar answered Oct 10 '22 08:10

BinodNepali