loading...
Cover image for You can create React styled components in 35 LOC

You can create React styled components in 35 LOC

dutiyesh profile image Dutiyesh Salunkhe Updated on ・5 min read

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 Jul 2 '19 by:

Discussion

markdown guide