What is proper way to detect device in Next.js SSR? - reactjs

I have <MobileLayout />, <DesktopLayout />. I'm using Next.js for Server Side Rendering.
And I noticed there are many famous ui library has mobile detection components like <Respnosive /> component in Semantic-UI-React. But all of this is client side method, not working properly on SSR
I read some documents the conclusion is I should check user-agent of server side req.headers. In Next.js, What is proper way to detect device and conditonally render one of MobileLayout / DesktopLayout?
What I tried
in _app.js
import isMobile from 'ismobilejs'
...
function Homepage({ Component, pageProps, mobile }){
return (
mobile ?
<MobileLayout><Component {...pageProps} /></MobileLayout> :
<DesktopLayout><Component {...pageProps} /></DesktopLayout>
)
}
HomePage.getInitialProps = async (appContext) => {
const userAgent = appContext.ctx.req.headers['user-agent']
const mobile = isMobile(userAgent).any
const appProps = await App.getInitialProps(appContext)
return { ...appProps, mobile }
}
But the problem is getIntialProps on _app.js executed every page load. with moving page with client, the appContext.ctx is undefined so it will omit error. and I think this method might block some nextjs builtin optimizations.
Error in error page getInitialProps: TypeError: Cannot read
property 'headers' of undefined
So what is propery way to check device in Next.js?

If you want to detect the user's device using userAgent, your best bet is this answer:
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
}
}
(Note you should actually be using getServerSideProps or getStaticProps when possible, if you have Next 9.3 or newer, but sometimes there is no replacement for the getInitialProps functionality.)
However, the folks at Mozilla advise:
It's worth re-iterating: it's very rarely a good idea to use user
agent sniffing. You can almost always find a better, more broadly
compatible way to solve your problem!
The maker of the isMobile package you're importing even warns:
You might not need this library. In most cases, responsive design
solves the problem of controlling how to render things across
different screen sizes.
So, see if you can use CSS3 media queries to conditionally render certain elements or change their size, etc., rather than having completely separate mobile and desktop layout components. But it's possible you have an edge case where you can't make any alternative option work.
If you are going to keep your current setup and use your two layouts on other pages, you might consider combining them into a parent <Layout> component that conditionally renders one or the other so you don't have to copy that logic into every page:
export const Layout = (props) => {
return (
props.mobile ?
<MobileLayout>{props.children}</MobileLayout> :
<DesktopLayout>{props.children}</DesktopLayout>
)
}

you can use "next-useragent"
Give access to user-agent details anywhere using withUserAgent method.
next-useragent npm package

Related

React Next.js - Three related questions about SSR patterns

