Reputation:
(Heads-up warning: I am not extremely familiar with React/GraphQL, so please go easy on me).
I am building a blog using Gatsby and my navbar changes its appearance based on the screen-size. In the middle of my navbar is the logo of the blog. Now, when the reader is viewing the page on Desktop, I want the full logo (including text) to show. When they are viewing it on mobile, I only want the actual logo (excluding text) to be rendered (I have one image with the full logo and one image with the shortened logo).
So far, my approach has been to 1. create two separate SiteNavLogo-components and 2. check the current window size using the following block of code (I removed all of the code that is not relevant to this problem and replaced it with "...") and 3. render one of the two logo-components conditionally:
class SiteNav extends React.Component {
...
};
render(): JSX.Element {
changeLogo = window.matchMedia('(max-width: 600px)').matches;
return (
<>
...
{changeLogo && (
<SiteNavLogoMobile />) || !changeLogo &&
<SiteNavLogo/>}
</>
);
}
}
This works fine with gatsby develop
but fails with gatsby build
because "window" is not available. In this article I found the recommendation to use componentDidMount, so I tried this:
class SiteNav extends React.Component {
changeLogo = false;
componentDidMount(): void {
this.changeLogo = window.matchMedia('(max-width: 600px)').matches;
}
};
render(): JSX.Element {
return (
<>
...
{this.changeLogo && (
<SiteNavLogoMobile />) || !this.changeLogo &&
<SiteNavLogo/>}
</>
);
}
}
The problem with this is that the first render cycle is executed before componentDidMount is executed, which means that the page will always be rendered with changeLogo=false.
How can I make it so that when a mobile user views the page, the shortened logo is rendered? (Do I even need two components for this or can I just change my GraphQL-query?) Any advice is highly appreciated.
Upvotes: 1
Views: 2646
Reputation: 11577
You shouldn't handle this with JS, as it'll always cause rendering mismatch.
For example, if by default, you render <Logo>
when the viewport width is smaller than 400px, but a user visit your site with a viewport width of 600px, they will see a brief flash of wrong component before React kicks in.
You could get around this by checking window
and just render nothing in SSR (which is a fine approach for non-SEO sensitive content), but then you lose out on benefit of using Gatsby in the first place, search engine can't see your logo, and depend on the way your layout is setup, user might still see a flash of layout shift.
If your component can't be simply modified with only CSS, what I recommend is the good ol' CSS switcharoo:
<div className="display-xs">
<MobileVariant />
</div>
<div className="display-md">
<DesktopVariant />
</div>
Upvotes: 3
Reputation: 29335
Try:
return (
<>
...
{ typeof window !== 'undefined'
? window.innerWidth <= 400 ? <SiteNavLogoMobile /> : <SiteNavLogo/>
: null
}
</>
);
Of course, this code should be refactored to avoid chained ternary conditions (bad readability) but I wanted to show the idea of the approach. If the window
is defined, it will match the first condition, rendering one component or another depending on the innerWidth
(you can change it to your matchMedia
) if not, it will not render anything (null
) until the code gets the innerWidth
of the window.
You can add it to a custom function instead of rendering it in the JSX, as you wish.
This code will be triggered as the window width changes.
Upvotes: 0
Reputation: 720
You should use the component's state to store the changeLogo
variable. By using a state variable, changeLogo
is correctly updated in componentDidMount()
resulting in the correct nav bar being displayed.
To ensure that changeLogo
updates depending on the viewport width, an addEventListener
has also been included in the component.
class SiteNav extends React.Component {
constructor(props) {
super(props);
this.state = {
changeLogo: false
};
this.handleResize = this.handleResize.bind(this);
}
handleResize() {
this.setState({
changeLogo: window.matchMedia("(max-width: 400px)").matches
});
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
render() {
return (
<div>
{this.state.changeLogo && <SiteNavLogoMobile />}
{!this.state.changeLogo && <SiteNavLogo/>}
</div>
);
}
}
Here's a Codesandbox demo.
Upvotes: -1