Gatsby w/ Chakra UI ColorMode not working - reactjs

I'm using Gatsby w/ Chakra UI and have an issue with either local storage or how the ColorMode is being accessed.
Here's my repo: https://github.com/RyanPinPB/pdm-gatsby
Live site: https://pearsondigitalmarketing.com
ColorMode and components are styled correctly on localhost, but in production the site has an issue when it renders the header after local storage has saved darkMode=true.
You can reproduce this issue by going to the live site, toggling dark mode, and refreshing. Or, if your browser has theme settings or depending on your OS (or time of day), it will show the issue at night, or if your browser/OS prefers dark mode.
What is weird, is that certain components receive the correct darkMode styling (colorings and background), but my logo, menu, and header background are not correct. Even tho all 3 of these are using colorMode styling conditionals as follows:
const bgColor = {
light: "rgba(255,255,255,.6)",
dark: "rgba(26, 32, 44, .6)",
}
const color = { light: "brand.400", dark: "brand.900" }
bg={bgColor[colorMode]}
color={color[colorMode]}
I added a console log to both index and header files to see if one of them is rendering before the colorMode is triggered/called from local storage, but both console logs print the correct colorMode state. I'm having a hard time figuring out why my header in darkmode has the wrong background color and color styling.
The issue is correct after any click of the "toggle button" in the header. It's just on the initial rendering of the page, if the page thinks it needs to be in DarkMode.
Any help is greatly appreciated.
ThemeContext.js:
import React from "react"
import { ColorModeProvider } from "#chakra-ui/core"
//dont have to use this file if we use Chakra UI
const defaultState = {
dark: false,
toggleDark: () => {},
}
const ThemeContext = React.createContext(defaultState)
// Getting dark mode information from OS!
// You need macOS Mojave + Safari Technology Preview Release 68 to test this currently.
const supportsDarkMode = () =>
window.matchMedia("(prefers-color-scheme: dark)").matches === true
class ThemeProvider extends React.Component {
state = {
dark: false,
}
toggleDark = () => {
console.log("ThemeContext.js: toggle dark/light mode")
let dark = !this.state.dark
localStorage.setItem("dark", JSON.stringify(dark))
this.setState({ dark })
}
componentDidMount() {
// Getting dark mode value from localStorage!
console.log("ThemeContext.js component did mount, dark: " + this.state.dark)
const lsDark = JSON.parse(localStorage.getItem("dark"))
if (lsDark) {
console.log("ThemeContext.js: lsDark: " + lsDark)
this.setState({ dark: lsDark })
} else if (supportsDarkMode()) {
console.log("ThemeContext.js: supports Dark Mode: true")
this.setState({ dark: true })
}
}
render() {
const { children } = this.props
const { dark } = this.state
return (
<ThemeContext.Provider
value={{
dark,
toggleDark: this.toggleDark,
}}
>
<ColorModeProvider>{children}</ColorModeProvider>
</ThemeContext.Provider>
)
}
}
export default ThemeContext
export { ThemeProvider }

Related

Warning: Prop `className` did not match. Switching from light to dark mode using cookies (Nextjs, Mui5)

