You can create React styled components in 35 LOC
Tags: javascript, webdev, react, css
Published: 2019-07-02 12:25:59

Have you ever wondered how styled component works under to hood?
Let's find out by building one.

## Understanding styled components API 🕵️‍

```jsx
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.

```javascript
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.

```javascript
// 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.

```jsx
// 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.

```jsx
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.

```javascript
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.

```jsx
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:

```jsx
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.

```jsx
// 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 💅

```jsx
// 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.

```jsx
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?

```javascript
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 📑

```jsx
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:

```jsx
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.

```javascript
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:

```jsx
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?

```jsx
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.

```jsx
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 ✨✨

```jsx
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;
```

{% codesandbox k7119 %}