Rewrite styled-components in ReactJS with just 60 lines of JavaScript code
duncan
Posted on September 29, 2023
"styled-components" is a library I really like for writing CSS in React. After a few days of research into its source code, I decided to rewrite it to understand it as deeply as possible. I felt that I learned a lot from this repository, so I wrote an article to share with others what I have learned from it. To understand the code in my repository, I will explain two concepts that were relatively new to me and are essential to understand. If you are already familiar with them, you can skip ahead. ^^
Stylesheet
If anyone is already familiar with stylesheets in JavaScript, they can skip this section. In styled-components, it is used to store styles. Each style tag corresponds to one sheet, and each sheet contains many rules. One rule corresponds to a className and its accompanying style. For example, I will illustrate adding a rule to a sheet below.
// to get all the sheets in the app
const sheet = document.styleSheets;
// choose 1 rule in sheet and add css to rule
sheet[1].insertRule ('.classname {width : 100%;height : 100%;}', indexStyle)
Detail docs about sheet: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet
Tagged templates
The tagged templates I'm referring to here are the syntax commonly used in styled-components.
// this function only return one ar chứ tất cả các đối số của function này
const templateFunc = (...args ) => args
templateFunc`
width : 100%;
height : 50%;
background : ${props => props};
The result is that we can retrieve the parameters like this.
Link to detail: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
Let's get started writing the library
I will write a ComponentStyle class to handle the initialization of stylesheets and the insertion of CSS for components.
class ComponentStyle {
sheet = {};
constructor() {
// as soon as the class is initialized, I will create a style tag and store the sheet of that style in the 'sheet' variable within the class.
const styleDom = this.makeStyleTag();
// retrieve the sheet and save it in the this.sheet variable.
this.sheet = styleDom.sheet;
}
makeStyleTag = () => {
// this function will create a style and add that style tag to the head of the document.
const style = document.createElement("style");
style.setAttribute("data-style-duc-version", "1.0.0");
document.head.insertBefore(style, document.head.childNodes[document.head.childNodes.length]);
return style;
};
insertBefore(css) {
// the function will generate random class names for components and return the class name.
const className = uuid();
const newName = "style-duc-" + className.slice(0, 5);
this.sheet.insertRule(
`.${newName}{${css}}`,
this.sheet.cssRules.length
);
return newName;
}
}
The code in the core.js file is probably the most complex part. I believe it would be much easier for everyone to understand the details of my code by cloning my repository code, running it, and then coming back here to read the explanation.
// This function will directly create a React element.
export default function createStyledComponent(target, css) {
let WrappedStyledComponent = {};
// init class ComponentStyle
const componentStyle = new ComponentStyle();
// it saves it in the WrappedStyledComponent variable so that later, in the useStyledComponentImpl function, it can be retrieved and used.
WrappedStyledComponent.componentStyle = componentStyle;
WrappedStyledComponent.target = target;
const forwardRef = (props, ref) => useStyledComponentImpl(WrappedStyledComponent, props, ref, css);
const Element = React.forwardRef(forwardRef);
Element.toString = () => {
return `.${WrappedStyledComponent.newClassToString}`;
};
return Element;
}
const renderCss = (cssRaw, propsElement) => {
// this function will handle the mapping of the cssRaw array into a CSS string. Each element in the cssRaw array can be either a function or a string.
let css = "";
for (const elementCss of cssRaw) {
if (typeof elementCss === 'function') {
const result = elementCss(propsElement);
css += result;
}
if (typeof elementCss === "string") { css += elementCss; }
}
return css;
};
// CORE FUNCTION
// this function is responsible for creating a React component, merging props, and passing the ref.
const useStyledComponentImpl = (WrappedStyledComponent, props, ref, css) => {
// support for the ThemeProvider in styled-components
const theme = React.useContext(ThemeContext);
const newProps = { ...props, ...{ theme, ref } };
const newCss = renderCss(css, newProps);
// insert css and get new className
const className = WrappedStyledComponent.componentStyle.insertBefore(newCss);
WrappedStyledComponent.newClassToString = className;
// truncate name these className
newProps.className = [props.className, className].join(" ");
// finally, it creates a React element.
return React.createElement(WrappedStyledComponent.target, newProps);
};
Finally, the code in the index.js file, which is the file that everyone will import whenever they use styled-components in the future.
// domElements is an array that contains HTML tags, e.g., domElements = ['a', 'div', 'section', 'svg', ...]. For more details, you can check my repository because it's lengthy, so I won't include it here.
import domElements from './utils/domElements';
import createStyledComponent from './core'
import css from './utils/css';
import {createContext} from 'react'
export const ThemeContext = createContext({})
export const ThemeProvider = ThemeContext.Provider
const styled = (tag) => {
return (...args) => createStyledComponent(tag, css(...args));
}
domElements.forEach(domElement => {
// I'm assigning this entity so that later everyone can use it like this: styled.div `... `
styled[domElement] = styled(domElement);
});
export default styled
And there you go, you can import my fake styled-components and use it as if it were a real library. 😄
Everyone can directly access the sandbox link to view and code with the demo.
Demo : https://codesandbox.io/embed/github/ducga1998/rewrite-styled-components/tree/master/
Repo : https://github.com/ducga1998/rewrite-styled-components
Don't forget to comment what you liked and didn't like about this tutorial and if there's something you know that I should have included in this article, do let me know in the comments down below.
Have a great one, bye! 👋
~ duncan
Posted on September 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 24, 2023
September 29, 2023