You can create React styled components in 35 LOC
Dutiyesh Salunkhe
Posted on July 2, 2019
Have you ever wondered how styled component works under to hood?
Let's find out by building one.
Understanding styled components API π΅οΈβ
import styled from 'styled-components'
const Heading = styled.h1`
color: palevioletred;
`;
const App = () => {
return <Heading>styled components</Heading>
}
Based on styled component syntax we can say that styled component returns a styled
object with HTML tag named methods and uses Tagged Template literal.
Tagged Template is like calling a function.
greeting('Bruce');
// same as
greeting`Bruce`;
The only difference being how Tagged Template handles its arguments, where the first argument contains an array of string values.
// logging function arguments
logArguments('Bruce');
// -> Bruce
logArguments`Bruce`;
// -> ["Bruce"]
Styled component Phases π
We will divide Styled component into 2 phases:
Phase 1: Creation Phase
In Creation Phase we invoke a styled component's tag named method like - h1
, which returns a Functional React Component.
// App.js
const Heading = styled.h1`
color: palevioletred;
`; // βοΈ Creation Phase
// styled-components.js
function h1(styleLiteral) {
return () => { // βοΈ Function component
return <h1></h1>
}
}
Phase 2: Rendering Phase
In Rendering Phase, we render the Function component created in Phase 1.
const Heading = styled.h1`
color: palevioletred;
`;
const App = () => {
return <Heading /> // βοΈ Rendering Phase
}
Approaching towards "Style" part of Styled component π
In Creation phase we passed style to h1
function, but how can we apply it to our component without inlining it? π€
We will use a class selector and assign a random name.
const className = `sc-${Math.random().toString(16).substr(2, 6)}`;
// Generate class names like - sc-79a268, sc-56d898
Now we will create a function to apply style to our class and append it in our page by creating a new style
tag if not present.
And to uniquely identify it from other style
tags, we will assign an id
of 'sc-style'
, so that we can use the same tag to append styles for other styled components.
function appendStyle(className, style) {
let styleTag = document.getElementById('sc-style');
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', 'sc-style');
document.getElementsByTagName('head')[0].appendChild(styleTag);
}
styleTag.appendChild(document.createTextNode(`.${className} { ${style} }`))
}
Combining above two steps, we get:
function h1(styleLiterals) {
return () => {
const className = `sc-${Math.random().toString(16).substr(2, 6)}`;
appendStyle(className, styleLiterals[0]); // pass first item at index 0
return <h1 className={className}></h1>
}
}
function appendStyle(className, style) {
let styleTag = document.getElementById('sc-style');
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', 'sc-style');
document.getElementsByTagName('head')[0].appendChild(styleTag);
}
styleTag.appendChild(document.createTextNode(`.${className} { ${style} }`))
}
Passing text to display inside our Styled component βοΈ
In Rendering Phase we can pass data as children to our component and use props.children
to render inside it.
// App.js
const App = () => {
return <Heading>styled components</Heading> // Rendering phase
}
// styled-components.js
function h1(styleLiterals) {
return (props) => { // βοΈ props from parent component
return <h1>{props.children}</h1>
}
}
We created Styled component π
// App.js
import styled from 'styled-components';
const Heading = styled.h1`
color: palevioletred;
`;
const App = () => {
return <Heading>styled components</Heading>
}
// styled-components.js
function h1(styleLiterals) {
return (props) => {
const className = `sc-${Math.random().toString(16).substr(2, 6)}`;
appendStyle(className, styleLiterals[0]);
return <h1 className={className}>{props.children}</h1>
}
}
function appendStyle(className, style) {
let styleTag = document.getElementById('sc-style');
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', 'sc-style');
document.getElementsByTagName('head')[0].appendChild(styleTag);
}
styleTag.appendChild(document.createTextNode(`.${className} { ${style} }`))
}
const styled = {
h1
}
export default styled;
Customizing Styled components with props π¨
Let's customize our component by passing a color
prop to render text in different colors.
const Heading = styled.h1`
color: ${(props) => ${props.color}}; // Apply color from props
`;
const App = () => {
return <Heading color="palevioletred">styled components</Heading>
}
If you notice above, we have an interpolation in our template literal.
So what happens to a function when we pass template literals with interpolations?
const username = 'Bruce';
greeting`Hello ${username}!`;
// -> ["Hello ", "!"] "Bruce"
Function will receive 2 arguments here, first will still be an array.
And second argument will be the interpolated content 'Bruce'
.
Update styled component to receive interpolation content π
function h1(styleLiterals, propInterpolation) {
return () => {
return <h1></h1>
}
}
As there can be an indefinite number of interpolation arguments, we will use the rest parameter to represent them as an array.
Our function now becomes:
function h1(styleLiterals, ...propsInterpolations) { // βοΈ with rest parameter
return () => {
return <h1></h1>
}
}
Generate style with interpolation π©βπ¨
Our function now receives 2 arguments - stringLiterals
and propsInterpolations
, we have to merge them to generate style.
For this, we will create a function that iterates over each item from both arrays and concatenates them one by one.
function getStyle(styleLiterals, propsInterpolations, props) {
return styleLiterals.reduce((style, currentStyleLiteral, index) => {
let interpolation = propsInterpolations[index] || '';
if (typeof interpolation === 'function') { // execute functional prop
interpolation = interpolation(props);
}
return `${style}${currentStyleLiteral}${interpolation}`;
}, '');
}
Using getStyle
function in our styled component:
function h1(styleLiterals, ...propsInterpolations) {
return (props) => {
const className = `sc-${Math.random().toString(16).substr(2, 6)}`;
const style = getStyle(styleLiterals, propsInterpolations, props); // pass required parameters to generate style
appendStyle(className, style);
return <h1 className={className}>{props.children}</h1>
}
}
Optimization time β‘οΈ
Have you noticed what happens when we render 2 styled component with same styling?
const Heading = styled.h1`
color: palevioletred;
`;
const App = () => {
return (
<React.Fragment>
<Heading>styled components</Heading>
<Heading>styled components</Heading>
</React.Fragment>
)
}
2 classes get generated even though their styles are the same.
To reduce the duplicate code, we will use JavaScript's Map
object to hold our styles with their class names in key-value pairs.
function h1(styleLiterals, ...propsInterpolations) {
const styleMap = new Map(); // maintain a map of `style-className` pairs
return (props) => {
let className = '';
const style = getStyle(styleLiterals, propsInterpolations, props);
if (!styleMap.has(style)) { // check whether style is already present
className = `sc-${Math.random().toString(16).substr(2, 6)}`;
appendStyle(className, style);
styleMap.set(style, className); // store class for a style in Map
} else {
className = styleMap.get(style); // reuse class for a style
}
return <h1 className={className}>{props.children}</h1>
}
}
End result β¨β¨
function h1(styleLiterals, ...propsInterpolations) {
const styleMap = new Map(); // maintain a map of `style-className` pairs
return (props) => {
let className = '';
const style = getStyle(styleLiterals, propsInterpolations, props);
if (!styleMap.has(style)) { // check whether style is already present
className = `sc-${Math.random().toString(16).substr(2, 6)}`;
appendStyle(className, style);
styleMap.set(style, className); // store class for a style in Map
} else {
className = styleMap.get(style); // reuse class for a style
}
return <h1 className={className}>{props.children}</h1>
}
}
function getStyle(styleLiterals, propsInterpolations, props) {
return styleLiterals.reduce((style, currentStyleLiteral, index) => {
let interpolation = propsInterpolations[index] || '';
if (typeof interpolation === 'function') { // execute functional prop
interpolation = interpolation(props);
}
return `${style}${currentStyleLiteral}${interpolation}`;
}, '');
}
function appendStyle(className, style) {
let styleTag = document.getElementById('sc-style');
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', 'sc-style');
document.getElementsByTagName('head')[0].appendChild(styleTag);
}
styleTag.appendChild(document.createTextNode(`.${className} { ${style} }`))
}
const styled = {
h1
}
export default styled;
Posted on July 2, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.