the following question about the usage of next.js and react to achieve SSR build on top of each other, so I thought I'd write that into a single post. My main question is the third one, but I feel I need to first understand the first two questions in order to get there. So here we go:
1. Am I right that the whole page is always reexecuted from scratch after the client has received it?
Consider this next.js page component:
const Page = () => {
const [state, setState] = useState(getState());
function getState() {
console.log("compute initial state");
return 1;
}
return <>{state}</>;
};
As far as I can tell, getState() is executed both on the server and on the client. If I'd want to exectue that computation only on the server I'd have to do it via getInitialProps()resp. getServersideProps(), right?
2. What value does the prerendered document have for the client if it will be immediately thrown away?
Taking the first question a step further, why is the prerendered document event handed to the client if it will be recalculated from scratch anyway. What advantages does it have for the client to get that initial document? Ok if the case the client can't execute js at all they at least have something. But is that all?
3. Does this mean I unnecessarily have to "double render" some components on the client side?
Let's say parts of my code depend on window and can't be executed on the server. As explained in different articles I found, it can lead to problems (and react warnings) if I rely on checks like typeof window === "undefined" in my code. Instead I think the better way is to execute those functionalities after the first render with a useEffect:
const Page = () => {
const [value, setValue] = useState();
// Effect will be executed after the first render, e.g. never on the server
useEffect(() => {
const value = window.innerWidth; // Some computations or subscriptions that depend on window
setValue(value)
}, []);
return (
<>
{!value && <h1>value pending ...</h1>}
{value && <h1>{value}</h1>}
</>
);
};
Now, with this pattern the app is fine from an SSR perspective. BUT I am introducing additional effort that has do be done on the client side: Even though window is defined on first render, it can only be used on second render.
For this smaller example this doesn't play a big role, but in a larger app, a flickering may be visible because an unnecessary first render is updated some milliseconds later. Furthermore the code becomes harder to read and more error-prone.
The solution I would want here is related to the first question above: Can I somehow avoid the app from starting from scratch but directly start with the second render? Is there some kind of pattern for this that I've missed so far?
Note: Of course I could rewrite the component to simply check if window is defined:
return (
<>
<h1>{window ? window.innerWidth : "value pending ..."}</h1>
</>
);
However this will cause a react warning and cause other problems as described in the article i have liked above. Also see the react docs:
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them.
Thank you a lot for your help!
Am I right that the whole page is always reexecuted from scratch after the client has received it?
Yes and no. The initial html is built on the server and sent to the client as html, and then react is hydrated so that your page becomes interactive. As an example, consider this "page":
export default function MyPage() {
const [greeting, setGreeting] = React.useState("Hello")
React.useEffect(() => {
setGreeting("Goodbye")
}, [])
return (<p>{greeting}</p>)
}
When the page is rendered on the server, it will render as html and send that html to the client. So if you inspect the source code of the page, you'll see:
<p>Hello</p>
Then, on the client, react is hydrated and useEffect runs, so in the browser you'd see a paragraph with the word "Goodbye".
This is true whether you're using SSR (the html is created on demand on the server) or SSG (the html is created at build time into a static html page).
What value does the prerendered document have for the client if it will be immediately thrown away?
The whole document is not thrown away if you see my first point. The value in SSR is if you want the initial greeting text to be something other than "Hello". For example, if you wanted your server to parse an auth token, get the user profile, and seed greeting with "Hello Jim" on page load, you'd prefer SSR. If you don't have any serverside processing prior to sending the html to the client, you could choose SSG.
Consider these two "pages":
// Using SSR
export default function MyPage({customerName}) {
return (<p>{customerName}</p>)
}
// Using SSG
export default function MyPage({customerName}) {
const [greeting, setGreeting] = React.useState("Hello")
React.useEffect(() => {
// Call server to get the customer's name
const name = myApi.get('/name')
setGreeting(`Hello ${name}`)
}, [])
return (<p>{greeting}</p>)
}
In the first example, the server renders the p tag with the customer name (coming from some process on the server) so the html source code will include that customer's name. Nothing is thrown out here.
In the second example, the site is built as html and that source code's p tag says "Hello". When the page is visited, useEffect runs and the p tag is updated when your api responds. So the user will see "Hello" for x microseconds and then it will switch to "Hello Jim".
Does this mean I unnecessarily have to "double render" some components on the client side?
No - you control what you render on the server versus on the client. If you seed a component with data on the server and don't change it on the client, it won't rerender.
In your example, yes - you're double rendering. But you may not need to. As you noted, window doesn't exist on the client. If you absolutely must show your "value pending..." line to your user before you get the window size, then you'll do double rendering - once to populate that string on the server and then once to replace it when react hydrates on the client.
If you don't need to show that pending line and just need to show the value on the client when window actually exists, you could rewrite it like this:
export default function Page() {
const [value, setValue] = React.useState();
React.useEffect(() => {
const newValue = window.innerWidth; // Some computations or subscriptions that depend on window
setValue(newValue)
}, []);
if(value) {
return <h1>{value}</h1>
}
return null
};
In this case, nothing is rendered on the server because there is no value on the server. Only when the client is hydrated will useEffect run, update value, and render your component one time.

React / Context API / TypeScript : How to init web app and avoid UI flickering at statup?

