I'm trying to create a custom hook which will eventually be packaged up on NPM and used internally on projects in the company I work for. The basic idea is that we want the package to expose a provider, which when mounted will make a request to the server that returns an array of permission strings that are then provided to the children components through context. We also want a function can which can be called within the provider which will take a string argument and return a boolean based on whether or not that string is present in the permissions array provided by context.
I was following along with this article but any time I call can from inside the provider, the context always comes back as undefined. Below is an extremely simplified version without functionality that I've been playing with to try to figure out what's going on:
useCan/src/index.js:
import React, { createContext, useContext, useEffect } from 'react';
type CanProviderProps = {children: React.ReactNode}
type Permissions = string[]
// Dummy data for fake API call
const mockPermissions: string[] = ["create", "click", "delete"]
const CanContext = createContext<Permissions | undefined>(undefined)
export const CanProvider = ({children}: CanProviderProps) => {
let permissions: Permissions | undefined
useEffect(() => {
permissions = mockPermissions
// This log displays the expected values
console.log("Mounted. Permissions: ", permissions)
}, [])
return <CanContext.Provider value={permissions}>{children}</CanContext.Provider>
}
export const can = (slug: string): boolean => {
const context = useContext(CanContext)
// This log always shows context as undefined
console.log(context)
// No functionality built to this yet. Just logging to see what's going on.
return true
}
And then the simple React app where I'm testing it out:
useCan/example/src/App.tsx:
import React from 'react'
import { CanProvider, can } from 'use-can'
const App = () => {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
{/* Again, this log always shows undefined */}
{can("post")}
</div>
</CanProvider>
)
}
export default App
Where am I going wrong here? This is my first time really using React context so I'm not sure where to pinpoint where the problem is. Any help would be appreciated. Thanks.
There are two problems with your implementation:
In your CanProvider you're reassigning the value in permissions with =. This will not trigger an update in the Provider component. I suggest using useState instead of let and =.
const [permissions, setPermissions] = React.useState<Permissions | undefined>();
useEffect(() => {
setPermissions(mockPermissions)
}, []);
This will make the Provider properly update when permissions change.
You are calling a hook from a regular function (the can function calls useContext). This violates one of the main rules of Hooks. You can learn more about it here: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions
I suggest creating a custom hook function that gives you the can function you need.
Something like this, for example
const useCan = () => {
const context = useContext(CanContext)
return () => {
console.log(context)
return true
}
}
Then you should use your brand new hook in the root level (as per the rules of hooks) of some component that's inside your provider. For example, extracting a component for the content like so:
const Content = (): React.ReactElement => {
const can = useCan();
if(can("post")) {
return <>Yes, you can</>
}
return null;
}
export default function App() {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
<Content />
</div>
</CanProvider>
)
}
You should use state to manage permissions.
Look at the example below:
export const Provider: FC = ({ children }) => {
const [permissions, setPermissions] = useState<string[]>([]);
useEffect(() => {
// You can fetch remotely
// or do your async stuff here
retrivePermissions()
.then(setPermissions)
.catch(console.error);
}, []);
return (
<CanContext.Provider value={permissions}>{children}</CanContext.Provider>
);
};
export const useCan = () => {
const permissions = useContext(CanContext);
const can = useCallback(
(slug: string) => {
return permissions.some((p) => p === slug);
},
[permissions]
);
return { can };
};
Using useState you force the component to update the values.
You may want to read more here
I know how to do Lazy Hydration and I know how to do Code Splitting, but how can I make the splitted chunck download only when the component is hydrating?
My code looks like this
import React from 'react';
import dynamic from 'next/dynamic';
import ReactLazyHydrate from 'react-lazy-hydration';
const MyComponent = dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
export const PageComponent = () => {
return (
...
<ReactLazyHydrate whenVisible>
<MyComponent/>
</ReactLazyHydrate>
...
);
};
MyComponent is rendered below the fold, which means that it is only gonna hydrate when the user scrolls. The problem is that the JS chunck for MyComponent will be downloaded right away when the page loads.
I was able to hack it by using the dynamic import only on client but this makes the component disappear for a second when it hydrates, because the html rendered on server will not be used by react. It will recreate the DOM element and it will be empty until the JS chunck loads.
When the element disappear for a sec it increases the page CLS and that's the main reason why I can not use this hack.
Here is the code for this hack
const MyComponent = typeof window === 'undefined'
? require('components/my-component').MyComponent
: dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
Note that I want to render the component's HTML on the server render. That't why I don't want to Lazy Load it. I want to Lazy Hydrate so I can have the component's HTML rendered on server but only download
and execute it's JS when it is visible.
Update:
In document:
// stops preloading of code-split chunks
class LazyHead extends Head {
getDynamicChunks(files) {
const dynamicScripts = super.getDynamicChunks(files);
try {
// get chunk manifest from loadable
const loadableManifest = __non_webpack_require__(
'../../react-loadable-manifest.json',
);
// search and filter modules based on marker ID
const chunksToExclude = Object.values(loadableManifest).filter(
manifestModule => manifestModule?.id?.startsWith?.('lazy') || false,
);
const excludeMap = chunksToExclude?.reduce?.((acc, chunks) => {
if (chunks.files) {
acc.push(...chunks.files);
}
return acc;
}, []);
const filteredChunks = dynamicScripts?.filter?.(
script => !excludeMap?.includes(script?.key),
);
return filteredChunks;
} catch (e) {
// if it fails, return the dynamic scripts that were originally sent in
return dynamicScripts;
}
}
}
const backupScript = NextScript.getInlineScriptSource;
NextScript.getInlineScriptSource = (props) => {
// dont let next load all dynamic IDS, let webpack manage it
if (props?.__NEXT_DATA__?.dynamicIds) {
const filteredDynamicModuleIds = props?.__NEXT_DATA__?.dynamicIds?.filter?.(
moduleID => !moduleID?.startsWith?.('lazy'),
);
if (filteredDynamicModuleIds) {
// mutate dynamicIds from next data
props.__NEXT_DATA__.dynamicIds = filteredDynamicModuleIds;
}
}
return backupScript(props);
};
in next config
const mapModuleIds = fn => (compiler) => {
const { context } = compiler.options;
compiler.hooks.compilation.tap('ChangeModuleIdsPlugin', (compilation) => {
compilation.hooks.beforeModuleIds.tap('ChangeModuleIdsPlugin', (modules) => {
const { chunkGraph } = compilation;
for (const module of modules) {
if (module.libIdent) {
const origId = module.libIdent({ context });
// eslint-disable-next-line
if (!origId) continue;
const namedModuleId = fn(origId, module);
if (namedModuleId) {
chunkGraph.setModuleId(module, namedModuleId);
}
}
}
});
});
};
const withNamedLazyChunks = (nextConfig = {}) => Object.assign({}, nextConfig, {
webpack: (config, options) => {
config.plugins.push(
mapModuleIds((id, module) => {
if (
id.includes('/global-brand-statement.js')
|| id.includes('signposting/signposting.js')
|| id.includes('reviews-container/index.js')
|| id.includes('why-we-made-this/why-we-made-this.js')
) {
return `lazy-${module.debugId}`;
}
return false;
}),
);
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
},
});
In file, using next/dynamic
<LazyHydrate whenVisible style={null} className="col-xs-12">
<GlobalBrandStatement data={globalBrandData} />
</LazyHydrate>
Not sure if this is what you’re after, but I use lazy hydration mixed with webpack plugin and custom next head to preserve the html but strip out below the fold dynamic imported scripts. So I only download the JS and hydrate the component just before the user scrolls into view. Regardless of it the component was used during render - I don’t need the runtime unless a user is going to see it.
Currently in production and has reduced initial page load by 50%. No impact to SEO
Get me on twitter #scriptedAlchemy if you need the implementation, I’ve not yet written a post about it - but I can tell you it’s totally possible to achieve this “download as you scroll” design with very little effort.
import { useState } from "react";
import dynamic from "next/dynamic";
const MyComponent = dynamic(() => import("components/my-component"));
export const PageComponent = () => {
const [downloadComp, setDownloadComp] = useState(false);
return (
<>
<div className="some-class-name">
<button onClick={() => setDownloadComp(true)}>
Download the component
</button>
{downloadComp && <MyComponent />}
</div>
</>
);
};
The above code will download the once you hit the button. If you want it to download if your scroll to position in that case you can use something like react-intersection-observer to set the setDownloadComp. I don't have experience using react-lazy-hydration but I have been using react-intersection-observer and nextjs dynamic import to lazy load components that depends on user scroll.
I have made a library to make this thing simple. And you can benefit with:
Fully HTML page render (Better for SEO)
Only load JS and Hydrate when needed (Better for PageSpeed)
How to use it
import lazyHydrate from 'next-lazy-hydrate';
// Lazy hydrate when scroll into view
const WhyUs = lazyHydrate(() => import('../components/whyus'));
// Lazy hydrate when users hover into the view
const Footer = lazyHydrate(
() => import('../components/footer', { on: ['hover'] })
);
const HomePage = () => {
return (
<div>
<AboveTheFoldComponent />
{/* ----The Fold---- */}
<WhyUs />
<Footer />
</div>
);
};
Read more: https://github.com/thanhlmm/next-lazy-hydrate
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;
React Stripe Elements works fine in development but deploying live via Netlify throws 'Webpack: Window is undefined' in Provider.js react stripe elements node module file.
As per some other suggestions I have tried ComponentDidMount method and also editing the Provider.js with this:
if (typeof window !== 'undefined') {
let iInnerHeight = window.innerHeight;
}
Both still result in failed deploys.
Also, I have tried setting stripe or apiKey in StripeProvider component, setting stripe throws error requiring Stripe object, e.g. Stripe(...) --> when switched with this get Stripe is not defined and apiKey throws window undefined error.
This is my gatsby-ssr.js file:
import React from 'react'
import { ShopkitProvider } from './src/shopkit'
import { StripeProvider, Elements } from 'react-stripe-elements'
import Layout from './src/components/Layout'
export const wrapRootElement = ({ element }) => {
return (
<StripeProvider apiKey={process.env.GATSBY_STRIPE_PUBLISHABLE_KEY}>
<ShopkitProvider clientId{process.env.GATSBY_MOLTIN_CLIENT_ID}>
<Elements>{element}</Elements>
</ShopkitProvider>
</StripeProvider>
)
}
export const wrapPageElement = ({ element, props }) => {
return <Layout {...props}>{element}</Layout>
}
Everything is working as expected on development, but SSR present window undefined issue with Webpack. I have also set env variables in Netlify as well in .env file
The problem is that there's a check for Stripe object in window inside StripeProvider. This means you can't use it raw in wrapRootElement. The simple solution is to not use StripeProvider in gatsby-ssr.js, you only need it in gatsby-browser.js.
However, since you're wrapping the root with multiple service providers, and also if you're loading Stripe asynchronously like this:
// somewhere else vvvvv
<script id="stripe-js" src="https://js.stripe.com/v3/" async />
You might as well make a common wrapper that can be used in both gatsby-ssr & gatsby-browser so it's easier to maintain.
I did this by creating a wrapper for StripeProvider where Stripe is manually initiated depending on the availability of window & window.Stripe. Then the stripe instance is passed as a prop to StripeProvider instead of an api key.
// pseudo
const StripeWrapper = ({ children }) => {
let stripe,
if (no window) stripe = null
if (window.Stripe) stripe = window.Stripe(...)
else {
stripeLoadingScript.onload = () => window.Stripe(...)
}
return (
<StripeProvider stripe={stripe}>
{children}
<StripeProvider>
)
}
This logic should be put in a componentDidMount or a useEffect hook. Here's an example with hook:
import React, { useState, useEffect } from 'react'
import { StripeProvider } from 'react-stripe-elements'
const StripeWrapper = ({ children }) => {
const [ stripe, setStripe ] = useState(null)
useEffect(() => {
// for SSR
if (typeof window == 'undefined') return
// for browser
if (window.Stripe) {
setStripe(window.Stripe(process.env.STRIPE_PUBLIC_KEY))
} else {
const stripeScript = document.querySelector('#stripe-js')
stripeScript.onload = () => {
setStripe(window.Stripe(process.env.STRIPE_PUBLIC_KEY))
}
}
}, []) // <-- passing in an empty array since I only want to run this hook once
return (
<StripeProvider stripe={stripe}>
{children}
</StripeProvider>
)
}
// export a `wrapWithStripe` function that can used
// in both gatsby-ssr.js and gatsby-browser.js
const wrapWithStripe = ({ element }) => (
<StripeWrapper>
<OtherServiceProvider>
{element}
</OtherServiceProvider>
</StripeWrapper>
)
By setting async to true in gatsby-config.js
{
resolve: `gatsby-plugin-stripe`,
options: {
async: true
}
}
It is possible to simplify the code above.
const Stripe = props => {
const [stripe, setStripe] = useState(null);
useEffect(() => {
(async () => {
const obj = await window.Stripe(process.env.STRIPE_PUBLIC_KEY);
setStripe(obj);
})();
}, []);
return (
<>
<StripeProvider stripe={stripe}>
{children}
</StripeProvider>
</>
);
};
I am using the https://www.npmjs.com/package/react-swipeable-routes library to set up some swipeable views in my React app.
I have a custom context that contains a dynamic list of views that need to be rendered as children of the swipeable router, and I have added two buttons for a 'next' and 'previous' view for desktop users.
Now I am stuck on how to get the next and previous item from the array of modules.
I thought to fix it with a custom context and custom hook, but when using that I am getting stuck in an infinite loop.
My custom hook:
import { useContext } from 'react';
import { RootContext } from '../context/root-context';
const useShow = () => {
const [state, setState] = useContext(RootContext);
const setModules = (modules) => {
setState((currentState) => ({
...currentState,
modules,
}));
};
const setActiveModule = (currentModule) => {
// here is the magic. we get the currentModule, so we know which module is visible on the screen
// with this info, we can determine what the previous and next modules are
const index = state.modules.findIndex((module) => module.id === currentModule.id);
// if we are on first item, then there is no previous
let previous = index - 1;
if (previous < 0) {
previous = 0;
}
// if we are on last item, then there is no next
let next = index + 1;
if (next > state.modules.length - 1) {
next = state.modules.length - 1;
}
// update the state. this will trigger every component listening to the previous and next values
setState((currentState) => ({
...currentState,
previous: state.modules[previous].id,
next: state.modules[next].id,
}));
};
return {
modules: state.modules,
setActiveModule,
setModules,
previous: state.previous,
next: state.next,
};
};
export default useShow;
My custom context:
import React, { useState } from 'react';
export const RootContext = React.createContext([{}, () => {}]);
export default (props) => {
const [state, setState] = useState({});
return (
<RootContext.Provider value={[state, setState]}>
{props.children}
</RootContext.Provider>
);
};
and here the part where it goes wrong, in my Content.js
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import SwipeableRoutes from 'react-swipeable-routes';
import useShow from '../../hooks/useShow';
import NavButton from '../NavButton';
// for this demo we just have one single module component
// when we have real data, there will be a VoteModule and CommentModule at least
// there are 2 important object given to the props; module and match
// module comes from us, match comes from swipeable views library
const ModuleComponent = ({ module, match }) => {
// we need this function from the custom hook
const { setActiveModule } = useShow();
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
return (
<div style={{ height: 300, backgroundColor: module.title }}>{module.title}</div>
);
};
const Content = () => {
const { modules, previousModule, nextModule } = useShow();
// this is a safety measure, to make sure we don't start rendering stuff when there are no modules yet
if (!modules) {
return <div>Loading...</div>;
}
// this determines which component needs to be rendered for each module
// when we have real data we will switch on module.type or something similar
const getComponentForModule = (module) => {
// this is needed to get both the module and match objects inside the component
// the module object is provided by us and the match object comes from swipeable routes
const ModuleComponentWithProps = (props) => (
<ModuleComponent module={module} {...props} />
);
return ModuleComponentWithProps;
};
// this renders all the modules
// because we return early if there are no modules, we can be sure that here the modules array is always existing
const renderModules = () => (
modules.map((module) => (
<Route
path={`/${module.id}`}
key={module.id}
component={getComponentForModule(module)}
defaultParams={module}
/>
))
);
return (
<div className="content">
<div>
<SwipeableRoutes>
{renderModules()}
</SwipeableRoutes>
<NavButton type="previous" to={previousModule} />
<NavButton type="next" to={nextModule} />
</div>
</div>
);
};
export default Content;
For sake of completion, also my NavButton.js :
import React from 'react';
import { NavLink } from 'react-router-dom';
const NavButton = ({ type, to }) => {
const iconClassName = ['fa'];
if (type === 'next') {
iconClassName.push('fa-arrow-right');
} else {
iconClassName.push('fa-arrow-left');
}
return (
<div className="">
<NavLink className="nav-link-button" to={`/${to}`}>
<i className={iconClassName.join(' ')} />
</NavLink>
</div>
);
};
export default NavButton;
In Content.js there is this part:
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
which is causing the infinite loop. If I comment out the setActiveModule call, then the infinite loop is gone, but of course then I also won't have the desired outcome.
I am sure I am doing something wrong in either the usage of useEffect and/or the custom hook I have created, but I just can't figure out what it is.
Any help is much appreciated
I think it's the problem with the way you are using the component in the Route.
Try using:
<Route
path={`/${module.id}`}
key={module.id}
component={() => getComponentForModule(module)}
defaultParams={module}
/>
EDIT:
I have a feeling that it's because of your HOC.
Can you try
component={ModuleComponent}
defaultParams={module}
And get the module from the match object.
const ModuleComponent = ({ match }) => {
const {type, module} = match;
const { setActiveModule } = useShow();
useEffect(() => {
if (type === 'full') {
setActiveModule(module);
}
},[module, setActiveModule]);
match is an object and evaluated in the useEffect will always cause the code to be executed. Track match.type instead. Also you need to track the module there. If that's an object, you'll need to wrap it in a deep compare hook: https://github.com/kentcdodds/use-deep-compare-effect
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match.type, module]);