[Styling, Demo]

1 Dec 2020

-

3 min read time

Reverse Engineering Styled Components

CSS-in-JS is cool, but how does it really work? Turns out it's not that difficult to create a styled-components like library yourself. Find out how right here.

By Shreyans Jain

image

The idea

Last year, when it was still possible, I attended a ReactDay Meetup in Berlin and was astounded by an awesome talk by an amazing speaker & a mentor, Tejas Kumar. His talk was called "Destructuring React" and instead of a bunch of slides, he amazed everybody by live-coding "Poor Man's React" (his words).

Starting from scratch, he reverse-engineered some React functions like createElement & useState building them in vanilla JS. This idea struck me making me believe that it is possible to do the same for many other libraries. I am finally putting my idea into words & code.

I am going to reverse engineer the styled-components library with some basic features. Here's how it goes:

Step 1: Basic File Setup

  • Create an empty directory with entry files - index.html & index.tsx

  • yarn add parcel react react-dom

  • yarn parcel index.html

index.html

<div id="app"></div>
<script src="index.tsx"></script>

To begin with, I am going to create a "styled" component and render it using React. Remember, I did not install the styled-components library, because I am going to create it!

index.tsx

import React from "react";
import ReactDOM from "react-dom";

const RedH1 = styled("h1")`
color: red;
`;

const App = () => {
return (
<>
<RedH1>RedH1</RedH1>
</>
);
};

ReactDOM.render(<App />, document.getElementById("app"));

Guess what happens when I serve this? I get an ERROR!

image

Step 2: Creating the styled function

To create my own styled-components library, I will create a file styled.tsx, but to even start writing code there, we need to understand how the styled-components API works.

Deciphering the way we write a styled component, styled is a function which accepts "tag name" as an argument & returns another function that accepts a "tagged template". According to MDN:

"Tags allow you to parse template literals with a function. The first argument of a tag function contains an array of string values. The remaining arguments are related to the expressions."

To test this logic, I am going to create a function called styled that just logs all the arguments that are passed to it.

styled.tsx

const styled =
(...args1) =>
(...args2) => {
console.log({ args1, args2 });
};

export default styled;

Testing this by including it in index.tsx, I get the following in console:

image

Since I am trying to render this component, I also get an error from React.

Recognizing the console.log, the first argument is the HTML tag (Tag) and the second argument is the array containing style strings (styleArr).

Step 3: Making things work

I am now going to return a functional component that renders the original HTML tag & persists all props. I am also going to de-structure the className prop and rename it to originalClassName.

styled.tsx

import * as React from "react";
const styled = (Tag) => (styleArr) => {
return ({ className: originalClassName, ...restProps }) => (
<Tag
className={`${originalClassName ? ` ${originalClassName}` : ""}`}
{...restProps}
/>
);
};

export default styled;

image

Step 4: Applying CSS

I must now take care of the styles and apply them to the h1 element. If you have ever noticed the generated HTML/CSS when using the original styled-components, it generates classNames and outputs all css styles in an internal stylesheet. Trying to recreate that, I am gonna focus of the following features:

  • There should only be one style tag in head section of the document.

  • All styled-components having same css should reuse the className.

  • The className should be generated using the css without considering the tag name.

  • The components should be chain-able (a component can use another component as a base).

styled.tsx

import * as React from "react";

// create & insert our style tag
const styleTag = document.createElement("style");
document.head.appendChild(styleTag);

// hashing function, generates a numerical string
// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
const hashCode = (s) =>
s.split("").reduce(function (a, b) {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0);

const styled = (Tag) => (styleArr) => {
const styles = styleArr[0].trim();
// hash styles string to generate a className
const className = `style${hashCode(styles)}`;
const styleStr = `.${className} {${styles}}`;

// check if styles already exist & prevent duplicate
const isDuplicate = styleTag.innerText.indexOf(styleStr) > -1;
if (!isDuplicate) {
styleTag.innerText = `${styleTag.innerText} .${className} {${styles}}`;
}

return ({ className: originalClassName, ...restProps }: any) => (
<Tag
// prefix generated className to originalClassName
className={`${className}${
originalClassName ? ` ${originalClassName}` : ""
}`}
{...restProps}
/>
);
};

export default styled;

imageI picked up a hashing function from StackOverflow that returns a numerical string, therefore, I prefix it with "style".

Rest is rather straightforward, and the comments should be self-explanatory.

Check out the whole code with some more examples here: https://github.com/shreyansqt/reverse-engineering-styled-components.

One More Thing

You must be wondering, what about the styled.h1 syntax of styled-components. Since all functions are objects in javascript, we can simply create object properties for all tag names as follows:

const tagNames = ["h1", "h2"];
tagNames.forEach((tagName) => {
styled[tagName] = styled(tagName);
});

I am off to some more adventures of similar sort. If you have any comments or would like to discuss this post, reach out to me on https://twitter.com/shreyansqt & let's grab a virtual coffee.

By Shreyans Jain

[More from our Blog]

Keep reading