<a href="https://www.livechat.com/chat-with/14893845/" rel="nofollow">Chat with us</a>, powered by<!-- --> <a href="https://www.livechat.com/?welcome" rel="noopener nofollow" target="_blank">LiveChat</a>
The Wrong Approach to CSS Refactoring

Not by the book: the "wrong" approach to CSS Refactoring

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.