You can create React styled components in 35 LOC

dutiyesh

Dutiyesh Salunkhe

Posted on July 2, 2019

You can create React styled components in 35 LOC

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>
}
Enter fullscreen mode Exit fullscreen mode

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`;
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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} }`))
}
Enter fullscreen mode Exit fullscreen mode

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} }`))
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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}`;
    }, '');
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

πŸ’– πŸ’ͺ πŸ™… 🚩
dutiyesh
Dutiyesh Salunkhe

Posted on July 2, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Change navbar color on mouse scroll in React
Weekly Digest 32/2022
javascript Weekly Digest 32/2022

August 14, 2022