When the browser loads if the cookie (darkMode) is set to OFF the light mode is on but when The cookie (darkMode) is set to ON which means the dark is on the browser does load the dark mode but only for the navbar and seems that it stops the rendering process and the warning happens I am using it with material ui switch.
import Cookies from 'js-cookie';
import { createSlice } from '#reduxjs/toolkit'
const initialState = {
darkMode: Cookies.get('darkMode') === 'ON' ? true : false,
}
export const DarkModeSlice = createSlice({
name: 'darkMode',
initialState,
reducers: {
toggleDarkMode: (state) => {
let newDarkMode = !state.darkMode;
Cookies.set("darkMode", newDarkMode ? "ON" : "OFF");
state.darkMode = newDarkMode
}
},
})
export const { toggleDarkMode } = DarkModeSlice.actions
export default DarkModeSlice.reducer
This video explains how to switch from dark to light using next-themes form npm watch it if there are some errors and there will be, come back and continue reading my instructions
Dark Mode Persistence with Next.js is Harder than with plain React (MUI example)
INSTRUCTIONS
Please Do not forget to remove the <CssBaseline /> under the theme provider or PageProvider if you are gonna follow the video because next-themes that you will download form npm is gonna do every thing for you. also add key attribute if you are using switch component. Follow The code below.
import { useTheme } from "next-themes";
const { theme, resolvedTheme, setTheme } = useTheme();
const [isDark, setIsDark] = useState();
useEffect(() => {
resolvedTheme === "light" ? setIsDark(false) : setIsDark(true);
return () => {};
}, [resolvedTheme]);
<Switch
key={isDark}
checked={isDark}
onChange={() =>
setTheme(resolvedTheme === "light" ? "dark" : "light")
}
/>
Copy and Paste it, it is gonna work in the component you wanna use the switch in, If any one still could not solve it please feel free to contact me.

nextjs react recoil persist values in local storage: initial page load in wrong state

I have the following code,
const Layout: React.FC<LayoutProps> = ({ children }) => {
const darkMode = useRecoilValue(darkModeAtom)
console.log('darkMode: ', darkMode)
return (
<div className={`max-w-6xl mx-auto my-2 ${darkMode ? 'dark' : ''}`}>
<Nav />
{children}
<style jsx global>{`
body {
background-color: ${darkMode ? '#12232e' : '#eefbfb'};
}
`}</style>
</div>
)
}
I am using recoil with recoil-persist.
So, when the darkMode value is true, the className should include a dark class, right? but it doesn't. I don't know what's wrong here. But it just doesn't work when I refresh for the first time, after that it works fine. I also tried with darkMode === true condition and it still doesn't work. You see the styled jsx, that works fine. That changes with the darkMode value and when I refresh it persists the data. But when I inspect I don't see the dark class in the first div. Also, when I console.log the darkMode value, I see true, but the dark class is not included.
Here's the sandbox link
Maybe it's a silly mistake, But I wasted a lot of time on this. So what am I doing wrong here?
The problem is that during SSR (server side rendering) there is no localStorage/Storage object available. So the resulted html coming from the server always has darkMode set to false. That's why you can see in cosole mismatched markup errors on hydration step.
I'd assume using some state that will always be false on the initial render (during hydration step) to match SSR'ed html but later will use actual darkMode value. Something like:
// themeStates.ts
import * as React from "react";
import { atom, useRecoilState } from "recoil";
import { recoilPersist } from "recoil-persist";
const { persistAtom } = recoilPersist();
export const darkModeAtom = atom<boolean>({
key: "darkMode",
default: false,
effects_UNSTABLE: [persistAtom]
});
export function useDarkMode() {
const [isInitial, setIsInitial] = React.useState(true);
const [darkModeStored, setDarkModeStored] = useRecoilState(darkModeAtom);
React.useEffect(() => {
setIsInitial(false);
}, []);
return [
isInitial === true ? false : darkModeStored,
setDarkModeStored
] as const;
}
And inside components use it like that:
// Layout.tsx
const [darkMode] = useDarkMode();
// Nav.tsx
const [darkMode, setDarkMode] = useDarkMode();
codesandbox link
Extending on #aleksxor solution, you can perform the useEffect once as follows.
First create an atom to handle the SSR completed state and a convenience function to set it.
import { atom, useSetRecoilState } from "recoil"
const ssrCompletedState = atom({
key: "SsrCompleted",
default: false,
})
export const useSsrComplectedState = () => {
const setSsrCompleted = useSetRecoilState(ssrCompletedState)
return () => setSsrCompleted(true)
}
Then in your code add the hook. Make sure it's an inner component to the Recoil provider.
const setSsrCompleted = useSsrComplectedState()
useEffect(setSsrCompleted, [setSsrCompleted])
Now create an atom effect to replace the recoil-persist persistAtom.
import { AtomEffect } from "recoil"
import { recoilPersist } from "recoil-persist"
const { persistAtom } = recoilPersist()
export const persistAtomEffect = <T>(param: Parameters<AtomEffect<T>>[0]) => {
param.getPromise(ssrCompletedState).then(() => persistAtom(param))
}
Now use this new function in your atom.
export const darkModeAtom = atom({
key: "darkMode",
default: false,
effects_UNSTABLE: [persistAtomEffect]
})

