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

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

Related

Absolute positioning of image based upon size of user's resolution (Gatsby)

I am attempting to place an image (using absolute positioning) above the fold on the initial render of a page I am building using React (Gatsby SSR). The issue I am having is that the useWindowSize hook fires immediately and then erroneously places the image in the wrong position.
My current solution determines whether the component exists in the vDOM and then uses a setTimeout , before pushing the new position values into state.
// On initial render, position hero container background
useEffect(() => {
if (elementRef && inView) {
setTimeout(() => {
console.log("FIRED");
const boundingClientRect = elementRef.current.getBoundingClientRect();
setContainerWidth(boundingClientRect.width);
setContainerHeight(boundingClientRect.height);
setContainerDistanceFromTop(boundingClientRect.top);
setContainerDistanceFromLeft(boundingClientRect.left);
}, 2000)
}
}, [inView]);
Obviously there are many, many flaws with this approach (won't trigger on slow devices) - but I'm struggling to think of the most optimal way to cause a re-render of the image.
Another solution would be to repeatedly check if the state has changed for a period of time (every second for 10 seconds), but this still doesn't feel very optimal.
I am sure there's a far more elegant approach out there, would be grateful if anybody could assist?
Thanks
Well, you can avoid 4 of your useStates using one single useState that contains an object with all your properties. In addition, you should be able to get rid of the setTimeout because using the empty deps ([]) will ensure you that the DOM tree is loaded (hence your element is in the view).
const [properties, setProperties]= useState({});
useEffect(() => {
if(elementRef.current){
const boundingClientRect = elementRef.current.getBoundingClientRect();
setProperties({
width: boundingClientRect.width,
height: boundingClientRect.height,
top: boundingClientRect.top,
left: boundingClientRect.left
})
}
}, []);
It's important to set initially the elementRef as null the avoid React's memoization and setting initially the value as null before rehydration. In that way, you only need to check for the elementRef.current and setting all properties at once using one single useState. After that, you only need to access each property like: properties.width, and so on.
The inView boolean is also unnecessary since the empty deps ([]) will fire your effect once the DOM tree is loaded.

I want to use transition effects in react app. What type/ library for animation in react app should I use according to the latest trend?

I want to use some transition effects in my react js app. I am using function components in my app.
How do I include transition effects in app according to the business requirement these days?
I want to use animation such that on every render I can see the effect. It would be great if someone can help me out with an example.
If you want to use a library, I would suggest react-spring
https://react-spring.io/ it is based on spring physics, If you want to read about that more check this out https://www.joshwcomeau.com/animation/a-friendly-introduction-to-spring-physics/
And there is also another good option which is framer motion https://www.framer.com/motion/ which apparently offers more possibilities maybe out of the box (I personally have never tried it before)
For examples you can check their websites they have good examples.
I'm not sure what effect you are trying to generate.
css can be used by itself to generate animations or transitions.
You want to see the effect on each render?
i.e. You want to tie the effect to the react render cycle?
non-memoized values will change on every render
You could use a simple statement like const trigger = {};
Then react to trigger with a useEffect
useEffect(() => { do something }, [trigger]);
finally, visual effect.. apply a class based on state and use setTimeout to remove the state (and therefore the class)
This could be overly involved for exactly what you are trying to achieve but this works for all possible flows based on the question.
Here is one example with div element is moving to according vertical scroll position .
Look carefully.
First, Set the position using useState and define the window.onscroll function.
const [cardTop, setCardTop] = useState(0);
window.onscroll = function() {
if (window.pageYOffset < 30) {
setCardTop(window.pageYOffset + 'px');
}
};
Second, Set the style's top as state variable.
<div className='card t-card' id='tCard' style={{top:`${cardTop}`}}> ...
Congratulations. It probably act exactly.
It's similar to use Jquery or another Javascript, Only use state variable.
Thanks.

Rendering issue with custom map component inside tabbed form of react-admin

I am using React-admin for a project where for some resources, I use the tabbed form to better organize the fields and inputs. I created a fairly simple custom map component based on react-leaflet, which I am using in these tabbed forms.
I am facing a weird issue where when the map is on other than the first tab, its contents do not display correctly. It appears as though the map "thinks" the viewport is much smaller than it actually is. Reloading the page (or even just opening developer tools in Chrome) forces re-render of the page and causes the map to start behaving correctly.
To better demonstrate, I created this simple Codesandbox project. It has a user resource from the RA tutorial with two tabs. Both contain an instance of the map component, but while the map on the first tab works correctly right away, the one on the second tab renders incorrectly.
I confess I am still kind of a noob at these things so I may well be doing something wrong, but I've been scratching my head for quite a few hours over this and I'd like to start eliminating possible culprits.
Any insight would be most welcome.
Thanks very much in advance for your time.
This issue has been discussed a lot in SO. If you search a bit you can find the reason. What you actually need to do is two things:
use setInterval in combination with map's invalidateSize method when switching a tab and then clean it on component unmount
use useEffect to change mapZoom and view since they are immutable.
SO since you use react-leaflet version 2.7.x you need to take the map instance using a ref:
const mapRef = useRef();
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current.leafletElement;
const mapZoom = zoom || defaultZoom;
let intervalInstance;
if (!center && marker) {
map.setView(marker, mapZoom);
intervalInstance = setInterval(() => map.invalidateSize(), 100);
} else if (!center) {
map.setView([0.0, 0.0], mapZoom);
}
map.setZoom(mapZoom);
return () => clearInterval(intervalInstance);
}, []);
<LeafletMap
ref={mapRef}
center={marker}
zoom={defaultZoom}
className={classes.leafletContainer}
>
Demo

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

Anime.Js Seek function not reversing in React

I'm trying to make an animation that transitions as you scroll. I need it to reverse while scrolling up. I know this is possible (here's an example) but I can't get a similar behavior to work in React.
I made this demo as a way to simplify my issue (based on what's in AnimeJs's docs):
const me = useRef();
const [zoom, setZoom] = useState(0);
const zoomies = anime({
targets: me.current,
translateY: "80vh",
autoplay: false
});
useEffect(() => {
zoomies.seek(zoomies.duration * (zoom / 100));
}, [zoom, zoomies]);
It's pretty simple, you can see it in action here:
https://codesandbox.io/s/animejs-react-seeking-wk09q
As you can see it only goes forward and not relative to slider. It also never reverses. I"m not sure what's going on...
I was able to fix my animation, you can see a working version here:
https://codesandbox.io/s/animejs-react-seeking-h8y6m
The biggest change is that I moved the animation declaration into a useState hook. What I think was happening was the animation was being recreated on every tick so there was no way for it to reverse. But once React knew about it between re-renders it just started working.
If this is wrong or in any way inaccurate someone please correct me!

Resources