When a user navigate to my site, I need to initialize the React web app as follow:
If the incoming user has never requested the web app, I want to set the UI language to browser default (navigator.language).
If the incoming user has already visited the site and chosen a prefered language (lang stored in the localStorage), I want to init the UI with this language.
If the incoming user has an account and is already connected (token available in localStorage), I want to auto-connect him and render the app accordingly : login button transformed into a welcome message, UI language set to user preference.
To do so, I'm using React Context API and a defaultUser object.
defaultUser: init a default user
const defaultUser = {
language: 'en_EN',
isConnected: false
}
Context: create a default context
export const AppContext = createContext({
connectedUser: defaultUser,
})
Provider: create the provider with default context
export function AppProvider({ children }: any) {
[...]
const provider = {
connectedUser
}
return (
<AppContext.Provider value={provider}>
{children}
</AppContext.Provider>
)
}
App: init the provider during app start up
export class App extends Component {
static contextType = AppContext
render() {
return (
<AppProvider>
<AppContainer />
</AppProvider>
)
}
}
AppContainer: render the app
export class AppContainer extends Component {
static contextType = AppContext
componentDidMount() {
/** If user is not connected, verify if a stored session exists and use it to connect user */
if (!this.context.connectedUser.isConnected) {
[...do things...]
}
}
The whole mecanism works well except an annoying thing : the web app is systematically initialized with default user values, until the AppContainer::componentDidMount() do the real init job.
This is causing a sort of flickering effect.
I'm struggeling for 2 days on how to fix that, trying to perform Context init before <AppContainer /> rendering, and I'm stuck.
Any recommandations?
EDIT :
For clarity, I'm adding a diagram. Currently :
React App is rendered at start.
Context is initialized at start with default value.
Context is updated when end is reached.
React App is rendered again when end.
Any layout change during these two steps (UI language, UI modification based on user permissions) are clearly visible to the user and generate a sort of flickering.
I found sort of a solution by simply conditionning <AppContainer/> loading, postponing it to the end of the sequence. However instead of having flickering I have now a lag and other unwanted side effects.
The goal would be to differ all the sequence before React Act is rendered, and after Window is available. Then dynamically create the Context, then render all.
I think the point would be resolved if I could dynamically create the AppContext and pass a variable to createContext() during App constructor() or maybe componentWillMount() (not sure when Window is available), but then TypeScript get into play with types issues and I'm still stuck.
You didn't share the code that initializes the context, but I suspect you put the default value to be either a hardcoded value, or navigator.language and therefore experience the flickering. I'd like to suggest two ways to solve this:
Solution 1
Perhaps instead of having a hardcoded default context you could generate the default context programmatically by accessing localStorage.get('lang') or similar? There is a slight drawback to this solution though: You will be mixing concerns of react and the browser, but I think in this case it's an alternative to consider, because it's very simple and obvious to the reader.
Solution 2
Alternatively, when calling ReactDOM.render you could pass down whatever you need from localStorage as a prop to your application and so you keep the browser related logic separate from the pure React stuff.
I hope this makes sense.
Here's my follow-up after Amit suggestions, in case it can help anyone else.
Init Context with functions
Instead of initializing defaultUser with hard-coded values and update it later, I set directly it with a function returning navigator.lang as suggested. This solved the flickering issue on UI labels.
Init data before RectDOM.render
However I still had flickering on UI components for which I have to get the appropriate state from an API call.
Eg, if the incoming user has a valid session token stored in localStorage, the Login button must be disabled. Before doing so, I need to make sure the session token is valid by an async call to the API. I didn't find a way to have it Β«awaitedΒ» by the Context init which seems to be synchronous.
That's where Amit second suggestion get into play. Instead of struggling finding a solution inside React, I did necessary processing before ReactDOM.render, then passing stuffs as props to <Apps/>.
This works pretty well to get and pass the data...
Except that Context API didn't setSate anymore as soon as any of its data was refering to an object from outside the Context. In other word using function calls is ok to init (probably by val), but reference to external objects breaks setState.
Conclusion
As my project is still in early stage, this gave me the chance to get rid of Context API, do the proper init as required, and code the props/states progagation with basic React.

wrong class names on pages with different content rendered on client than on server

I have been switching my React + material-ui SPA to a Next.js statically rendered site (with next export). I have followed the steps shown on the material-ui example with next.js and everything works fine on non-mobile screen widths (> 960), but the content is shown unstyled in the initial render if the screen width on initial render is at or below the mobile breakpoint. Subsequently navigating to any page on the client renders pages correctly, even when navigating back to the original offending page which was broken on initial render, again this is only on mobile screen widths.
In my code there is a lot of this:
...
const windowWidth = useWindowWidth();
const isMobile = windowWidth < 960;
return (
// markup
{ isMobile ? (...) : (...) }
// more markup
);
...
Where useWindowWidth.js does this:
function useWindowWidth() {
const isClient = typeof window === "object";
const [width, setWidth] = useState(isClient ? window.innerWidth : 1000); // this will be different between server and client
useEffect(() => {
setWidth(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return width;
}
Any page that has this will show this warning in the console when the initial render is done within the bounds of a mobile screen width:
Warning: Expected server HTML to contain a matching <div> in <div> // or something similar depending on what was conditionally rendered with isMobile
Only these pages have this css styling issue. It seems that when rendering these pages within that screenwidth when there is conditional rendering creates styles with a different name, so instead of the makeStyles-button-96 class the element calls for it will only have makeStyles-button-97 therefore leaving the element unstyled.
I have been through the material-ui issues and the docs, and made sure my project reasonably mirrors the examples. Including the _document.js and _app.js files. How do I remedy this?
PS:
There was something I recall reading on my search which stated that React expects server and client rendered output to match but if there is no way around it there is some way to signify this in the code. I am not sure if this only silences the warning or if it prevents the class renaming altogether. Can someone please shed some light on this? I can't seem to find where I read that...
Problem Identified:
To be clear, the window width difference between the server and client, is the offender here. In the useWindowWidth hook shown above, setting the default to below the 960 mobile threshold, like this:
const isClient = typeof window === "object";
const [width, setWidth] = useState(isClient ? window.innerWidth : 900); // change the default to 900 if not on client, so below the mobile threshold
Makes the inverse of my problem happen. So initial load on a mobile screenwidth is fine but a larger screen width breaks the css with mismatched class names. Is there a recommended method to conditionally render depending on screen width that would somehow keep the output the same?
UPDATE:
While I have found a fix, as stated in my own answer below, I am not satisfied with it and would like to better understand what is happening here so I can address this at build time as opposed to this solution which patches the issue as opposed to preventing it. At this point any answer which just points me in the right direction will be accepted.
From the ReactDOM.hydrate documentation:
React expects that the rendered content is identical between the server and the client.
But by leveraging window.innerWidth during your initial render in the following:
const [width, setWidth] = useState(isClient ? window.innerWidth : 1000);
you are causing the initial client rendering to be different than the server whenever the width causes different rendering than what is caused by a width of 1000 (e.g. such as when it is less than 960 in your code example that branches on isMobile). This can cause various hydration issues depending on what kind of differences your width-based branching causes.
I think you should be able to fix this by just simplifying the useState initialization to hard-code 1000:
function useWindowWidth() {
const isClient = typeof window === "object";
const [width, setWidth] = useState(1000);
useEffect(() => {
setWidth(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return width;
}
The effect is already calling setWidth(window.innerWidth) unconditionally which should take care of updating the layout after initial rendering if needed (e.g. for mobile).
If you aren't ever using the specific width and are only using it as a threshold for branching, I would recommend using Material-UI's useMediaQuery instead of your custom useWindowWidth hook. The documentation then explains ways of dealing with server-side rendering reliably. In your case (using next export), you could use a simpler server-side ssrMatchMedia implementation that always assumes 1024px rather than including user-agent parsing to try to detect device type.
In addition to taking care of SSR issues, useMediaQuery will trigger less re-rendering on width changes since it will only trigger a render when the window size crosses the threshold of whether or not the media query specified matches.
Related answer: How to implement SSR for Material UI's media queries in NextJs?
You can use next/dynamic with { ssr: false } as described here. Basically, isolate the relevant code for this issue into its own component, and then dynamically import it with ssr turned off. This avoids loading the specific code that requires window server-side.
You can also use a custom loading component while the component is dynamically fetched as described here.
or even just provide an explanation on Next's build time mechanics where styling is addressed, that would be greatly appreciated.
The problem basically boils down to there not being a window object present during ssr is my basic understanding. I had a similar issue to you with a Bootstrap carousel I was working with, and I think the dynamic import is what I'll be going with - this solution allows us to not modify our code at all for the sake of ssr, aside from simply isolating the relevant code.
For anyone with a similar problem, here is the fix I found. I have to say though, it feels like a hack instead of addressing the root problem.
Material UI: Styles flicker in and disappear
Basically, the poster (#R R) is fixing the problem after the fact by forcing a refresh/re-render on the client with an on mount effect by changing the key prop in an element in his _app.js file.
While this does fix the styling I would think a cleaner solution would address the problem at build time. If anyone has any idea how to address this at build time or can at least shed some light on where to look for the issue, or even just provide an explanation on Next's build time mechanics where styling is addressed, that would be greatly appreciated.
My guess is that the difference in what is rendered between mobile and other screen widths through the conditional rendering outlined in the question causes some kind of branching of class names. At least that's what the warning logged in the console would lead me to believe. I still can't find that article I had mentioned in my question discussing that warning and a way to address it (whether just silencing the warning, or more importantly, preventing the mismatched class names altogether). If anyone has a link to that article/blog/site, I would greatly appreciate it.
you can try useMediaQuery hook from material ui, this will give the width of the window and do the update if its change. If you need custom breakpoints also you can update in theme
import withWidth from '#material-ui/core/withWidth';
function MyComponent({width}) {
const isMobile = (width === 'xs');
return (
// markup
{ isMobile ? (...) : (...) }
// more markup
);
export default withWidth()(MyComponent);
For custom breakpoint you can try like this
const theme = createMuiTheme({
breakpoints: {
values: {
mobile: 540,
tablet: 768,
desktop: 1024,
},
},
})

Next.js: Reduce data fetching and share data between pages

I'm looking for solutions for better data fetching in a Next.js app. In this question I'm not just looking for a solution, I'm looking for multiple options so we can look at the pros and cons.
The problem I have
Right now I have a few pages that all include a component that displays som static content and a that have some dynamic content that is fetched from an API. Each page do a fetch() in their getInitialProps() to get their own page data, but also the footer data, which is the same for all pages.
This of course works, but there is a lot of duplicated data fetching. The footer data will always be displayed for all pages and always be the same. It will also rarely be changed in the API, so no need for revalidate the data.
The answers I'm looking for
I'm not just looking to solve this one problem, I'm looking for an overview to learn some new practice for future projects as well. I like writing "obvious" code, so not looking for too hacky solutions, like writing to the window object etc. Simple solutions with less dependancies are preferred. The goal is a fast site. It's not that important to reduce network usage/API calls.
What I have thought so far
This is the possible solutions I've come up with, somewhat sorted from simple/obvious to more complex.
Do a fetch inside the Footer component (client side)
Do a fetch in getInitialProps (server side & client side) on all /pages
Do a fetch in _app.js with a HOC and hooking into it's getInitialProps() and add it to props, so data is available for all pages
Use zeit/swr and data prefetching to cache data
Use redux to store a global state
All of these "work", but most of them will refetch the data unnecessarily, and/or adds a bit more complexity. Here are the pros/cons as I see it (numbers are the same as above):
πŸ‘ Simple! Fetch code is only in one place, it's located where it's used. πŸ‘Ž Data is fetched after page is loaded, so the content "jumps" in to view. Data is refetched all the time.
πŸ‘ Simple! Data is fetched on the server, so content is available before the page is rendered. πŸ‘Ž Data is refetched for each page. We have to remember to fetch the same footer data for each page in their getInitialProps().
πŸ‘ We can do the fetch in one place and add it to all the pages props, so footer data is automatically available for all pages' props. πŸ‘Ž Might be a bit more complex for some to easily understand what's going on, as it requires a bit more understanding of how Next.js/React works. Still refetches the data for all pages. We now do two fetch() calls after each other (first in _app.js to load footer content, then in each page to get custom content), so it's even slower.
πŸ‘ Somewhat simple. We can use the prefetching to load data to cache even before the JS is loaded. After first page load, we will have fast data fetching. Can have fetch code directly in footer component. πŸ‘Ž The rel="preload" prefetching technique won't work with all types of fetching (for instance Sanity's client using groq). To not have "jumpy" content where the data is loaded after initial page load, we should provide useSWR() with initialData which still will require us to fetch data in getInitialProps(), but it would be enough to just do this on the server side. Could use the new getServerSideProps().
πŸ‘ We can load data once(?) and have it available throughout the application. Fast and less/no refetching. πŸ‘Ž Adds external dependency. More complex as you'll have to learn redux, even to just load one shared data object.
Current solution, using the solution described in bullet point number 2.
const HomePage = (props) => {
return (
<Layout data={props.footer}>
<Home data={props.page} />
</Layout>
)
}
// Not actual query, just sample
const query = `{
"page": *[_type == "page"][0],
"footer": *[_type == "footer"][0]
}`
HomePage.getInitialProps = async () => {
const data = await client.fetch(query)
return {
page: data.page
footer: data.footer
}
}
export default HomePage
Would love some more insight into this. I'm a missing something obvious?
O'right! I found this thread while I was looking for something else. But since I had to work on similar issues, I can give you some directions, and I will do my best to make it clear for you.
So there are some data which you want to have it share, across your app (pages/components).
Next.js uses the App component to initialize pages. You can override it and control the page initialization. to achieve that simply create _app.js file in root of pages directory. For more information follow this link: https://nextjs.org/docs/advanced-features/custom-app
Just like the way you can use getInitialProps in your pages to fetch data from your API, you can also use the same method in _app.js. So, I would fetch those data which I need to share them across my app and eliminate my API calls.
Well, Now I can think of two ways to share the data across my app
Using of createContext hooks.
1.1. Create a DataContext using createContext hooks. and wrap <Component {...pageProps} /> with your <DataContext.Provider>.
Here is a code snippet to give you a better clue:
<DataContext.Provider value={{ userData, footerData, etc... }}>
<Component {...pageProps} />
</DataContext.Provider>
1.2. Now in other pages/components you can access to your DataContext like following:
const { footerData } = useContext(DataContext);
And then you are able to do the manipulation in your front-end
populates props using getInitialProps
2.1. getInitialProps is used to asynchronously fetch some data, which then populates props. that would be the same case in _app.js.
The code in your _app.js would be something like this:
function MyApp({ Component, pageProps, footerData }) {
//do other stuffs
return (
<Component {...pageProps} footerData={footerData} />
;
}
MyApp.getInitialProps = async ({ Component, ctx }) => {
const footerRes = await fetch('http://API_URL');
const footerData = await footerRes.json();
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps, footerData };
};
2.2. Now in your pages (not in your components) you can access to props including those you have shared from _app.js
and you can start to do you manipulation.
Hope I could give you a clue and direction. Have fun exploring.

Switching Layouts Based on Screen Size in Gatsby

I am building a web app using Gatsby that needs to load a separate layout built using mobile ui library in mobile browser and different layout with another ui library when opening in desktop browser.
How should I achieve this at the root (app) component level?
Thank you
I think you can experiment with wrapPageElement in gatsby-browser.js and gatsby-ssr.js, returning different layout based on browser height:
const React = require("react")
const Layout = ...
const MobileLayout = ...
exports.wrapPageElement = ({ element, props }) => {
// a made up hook to detect browser size, implement your own
const isMobile = useDetectMobile()
return (
isMobile
? <MobileLayout {...props}>{element}</MobileLayout>
: <Layout {...props}>{element}</Layout>
)
}
This get tricky though, because during server-side generation you'd need a default layout, which may (or may not?) lead to faulty behavior? I'm not sure.
A safe bet is to generate a mobile version & a desktop version for your app, then detect browser size / type in gatsby-browser & redirect accordingly.

Resources