Switching Layouts Based on Screen Size in Gatsby - reactjs

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.

Related

How to make Ionic React implementation respect UI = f(state)?

Ionic React seems not respecting UI = f(state).
I started an application with
ionic start ionic-tests tabs --type react
import IonCheckbox from #ionic/react and used with <IonCheckbox checked />, the expected behavior while clicking is keep checked, but the checkbox was toggling...
Is it the Ionic purpose? There is any way to ensure Ionic React UI as state function?
Ionic uses stencil (web-components) and a layer (called output targets) to solve some connection problems with React. By investigating the code I saw about the #Prop decorator with the mutability option, but it seems doesn't work (so what is the point about the mutability option?).
manage state
const [checked, setChecked] = useState<boolean>(true);
render
<IonCheckbox
checked={checked}
onIonChange={(e: any) => setChecked(e.detail.checked)}
></IonCheckbox>
https://ionicframework.com/docs/api/checkbox

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,
},
},
})

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

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

What is the best way to have a React-generated static (SEO) "public" frontend alongside a CRA "private" app?

I've been using Create-React-App and dig the whole setup, basically I'm looking to keep developing with JSX instead of switching to Gatsby/React-Static's Markdown/etc coding style. Similar question to this one regarding Gatsby. I'm looking to have a search engine optimized static "public" frontend (e.g. product pages, blog, etc.) that is generated by Gatsby or react-static. But I'd also like a typical client-side rendered React app for the "private" section (e.g. sellers can edit their product pages). I know you can set the "homepage" so I was thinking of setting the private section to "example.com/in/" or similar.
Any advice on how to best split this project?
If you aren't using the GraphQL stuff, Gatsby is predominantly just using React SSR and a custom webpack configuration with Reach Router and some glue to stick it all together.
You can absolutely have a separate Webpack configuration that outputs to your public folder and set up your host/deployment to route all of your non-static routes to your application entry.
You can also use Gatsby to generate all of those dynamic pages with juicy client-side fetches, and you basically get free static skeleton entry points for each of your pages like this:
const useMounted = () => {
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
}, [])
return isMounted
}
const SomeComponent = props => {
const isMounted = useMounted()
return isMounted
? <div>Mounted client-side component</div>
: <SomeComponentSkeleton />
}
Effectively this just prevents issues with hydration that occur if you return null on server-side render and then return a component on the client-side. You can also use isMounted to kick off any code that uses window et al.

Creating Different mobile layout for a component in ReactJs

How can we create a completely different (mobile type layout) for a component (having a different desktop type layout) using ReactJs.
(Not Responsive , responsive is something css has to take care of.)
It should be different layout for the component i.e here Creating a page with a menu(header menu) for desktop screens which becomes a navigation sidebar with logo on small screen.
Honestly, a simple resposive css layout may be the best solution, but the steps are to
1) Detect in JS if user in on mobile or desktop. For example this question has a good suggestion as an answer: Detecting a mobile browser
2) Use it to decide in your root component that which layout to use:
function isMobile() {
// some js way to detect if user is on a mobile device
}
class Root extends Component {
render() {
return isMobile() ? ( <MobileLayout /> ) : ( <DesktopLayout /> )
}
}
Checkout react-responsive, You can use media query to render different component depending on device size.

Resources