Not by the book: the "wrong" approach to CSS Refactoring
APRIL 7, 2023
I bet many of you have encountered a dilemma: I know how to do it right (and I know why it is good to do so), yet it makes little to no sense in my case. I wouldn’t tease you with phrases like “rules are made to be broken” or “rulebreakers define the course of history”. It is more about sharing some practical examples that complement my other more theoretical article on style sheet refactoring.
Actually, if you are interested in some breaking rules theory, go and take a look at this article.
I was not aware of its existence while making my decisions, but I came across it while working on the current text, it is awesome, and there is TLDR; version!
The project I am working on is a presentational statically generated website made with React, we use Gatsby and Bootstrap. As mentioned, I have made some not-by-the-book decisions because it made much more sense within our team and for the project in general. Below I will discuss the first step of refactoring I have undertaken to safeguard our project against the deprecating @import
rule. Some other steps are combined and covered in a separate article since fewer not-by-the-book approaches exist.
@import to @use & @forward migration
The @import
the rule has several issues and will be deprecated within the next few years, according to the official documentation. The “right” alternative is the @use
rule, which introduces modules and addresses all the "wrongs" of the @import
.
If you are not sure about the difference between @use
, @import
, and @forward
in SASS, please take your time and check out this short video to follow the code changes with more finesse.
Modules
The idea is calm and clear: you isolate some style blocks from each other, enabling the same variable names repeatedly, plus you have an excellent maintainable structure. However, it shines bright only when you have this in mind from the beginning. Once you have a fully functioning project with overlapping imports and accessing variables and breakpoints from the same scope everywhere, it is quite a challenge to separate and rewrite styles module-wise.
We had the decision to make: either to do everything in the “right way” and separate our variables from bootstrap breakpoints, access them via namespaces, and so on. Or, leave things as they are but with the @use
rule.
What do we gain from the “wrong” approach? Is it irreversible?
Pros:
Less code to rewrite, which means greater speed and fewer error-prone changes.
Less code to write in the future. Currently, we have only one file with variables and the bootstrap import. No need to add several modules in this particular case.
The autocomplete still works fine; the team, accustomed to no-module workflow over the months, does not need to get used to something new.
If we need some modules in the future to be separated, we add them.
Cons:
It is wrong.
@use
is meant to be modular.It is harder to manage overlapping variables.
Now that you have some context of the changes I made in the project, I would like to guide you through them step by step, revealing all the caveats I encountered. Without any further delay, let’s finally eliminate this outdated @import
rule from our project!
Step 1: Replace @import with @forward if the Data is Not Used
Before, we had a start.scss
file, which imported everything from our variables and bootstrap to all styles, then everything became accessible everywhere. Note that we did not use anything within start.scss
, hence we can use @forward
rules for the style sheets to access their say in Layout.tsx
We must also remove bootstrap and our variables imports since those will be used in the forwarded style sheets and not in JSX. The changes looked like this:
Step 2: Manage the Reusable Data Flow and Put it One Place
To combine bootstrap and our variables, we created a new file imports.scss
and use @forward
rule again because we won’t use any values here. Immediately we face the issue of overlapping variables, such as primary
color and different breakpoints
.
Again there are at least two ways to handle it. The recommended approach is shown in the documentation and implies creating one more file to reassign the variables.
Something I really dislike in this approach is that you need to organize files in a certain way not to be confused about how variables are changing. Sure, it is possible to store all changes in the _override.scss
but then again, why bother, if there is a beautiful with()
configuration option? I, for one, favor this approach:
we have everything in one place - it is easier to maintain unless you have an overwhelming amount of overlapping variables;
we do not create extra files - which makes the project much more readable for newcomers;
we clearly see where and what is modified - ensuring no double overlapping.
imports.scss
, a container to @forward
combined values
I ended up with some container, which can store and @forward
some values, but what if I need to add certain base styles and make them accessible via the @extend
rule? If you were paying attention to my screenshots, you would have already guessed what I was talking about. It is the global.scss
under the variables
directory.
Why not copy all these “standardizing“ classes into import? Because here, we gain the most out of modules: we can use imports.scss
and their variables in those "base" classes.
we @use
imports.scss
in global.scss
to access variables
See, what did we do there? Our code is changed only with one line; we use as *
syntax to avoid the default module name (directory name) not to refactor the variables used.
Step 3: Make Combined Data Easily Accessible
The last thing to implement is to enable developers to use both imports.scss
and global.scss
data without changing the rest of the project.
combine imports.scss
and global.scss
I created _index.scss
under the styles directory because it is where all our style sheets reside. This enabled me to have a very simple addition to the rest of my styles, copying over precisely one line.
and one line to rule them all
Step 4: Bonus Challenge with @forward
Just in case you would think Step 1 was smooth, it wasn’t. Well, not quite smooth. You see, in our project, we needed one file, such as start.scss
to combine all styles. What naturally happened was the overlapping of the local variables of those style sheets. We did not want to reassign them though. Again, the “right” way would be creating a separate file for each style sheet with local variables and @use
those variables only there. It is cool, friendly and sweet, but it would be a lot of work, which was totally unnecessary. Let me show it.
@hide
is also a valid option!
In the whole file, where I changed @import
to @forward
there was only one conflict. Technically two, but they were within one file. And since there is an option to control visibility in SASS, I just used it. That is why I acted against the clean and friendly architectural solution. Because if we face a lot of overlapping, we create a separate file and now this structural change would create an enormous overhead to make it aesthetically cooler.
Step 5: Remove the Last @import
I would repeat myself here: “do not squander your focus; remember what your goal is.” We need to refactor style sheets to replace @import
the rule with the @use
and @forward
, it is our primary goal, and we made it. But! We also used an external font variables.scss
and @use
cannot do it.
1@import url("https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;700&family=Open+Sans:wght@400;500;700&display=swap");
As everything is done and this is a really small thing, I guess we could step out of the initial scope a little. Since we use Gatsby, I googled their solutions and found a neat package exactly for our purpose: "gatsby-omni-font-loader": "^2.0.2"
. To use it, we need to add a configuration object to gatsby-config.js
under the plugins
array.
make fonts accessible everywhere without @import
Now we can remove the last @import in the project and leave the font variable not to disrupt the existing workflow.
the last @import
is gone!
In conclusion
Some practices are so cool, called “best practices.” Are they universal, though? No, not really; I mean, it is really sweet to have “ideal architecture” or “clean code,” but how many things have to conjoin to make this possible? In real life, we need to seek the balance between fast delivery and ease of maintenance.
Our goal is to adapt and deliver and not to idealize the “what if” branch. Just be aware every decision leads to a consequence, but you are cool while it is predictable. So be brave and do things in a “wrong” way, as long as you can justify your rulebreakers.