Max Schmitt

April 12 2024

React Breakpoint Props: How Simple Components Become Complicated due to Server-Side Rendering

For a recent project, I was working with a <Button> component that had two different variants: "default" and "minimal".

Using React and Tailwind CSS, this component is very easy to implement:

components/Button.jsx

function Button({ variant = 'default', children, ...rest }) {
return (
<button
type="button"
className={twMerge(
variant === 'default' && 'bg-blue-500 text-white p-4',
variant === 'minimal' && 'bg-transparent text-blue-500 p-0'
)}
{...rest}
>
{children}
</button>
)
}

However, now my client designed a section where the <Button> component needed to appear in different variants depending on the screen size.

Rendering Different Variants for Different Screen Sizes

If the React application was rendered entirely on the client side, this would be simple:

JSX

const windowSize = useWindowSize()
const isMobile = windowSize.width < 640
return <Button variant={isMobile ? 'default' : 'minimal'}>Click me</Button>

But of course: The React app was server-rendered. Ugh!

The above approach would still work but it would cause content flashes and layout shifts as soon as the client code takes over.

Here's the solution I came up with. It's kind of ugly but it works.

Using SASS to Support Different Variants for Different Breakpoints with SSR

First, I created SASS mixins for the two button styles:

SCSS

@mixin button--button {
@apply bg-blue-500 text-white p-4;
}
@mixin button--minimal {
@apply bg-transparent text-blue-500 p-0;
}

Then, I added classes for each possible breakpoint / variant combination:

SCSS

// Mobile
.button--default {
@include button--default;
}
.button--minimal {
@include button--minimal;
}
// Tablet+
.button--md--minimal {
@media (min-width: theme('screens.md')) {
@include button--minimal;
}
}
.button--md--default {
@media (min-width: theme('screens.md')) {
@include button--default;
}
}

And as a final step, I let my React <Button> component accept a separate variant for each breakpoint:

JSX

function Button({ variant = 'default', mdVariant, children, ...rest }) {
return (
<button
type="button"
className={twMerge(
variant === 'default' && 'button--default',
variant === 'minimal' && 'button--minimal',
mdVariant === 'default' && 'button--md--default',
mdVariant === 'minimal' && 'button--md--minimal'
)}
{...rest}
>
{children}
</button>
)
}

In the actual code base, the button also had different icons that would be shown depending on the variant. With this SASS-supported approach, it was pretty easy to add that functionality as well.

The above approach also works with CSS modules which is great if your code base already uses them.

Conclusion

I love the prop-based approach for styling React components but it starts breaking down in a server-rendered environment where we can't access the user's screen size via JavaScript.

In these cases, we need to bridge the gap using more traditional CSS techniques.

These kinds of experiences have also taught me to avoid server-side rendering when it's not necessary.

I hope this articles helped you out a bit. If you have a better solution to the problem I'm describing, please let me know on Twitter. I'd love to hear from you!