Article summary
In server-side rendering (SSR), the client and server don’t always communicate perfectly. Recently, we faced a bit of a challenge configuring our app to display adaptive React components smoothly.
Ideally, all React components would be completely responsive, adapting themselves to every screen. But sometimes, we want to completely change the integrity of the component for a different device.
For example, imagine that you create an always-visible search bar that fills the top of the page on desktop screens. For mobile, however, you want the search bar to appear in a full-screen dropdown only after a button has been clicked. This was the dilemma I faced, and I realized that replacing components called for a more comprehensive approach than just resizing components.
What Didn’t Work
CSS media queries can be a great tool for controlling the visibility of components and thereby making them adaptive. However, my project is using a relatively complex UI toolkit that nests a lot of elements. Because of these complex sub-trees, we often can’t insert IDs and class names into elements that we want to make adaptive. CSS selectors become quite tricky because we’re left selecting nested components by the name the toolkit gives them. Should the toolkit ever get updated (resulting in code changes), the CSS selectors could become obsolete.
Another approach to creating adaptive layouts is to make a custom component (I first created one with React Hooks) that can define screen breakpoints for other components to reference. But with SSR, the server is not always aware of the window size. The React Hooks rely on a window resize event being triggered, but this client-side event doesn’t always make it to the server. In my experience, this caused odd rendering behavior where desktop components would get rendered initially until the server was able to receive the window information from the client.
What Worked–fresnal
That’s when I found @artsy/fresnal.
Artsy is a really cool place to discover and browse art. What I didn’t know is that the team at Artsy has created several awesome open-source projects, one of which is fresnal. This tool allows you to define a set of screen breakpoints that render components accordingly. It wraps these components with generated CSS and controls their visibility.
The main thing to keep in mind about this tool is that the server renders the HTML defined at every breakpoint. Although this might seem counterintuitive, it’s necessary for smooth adaptive implementation with SSR. When the client receives the HTML from the server to start rendering, it includes all of the components that it might need to render and checks its CSS to determine which ones to use. This means it doesn’t have to wait for communication with the server to adjust to the screen size–the problem I was having with a custom component.
Certainly, rendering all of the components can have an effect on performance. However, in our case, this was a minor concern as we only needed to rely on this solution for a couple of our components. Let’s check out how we used it.
In our media file, I defined our specific screen breakpoints:
import { createMedia } from "@artsy/fresnel";
export const AppMedia = createMedia({
breakpoints: {
sm: 0,
md: 540,
lg: 780,
},
});
export const { MediaContextProvider, Media } = AppMedia;
export const mediaStyle = AppMedia.createMediaStyle();
Then, I wrapped the components I wanted to make adaptive like this:
export const SomeComponent = (props) => {
return {
<MediaContextProvider>
<Media at="sm">
<MobileContent {...props} />
<Media/>
<Media at="md">
<TabletContent {...props} />
<Media/>
<Media greaterThanOrEqual="lg">
<DesktopContent {...props} />
<Media/>
</MediaContextProvider>
};
};
MobileContent, TabletContent, and DesktopContent are all their own sub-trees. Now I can import this component into any page file, and it will display correctly for each screen size. And that’s it! A perfectly functioning adaptive component with SSR.