Reputation: 287
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?
Upvotes: 49
Views: 136721
Reputation: 63
In next js with App dir you can do like
import { headers } from "next/headers";
export const isMobileRequest = () => {
const userAgent = headers().get("user-agent") || "";
const mobileRegex =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
const isMobile = mobileRegex.test(userAgent);
return isMobile
};
Upvotes: 0
Reputation: 4012
import { NextRequest, NextResponse, userAgent } from 'next/server'
export function middleware(request: NextRequest) {
const url = request.nextUrl
const { device } = userAgent(request)
const viewport = device.type === 'mobile' ? 'mobile' : 'desktop'
url.searchParams.set('viewport', viewport)
return NextResponse.rewrite(url)
}
https://nextjs.org/docs/app/api-reference/functions/userAgent
Upvotes: 2
Reputation: 1213
I understand this is a much answered question, but I've just addressed this issue and want to share my findings.
Scenario - In my case, I want to hide and show app store buttons/qr codes based on device.
Client Side Solution - I tried using react-device-detect
for client components but relying on isMobile
caused className issues with SSR unless using useEffect
which caused a page flash and clunky experience.
Server Side Solution - As its possible to access the userAgent via middleware, I've added the os name to the request headers, which I then access in a server component (e.g OSWrapper).
export const middleware = async (request: NextRequest) => {
const {
os: { name },
device: { type },
} = userAgent(request)
...
const requestHeaders = new Headers(request.headers)
name && requestHeaders.set('x-request-agent', name)
const res = NextResponse.next({
request: {
headers: requestHeaders,
},
})
...
import { ANDROID, ANDROID_CLIENT, IOS, IOS_CLIENT, UNKNOWN_CLIENT } from '@utils/constants'
import { headers } from 'next/headers'
type Props = {
children: React.ReactNode
}
export const OSWrapper = ({ children }: Props) => {
const heads = headers()
const agent = heads.get('x-request-agent')
let os
switch (agent) {
case ANDROID:
os = ANDROID_CLIENT
break
case IOS:
os = IOS_CLIENT
break
default:
os = UNKNOWN_CLIENT
break
}
return <div className={os}>{children}</div>
}
export default OSWrapper
That you I can use global css via:
* {
:global(.android) {
.wrapper {
...
}
}
}
Upvotes: 0
Reputation: 1069
Ideally it is best to do it the CSS way, as it is the most accurate. But if you need to make more complex layout decisions, you can use the user agent header to guess the type of device.
This is an example using React/NextJS/SSR. You can store the user-agent in a context and read it from a hook useIsMobile()
which returns true or false based on the parsed user-agent header.
We create a context to store the parsed user-agent header.
// userAgentContext.ts
import { userAgent } from "next/server";
import { createContext } from "react";
export type UserAgent = ReturnType<typeof userAgent>;
export const UserAgentContext = createContext<UserAgent | undefined>(undefined);
We create a hook to consume the context and check device type.
// useIsMobile.ts
import { useContext } from "react";
import { UserAgentContext } from "./userAgentContext.ts";
export const useIsMobile = () => {
const userAgent = useContext(UserAgentContext);
if (!userAgent) {
throw new Error("useIsMobile must be used within a UserAgentContext.Provider");
}
return userAgent.device.type === "mobile";
};
We can use a sever component as the layout (app router) to get the user agent from request headers
// layout.tsx
"use server";
import { headers } from "next/headers";
import { userAgent } from "next/server";
import { LayoutClient } from "./layoutClient.ts";
export default async function Layout({ children }: { children: React.ReactNode }) {
const reqUserAgent = userAgent({ headers: headers() });
return <LayoutClient reqUserAgent={reqUserAgent}>{children}</LayoutClient>
}
We can then pass the user agent to a client component that will provide the initial value for the context. Since client components with "use client"
directive are still rendered once on the server with initial values then hydrated on the client.
// layoutClient.tsx
"use client";
import { UserAgentContext } from "./userAgentContext.ts";
export default function LayoutClient({ reqUserAgent, children }: { reqUserAgent: UserAgent, children: React.ReactNode }) {
return <UserAgentContext.Provider value={reqUserAgent}>{children}</UserAgentContext.Provider>;
}
Now when this page is rendered for the first time on the server with initial values, useIsMobile() should accurately return a value based on the user-agent device type.
// page.tsx
"use client";
export default function Page() {
const isMobile = useIsMobile();
return isMobile ? <>Mobile</> : <>Desktop</>;
}
You can rewrite the hook to return more precise selectors with react-device-detect
import { getSelectorsByUserAgent } from "react-device-detect";
export const useSelectorsByUserAgent = () => {
const userAgent = useContext(UserAgentContext);
if (!userAgent) {
throw new Error("useUserAgent must be used within a UserAgentContext.Provider");
}
return getSelectorsByUserAgent(userAgent);
};
const { isIOS, isIPad13 } = useSelectorsByUserAgent();
return isIPad13 ? <>IPad 13</> : isIOS ? <>IOS</> : <>Other</>;
Upvotes: 1
Reputation: 173
One easy way to fix this is to use something like the NoSsr component from MUI.
For example
{ isMobile &&
<NoSsr>
<ComponentYouWantToRenderInMobile />
</NoSsr>
}
That will stop the error from appearing.
So in OPs case:
import Link from "next/link";
import { BrowserView, MobileView } from "react-device-detect";
import { NoSsr } from "@mui/material";
export default () => (
<div>
Hello World.{" "}
<Link href="/about">
<a>About</a>
</Link>
<NoSsr>
<BrowserView>
<h1> This is rendered only in browser </h1>
</BrowserView>
<MobileView>
<h1> This is rendered only on mobile </h1>
</MobileView>
</ NoSsr>
</div>
);
Upvotes: 0
Reputation: 123
for ssr just use
import {headers} from 'next/headers';
const layout = ({children}: { children: React.ReactNode }) => {
const headersList = headers()
const isMobile = isMobileUserAgent(headersList.get('user-agent'))
and function is:
export const isMobileUserAgent = (userAgent) => {
return /iPhone|iPad|iPod|Android/i.test(userAgent);
};
Upvotes: 3
Reputation: 153
react-device-detect
now offers a getSelectorsByUserAgent
you can use server-side and you need to pass the user-agent as an argument. If you are using app router in next.js, you can get the user-agent from the headers.
// index.tsx
import { headers } from "next/headers"
import { getSelectorsByUserAgent } from "react-device-detect"
import MobileVersion from "./mobile"
import BrowserVersion from "./browser"
export default function SomeComponent() {
// you can get more than just isMobile from this.
const { isMobile } = getSelectorsByUserAgent(
headers().get("user-agent") ?? ""
)
return isMobile ? <Mobile /> : <Browser />
}
//mobile.tsx
export default () => <div>this is the mobile version</div>
//browser.tsx
export default () => <div>this is the browser version</div>
Upvotes: 12
Reputation: 106
If you want to do something with user-agent information in nextjs
from server side you'll have to use getServerSide
props. because this is the only function that has access to req object. getStaticProps
is not helpful.
First create a helper function just to reuse on several pages.
const getDevice = (userAgent) => {
let device = "";
if(userAgent && userAgent !== ""){
let isMobile = userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)
if(isMobile && isMobile?.length > 0){
device = "mobile";
}
}
return device
}
You can further modify above function as per your need.
Now in your getServerSideProps:
export const getServerSideProps = ({req}) => {
const device = getDevice(req.headers['user-agent']);
return {
props: {
device,
}
}
}
Now you have device information in your page. You can use to render different totally different layouts just like flipkart and olx.
NOTE : Changes will only reflect when a fresh page will be requested because server does not aware of client changes in viewport. If you want such thing probably you can use context api.
The downside is : You have to make each page that shifts layout, a server rendered page.
However if you are going to deploy your nextjs on netlify consider using middlewares
with combination of @netlify/next
package. More info here
Upvotes: 1
Reputation: 502
There is a way to resolve with react-device-detect.
export async function getServerSideProps({ req, res }: GetServerSidePropsContext) {
const userAgent = req.headers['user-agent'] || '';
const { isMobile } = getSelectorsByUserAgent(userAgent);
return {
props: { isMobile },
};
}
you can find more keys below because it is not specified on type definition of react-device-detect lib.
{
isSmartTV: false,
isConsole: false,
isWearable: false,
isEmbedded: false,
isMobileSafari: false,
isChromium: false,
isMobile: false,
isMobileOnly: false,
isTablet: false,
isBrowser: true,
isDesktop: true,
isAndroid: false,
isWinPhone: false,
isIOS: false,
isChrome: true,
isFirefox: false,
isSafari: false,
isOpera: false,
isIE: false,
osVersion: '10.15.7',
osName: 'Mac OS',
fullBrowserVersion: '107.0.0.0',
browserVersion: '107',
browserName: 'Chrome',
mobileVendor: 'none',
mobileModel: 'none',
engineName: 'Blink',
engineVersion: '107.0.0.0',
getUA: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36',
isEdge: false,
isYandex: false,
deviceType: 'browser',
isIOS13: false,
isIPad13: false,
isIPhone13: false,
isIPod13: false,
isElectron: false,
isEdgeChromium: false,
isLegacyEdge: false,
isWindows: false,
isMacOs: true,
isMIUI: false,
isSamsungBrowser: false
}
Upvotes: 3
Reputation: 664
I solved a case like this using next-useragent
.
const mobileBreakpoint = 1280;
/**
*
* @param userAgent - the UserAgent object from `next-useragent`
*/
export const useIsMobile = (userAgent?: UserAgent): boolean => {
const [isMobile, setIsMobile] = useState(false);
// Some front-end hook that gets the current breakpoint, but returns undefined, if we don't have a window object.
const { breakpoint } = useResponsive();
useEffect(() => {
if (breakpoint) {
setIsMobile(breakpoint.start < mobileBreakpoint);
}
else if (userAgent) {
setIsMobile(userAgent.isMobile);
} else if (!isBrowser) {
setIsMobile(false);
}
}, [userAgent, breakpoint]);
return isMobile;
};
And the usage of it is:
// Inside react function component.
const isMobile = useIsMobile(props.userAgent);
export const getServerSideProps = (
context: GetServerSidePropsContext,
): GetServerSidePropsResult<{ userAgent?: UserAgent }> => ({
// Add the user agent to the props, so we can use it in the window hook.
props: {
userAgent: parse(context.req.headers["user-agent"] ?? ""),
},
});
This hook always returns a boolean isMobile
. When you run it server-side, it uses the user-agent header to detect a mobile device in the SSR request. When this gets to client side, it uses the breakpoints (in my case), or any other logic for width detection to update the boolean. You could use next-useragent
to also detect the specific device type, but you can't make resolution-based rendering server-side.
Upvotes: 0
Reputation: 257
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>
Upvotes: 2
Reputation: 4301
Was able to avoid dynamic importing or component props, by using React state instead. For my use case, I was trying to detect if it was Safari, but this can work for other ones as well.
Import code
import { browserName } from 'react-device-detect';
Component code
const [isSafari, setIsSafari] = useState(false);
useEffect(() => {
setIsSafari(browserName === 'Safari');
}, [browserName]);
// Then respect the state in the render
return <div data-is-safari={isSafari} />;
Upvotes: 2
Reputation: 2234
This always works. (I used this package after trying the above technique and it didn't work for me.)
The advantage: The component renders server side so there's no flashing on client side when trying to detect user agent.
import { isMobile } from "mobile-device-detect";
just import the package and create your layout.
import { isMobile } from "mobile-device-detect";
const Desktop = () => {
return (
<>
desktop
</>
);
};
Desktop.layout = Layout;
const Mobile = () => {
return (
<>
mobile
</>
);
};
Mobile.layout = LayoutMobile;
const Page = isMobile ? Desktop : Mobile;
export default Page;
Upvotes: -1
Reputation: 13588
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.
Upvotes: 5
Reputation: 777
import React, { useState, useEffect }
import { isMobile } from 'react-device-detect'
...
const [_isMobile, setMobile] = useState();
useEffect(() => {
setMobile(isMobile);
}, [setMobile]);
<div hidden={_isMobile}> Desktop View</div>
<div hidden={!_isMobile}> MobileView </div>
Upvotes: 1
Reputation: 1016
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}
/>
)
}
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
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.
Upvotes: 51
Reputation: 900
If you don't mind rendering always desktop version and figuring the logic on the front-end, then the hook logic can be pretty straightforward.
export const useDevice = () => {
const [firstLoad, setFirstLoad] = React.useState(true);
React.useEffect(() => { setFirstLoad(false); }, []);
const ssr = firstLoad || typeof navigator === "undefined";
const isAndroid = !ssr && /android/i.test(navigator.userAgent);
const isIos = !ssr && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
return {
isAndroid,
isIos,
isDesktop: !isAndroid && !isIos
};
};
Upvotes: 0
Reputation: 1523
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.
Upvotes: 4
Reputation: 1254
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
Upvotes: 41