Will React Server Components kill CSS-in-JS? Why We Switched from Styled Components to SCSS-Modules ππ
Εukasz Fiuk
You might wonder... Why? π€π¨
Before we even begin, I'd like to explain why I'm even considering switching from Styled Components to scss-modules. Over the past few years, since I first used Styled Components
, I fell in love with CSS-in-JS
solutions. Why? Mainly because my markup has become much more readable, and I've been able to share variables between JavaScript
and my CSS
. On top of that, adding dynamic styles or conditional props has been a breeze. Reusing media queries or common variables just worked, and I could use TypeScript
everywhere.
So why would I even consider switching from CSS-in-JS
to a more traditional approach like scss-modules
? Well, there are a few reasons why some people don't like Styled Components
, with the three main points of critique being:
- π Performance:
Styled Components
require aJavaScript
runtime, and therefore could be slightly less performant than pureCSS
. - ππΎββοΈ Extra configuration / dependencies: Using
Styled Components
requires depending on external packages and extra configuration (not always) that doesn't work outside theReact
Ecosystem. - π§ Additional abstractions:
Styled Components
require getting used to a new syntax, and it takes some time to become familiar with it.
I was cool with all these cons, but then Next.js 13.4 launched with React Server Components
and finally stable app directory
. Those Server Components bring one huge issue - we cannot render Styled Components
on the server, because they rely on web APIs and React Context
. Thatβs painful, because even though we could still use Styled Components
inside βclientβ components, we cannot use power of RSC
to its full potential as almost all of my components require some form of styling⦠Because of this, the Next.js team recommends using css-modules, and I'm going to stick to this recommendation.
Quick note - I know there are
CSS-in-JS
alternatives that could work, like Linaria, but after reading a long list of potential issues on GitHub, I decided to bite the bullet and finally go back to almost pure CSS. It's been quite a refreshing experience. π±
Configuration π§
Great, with that out of the way, let's focus on actually moving from Styled Components
to scss-modules
. I'm using Next.js
13.4 with an app directory
, but the process should look the same if you are using older versions with pages, and it should be very similar even if you aren't using Next.js
at all.
Previously, I mentioned that scss-modules
doesn't require setup, but that's only partially true. You can use CSS modules in Next.js without any configuration, but I'd like to use the Sass SCSS
syntax. For that, you'll need to actually install Sass as a dev dependency:
πΎ >_ terminalnpm install --save-dev sass
OR if you are using yarn
πΎ >_ terminalyarn add sass --dev
Yay, you can now use scss-modules
in your Next.js
project! Just create files with a .module.scss extension, and import your classes into your component.
Here's a basic example:
πΎ >_ txt/1_components/ βββ Button/ β βββ Button.tsx β βββ Button.module.scss
πΎ >_ tsx// import all classes under styles ( or different ) namespace import styles from "./Button.module.scss"; const Button = ({children}) => { return( {/* apply classes */} <button className={styles.button}> {children} </button> ) }
IntelliSense Support π€
To make our coding lives easier, let's get IntelliSense support, so it can give us with helpful hints, such as the available CSS classes we can use. To enable this functionality, we're going to install the typescript-plugin-css-modules
package.
You can install it via the npm command:
πΎ >_ terminalnpm install -D typescript-plugin-css-modules
Or if you're using Yarn:
πΎ >_ terminalyarn add -D typescript-plugin-css-modules
Once installed, add this plugin to your tsconfig.json
:
πΎ >_ ts{ "compilerOptions": { "plugins": [{ "name": "typescript-plugin-css-modules" }] } }
You'll be rewarded with these lovely auto-complete suggestions for your CSS classes:
Global Styles and Theming ππ¨
Let's start with global styles. It's as easy as creating a stylesheet and importing it into your top-level component. For me, that's the RootLayout component.
πΎ >_ tsximport βstyles/globalStyles.scssβ export const RootLayout = ({children}) => { return( <body data-theme=βdarkβ> {/* note data-theme attribute */} <main> {children} </main> </body> ) }
Theming? Piece of cake. π° We'll use the data-theme
attribute on our body
tag. Our color palette and other constants will be stored in Sass variables, ready to be used anywhere in the project. Let's create a _palette.scss
:
πΎ >_ scss///////////////////////////////////// // Color Palette // Names were generated by at chir.ag/projects/name-that-color/ ///////////////////////////////////// $white: #fff; $gray: #878787; $cod_gray: #111010; $transparent: #00000000; // Export the color palette to make it accessible to JS :export { white: $white; gray: $gray; cod_gray: $cod_gray; transparent: $transparent; }
And here's how to use these variables in our JavaScript files:
πΎ >_ tsximport variables from './_palette.scss';
Finally, let's use the power of data attributes to handle theming:
πΎ >_ scss// globalStyles.scss body{ [data-theme="light"] { --background: $white; --primary: $cod_gray; --secondary: $gray; } [data-theme="dark"] { --background: $cod_gray; --primary: $white; --secondary: $gray; } }
And that's it! Your styles are now incredibly flexible, and it's easy to switch your design between different themes. π¨
NOTE: I'm assigning colors to SCSS variables, so we can export our styles for usage in JavaScript. Then in the project I'm using pure css variables that will have responsive value based on current theme.
Extending Styles & Combining Classes πͺ
If you've used Styled Components
before, you might recognize this pattern for extending styles of certain components:
πΎ >_ tsxexport const CustomButton = styled(Button)` color: red; `
Good news! You can achieve similar functionality with scss-modules. However, instead of styled components, you'll pass the className prop to your components. Here's an example:
πΎ >_ tsximport clsx from "clsx" export const Button = ({className, children}) => { return ( <button className={clsx(styles.button, className)}> {children} </button> ) }
You might be wondering about clsx
- it's a tiny utility library that allows you to construct className
strings in a more intuitive way. It helps in easily combining multiple classes.
To install clsx
, use the following commands:
If you are using npm:
πΎ >_ terminalnpm install clsx
Or yarn:
πΎ >_ terminalyarn add clsx
Now, extending and combining classes should be super easy, just pass a className π€
Reusable & Consistant MediaQueries π
In this section, we're defining breakpoints for our layout, and creating a reusable @mixin
for media queries.
πΎ >_ scss// Define your breakpoints $tablet: 640px; $desktop: 1024px; $desktop-large: 1280px; // Define your media mixin @mixin media($breakpoint) { @if $breakpoint == "tablet" { @media (min-width: $tablet) { @content; } } @else if $breakpoint == "desktop" { @media (min-width: $desktop) { @content; } } @else if $breakpoint == "desktopLarge" { @media (min-width: $desktop-large) { @content; } } }
The @mixin
directive lets you create reusable chunks of CSS
. In this case, we've made a mixin called media that accepts a $breakpoint
parameter.
Here's how you would use this mixin:
πΎ >_ scss.example { font-size: 1rem; @include media("desktop") { font-size: 1.5rem; } }
Please note that you'll have to import your mixins, before using them in
.scss
files. You can use either@import
or@use
but that's basicSass
so I won't explain it further.
Wrapping Up π¦
At first, leaving Styled Components
behind was tough. I was a big fan of CSS-in-JS
. But a few weeks into using scss-modules
, I've got to say - I'm having a pretty good time. My code has gotten simpler, and it's easier to read than I first thought it would be.
I hope you had fun reading this article. If you have any questions, don't hesitate to drop them in the comments section. Cheers and happy hecking!