While I do prefer React to Angular, both frameworks have their particular charm. Among the many out-of-the-box niceties that Angular provides, style encapsulation has been one of the most beneficial. What is style encapsulation? Style encapsulation refers to scoping your component classes and ids, ensuring that styles don’t leak to other components in your app. Before modern frameworks like Angular and React existed, and before build tools like webpack were a mainstream thing, we had to scope our classes and ids manually. We would try and write descriptive class names like ‘submit-button-for-user-form’ or worse, we’d end up in a specificity battle with other parts of our app, forced to piece together ancestor tags and selectors. In some cases, it was easier to just pull out the trump card and apply a !important to the end of the a particular style. Needless to say, it was really difficult to ensure that new styling wouldn’t stomp existing styling. React and Angular make those issues a thing of the past.
Style Encapsulation With Angular
Let’s imagine that we’ve created two submit button components. Both components will be functionally equivalent but each component will use different styling. Here’s our first submit button:
// submitButtonFirst.component.ts
import { Component } from "@angular/core";
@Component({
selector: "app-submit-button-first",
template: '<button class="submit-button">Submit</button>',
styles: [
`
.submit-button {
color: white;
background-color: red;
width: 200px;
height: 50px;
border-radius: 3px;
}
`,
],
})
export class SubmitButtonFirst {}
And here’s the code for our second submit button component:
// submitButtonSecond.component.ts
import { Component } from "@angular/core";
@Component({
selector: "app-submit-button-second",
template: '<button class="submit-button">Submit</button>',
styles: [
`
.submit-button {
color: white;
background-color: green;
width: 100px;
height: 30px;
border-radius: 3px;
}
`,
],
})
export class SubmitButtonSecond {}
Notice that in both components, we’re using the same ‘submit-button’ class in our template. In the old Javascript world, this would be a big issue. Whichever css class came first in the file would be overwritten by the second class, or at least the overlapping styles would be overwritten. Lucky for us, we’re living in the golden age of Javascript. When the page loads, we’ll see that both components display how we would expect. And on further inspection of the DOM, we notice something interesting:
/* the generated style for the first submit button */
.submit-button[_ngcontent-c1] {
color: white;
background-color: red;
width: 200px;
height: 50px;
border-radius: 3px;
}
/* the generated style for the second submit button */
.submit-button[_ngcontent-c2] {
color: white;
background-color: green;
width: 100px;
height: 30px;
border-radius: 3px;
}
Under the hood, Angular adds a unique identifier to each component we create. It then uses this custom identifier, in combination with our component style sheet, to create a unique style for each component.
Style Encapsulation With React
In React, this is something we don’t have by default. Instead, we need to install another dependency such as Emotion, a library which lets us write css in our javascript files. Here’s what that might look like:
// App.tsx
import * as React from "react";
import { css } from "emotion";
export class Button extends React.Component<{}> {
render() {
const buttonClass = css({
color: "white",
backgroundColor: "red",
width: 200,
height: 50,
borderRadius: 3,
});
return <button className={buttonClass}>Submit</button>;
}
}
If you inspect the actual class being applied, you’ll see that it’s something unique like ‘css-1tubuvb’. Under the hood, Emotion and Angular are effectively doing the same thing.
Merging React And Angular Patterns
Having seen how both frameworks manage style encapsulation, I realized that I wanted the same file structure in my React components that I have in my Angular components. In particular, I wanted to separate my main component file from the styles associated with that component. Why? First, it makes it really easy to find what you’re looking for because typically, a bug falls into one of two categories: logic or styling. Having the component separated from the styles makes it easier to find what you’re looking for. Second, it makes our component files significantly smaller. While this may not be obvious from the simple component we’re showing, a typical component could have 10 – 15 classes and could really clutter up our code. I don’t know about you but when I see a component file that hundreds and hundreds of lines long, it tends to stress me out. I tend to lose my train of thought a little bit easier. In general, we should avoid long blocks of code in favor of smaller, more modular files.
In my initial attempt to solve this problem, I created a sibling file and placed all of my styles in that file.
// App.styles.tsx
export const buttonClass = css({
color: "white",
backgroundColor: "red",
width: 200,
height: 50,
borderRadius: 3,
});
export const submitClass = css({
color: "black",
backgroundColor: "green",
width: 100,
height: 30,
borderRadius: 3,
});
And then our component would import the variables from this file.
// App.tsx
import * as React from "react";
import { css } from "emotion";
import { buttonClass, submitClass } from "./app.component.styles";
export class App extends React.Component<{}> {
render() {
return (
<div>
<button className={buttonClass}>Regular Button</button>
<button className={submitClass}>Submit Button</button>
</div>
);
}
}
After experimenting with this for a little while, I realized that it had a few major downsides. The biggest downside here is that I can’t use my component’s props and state to dynamically render styles. Let’s see if we can fix that.
// And here's our base component file:// App.ias.tsx
export interface AppComponentProps {
regularButtonColor?: string;
submitButtonColor?: string;
}
export interface AppComponentState {}
export const createAppClasses = (
props: AppComponentProps,
state: AppComponentState
) => {
const buttonClass = css({
color: props.regularButtonColor || "white",
backgroundColor: "red",
width: 200,
height: 50,
borderRadius: 3,
});
const submitClass = css({
color: props.submitButtonColor || "black",
backgroundColor: "green",
width: 100,
height: 30,
borderRadius: 3,
});
return {
buttonClass,
submitClass,
};
};
And here’s our base component file:
// App.tsx
import * as React from "react";
import { css } from "emotion";
import {
createButtonClasses,
AppComponentProps,
AppComponentState,
} from "./app.component.ts";
export class App extends React.Component<AppComponentProps, AppComponentState> {
render() {
const { buttonClass, submitClass } = createButtonClasses(
this.props,
this.state
);
return (
<div>
<button className={buttonClass}>Submit</button>
<button className={submitClass}>Submit button</button>
</div>
);
}
}
Here, we’ve essentially created the same thing as Angular. We have the benefit of using our props and state to dynamically alter class values, our component styles are completely isolated, and we don’t have to deal with giant files. As a side note, I add on .ias to the style files. It’s an abbreviation for the ‘interfaces and styles’, which we group together to avoid circular dependencies. If we didn’t do it this way, our styles file would import our interfaces from the base component while the base component was importing the styles. This is a good way to avoid that.
There’s certainly a lot of ways to do component encapsulation in a React project and every way has its merits. Give this one a go on your next project and let me know what you think!