React Hook does not work properly on the first render in gatsby production mode

I have the following Problem:
I have a gatsby website that uses emotion for css in js. I use emotion theming to implement a dark mode. The dark mode works as expected when I run gatsby develop, but does not work if I run it with gatsby build && gatsby serve. More specifically the dark mode works only after switching to light and back again.
I have to following top level component which handles the Theme:
const Layout = ({ children }) => {
const [isDark, setIsDark] = useState(() => getInitialIsDark())
useEffect(() => {
if (typeof window !== "undefined") {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark])
return (
<ThemeProvider theme={isDark ? themeDark : themeLight}>
<ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>
</ThemeProvider>
)
}
The getInitalIsDark function checks a localStorage value, the OS color scheme, and defaults to false. If I run the application, and activate the dark mode the localStorage value is set. If i do now reload the Application the getInitialIsDark method returns true, but the UI Renders the light Theme. Switching back and forth between light and dark works as expected, just the initial load does not work.
If I replace the getInitialIsDark with true loading the darkMode works as expected, but the lightMode is broken. The only way I got this to work is to automatically rerender after loading on time using the following code.
const Layout = ({ children }) => {
const [isDark, setIsDark] = useState(false)
const [isReady, setIsReady] = useState(false)
useEffect(() => {
if (typeof window !== "undefined" && isReady) {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark, isReady])
useEffect(() => setIsReady(true), [])
useEffect(() => {
const useDark = getInitialIsDark()
console.log("init is dark " + useDark)
setIsDark(useDark)
}, [])
return (
<ThemeProvider theme={isDark ? themeDark : themeLight}>
{isReady ? (<ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>) : <div/>}
</ThemeProvider>
)
}
But this causes an ugly flicker on page load.
What am I doing wrong with the hook in the first approach, that the initial value is not working as I expect.
Did you try to set your initial state like this?
const [isDark, setIsDark] = useState(getInitialIsDark())
Notice that I am not wrapping getInitialIsDark() in an additional function:
useState(() => getInitialIsDark())
You will probably crash your build because localStorage is not defined at buildtime. You might need to check if that exists inside getInitialIsDark.
Hope this helps!
#PedroFilipe is correct, useState(() => getInitialIsDark()) is not the way to invoke the checking function on start-up. The expression () => getInitialIsDark() is truthy, so depending on how <ThemedLayout isDark={isDark}> uses the prop it might work by accident, but useState will not evaluate the fuction passed in (as far as I know).
When using an initial value const [myValue, setMyValue] = useState(someInitialValue) the value seen in myValue can be laggy. I'm not sure why, but it seems to be a common cause of problems with hooks.
If the component always renders multiple times (e.g something else is async) the problem does not appear because in the second render the variable will have the expected value.
To be sure you check localstorage on startup, you need an additional useEffect() which explicitly calls your function.
useEffect(() => {
setIsDark(getInitialIsDark());
}, [getInitialIsDark]); //dependency only needed to satisfy linter, essentially runs on mount.
Although most useEffect examples use an anonymous function, you might find more understandable to use named functions (following the clean-code principle of using function names for documentation)
useEffect(function checkOnMount() {
setIsDark(getInitialIsDark());
}, [getInitialIsDark]);
useEffect(function persistOnChange() {
if (typeof window !== "undefined" && isReady) {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark])
I had a similar issue where some styles weren't taking effect because they were being applied to through classes which were set on mount (like you only on production build, everything worked fine in develop).
I ended up switching the hydrate function React was using from ReactDOM.hydrate to ReactDOM.render and the issue disappeared.
// gatsby-browser.js
export const replaceHydrateFunction = () => (element, container, callback) => {
ReactDOM.render(element, container, callback);
};
This is what worked for me, try this and let me know if it works out.
First
In src/components/ i've created a component navigation.js
export default class Navigation extends Component {
static contextType = ThemeContext // eslint-disable-line
render() {
const theme = this.context
return (
<nav className={'nav scroll' : 'nav'}>
<div className="nav-container">
<button
className="dark-switcher"
onClick={theme.toggleDark}
title="Toggle Dark Mode"
>
</button>
</div>
</nav>
)
}
}
Second
Created a gatsby-browser.js
import React from 'react'
import { ThemeProvider } from './src/context/ThemeContext'
export const wrapRootElement = ({ element }) => <ThemeProvider>{element}</ThemeProvider>
Third
I've created a ThemeContext.js file in src/context/
import React, { Component } from 'react'
const defaultState = {
dark: false,
notFound: false,
toggleDark: () => {},
}
const ThemeContext = React.createContext(defaultState)
class ThemeProvider extends Component {
state = {
dark: false,
notFound: false,
}
componentDidMount() {
const lsDark = JSON.parse(localStorage.getItem('dark'))
if (lsDark) {
this.setState({ dark: lsDark })
}
}
componentDidUpdate(prevState) {
const { dark } = this.state
if (prevState.dark !== dark) {
localStorage.setItem('dark', JSON.stringify(dark))
}
}
toggleDark = () => {
this.setState(prevState => ({ dark: !prevState.dark }))
}
setNotFound = () => {
this.setState({ notFound: true })
}
setFound = () => {
this.setState({ notFound: false })
}
render() {
const { children } = this.props
const { dark, notFound } = this.state
return (
<ThemeContext.Provider
value={{
dark,
notFound,
setFound: this.setFound,
setNotFound: this.setNotFound,
toggleDark: this.toggleDark,
}}
>
{children}
</ThemeContext.Provider>
)
}
}
export default ThemeContext
export { ThemeProvider }
This should work for you here is the reference I followed from the official Gatsby site

How do I resolve React flash error using dark mode?

I am using a theme toggler function for dark mode. This function lives inside my src/components/Toggle/Toggle.jsx file, which renders a toggle button that changes the theme. When I reload my page, I get a white flash. I think this is happening because my scripts are not rendering before my HTML doc and it appears the solution is to add a script to my <head>....but I am unsure how to move my Toggle.jsx function out of that component into my <head>...any ideas?
Below is my dark mode function inside my Toggle.jsx...
import "./Toggle.css";
import Icon from "../Icon/Icon";
function Toggle() {
//optionally parse from localStorage
const [theme, setTheme] = React.useState(getInitialMode());
React.useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", JSON.stringify(theme));
}, [theme]);
function getInitialMode() {
const isReturningUser = "theme" in localStorage;
const savedMode = JSON.parse(localStorage.getItem("theme"));
const userPrefersDark = getPrefColorScheme();
// if mode was saved -> dark / light
if (isReturningUser) {
return savedMode;
}
// if preferred color scheme is dark -> dark
else if (userPrefersDark) {
return "dark";
} else {
// otherwise -> light
return "light";
}
}
function getPrefColorScheme() {
if (!window.matchMedia) return;
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
return (
<button
className="toggle-button"
onClick={() => {
setTheme(theme === "light" ? "dark" : "light");
}}
>
<Icon />
</button>
);
}
export default Toggle;
I guess easy option would be to set dark mode as default so whenever you refresh the page before the selected theme is loaded your dark theme will load and prevent your screen from flashing

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?
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.
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.
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>
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
}
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} />;
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
};
};
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>
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.
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
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;

Resources