Recriando o Styled Components
Que tal fazer o seu próprio styled-components da forma simples?
Introdução
TL;DRSe você só quiser código: Link do Gist
Você conhece o styled-components? Se não, te apresento agora a famosa biblioteca de CSS-in-JS mais utilizada no mundo React.
Particularmente, prefiro trabalhar com CSS e JS em arquivos separados, mas isso fica pra próxima
Se você nunca utilizou styled-components, observe um exemplo que peguei da documentação oficial:
import styled, { css } from 'styled-components'
const Button = styled.a`
display: inline-block;
border-radius: 3px;
padding: 0.5rem 0;
margin: 0.5rem 1rem;
width: 11rem;
background: transparent;
color: white;
border: 2px solid white;
${props => props.primary && css`
background: white;
color: black;
`}
`;
const App = () => (
<div>
<Button
href="https://github.com/styled-components/styled-components"
target="_blank"
rel="noopener noreferrer"
primary
>
GitHub
</Button>
<Button as="a" href="/docs">Documentation</Button>
</div>
);
Incrível né? Você escreveu CSS dentro do JS e isso funcionou. Parece até magia. E é exatamente por parecer magia que estou escrevendo esse post.
Até o momento ainda não consegui entender o código todo do styled-components, mas analisando seu funcionamento em algumas páginas, é possível perceber que:
- Ao carregar a página, o styled-components insere no
<head>da aplicação uma tag<style>com alguns atributos para identificação - Insere os estilos passados no CSS-in-JS nos elementos que são filhos de styled-components
- O CSS gerado é inserido no HTML. Em algumas versões ele era inserido no
head>stylecom todo o CSS necessário, nas versões novas parece ser um texto encodado e transformado em estilo CSS
Talk is cheap, show me the code
Para podermos criar qualquer elemento HTML, sendo estes passados por parâmetros, devemos conhecer o React.createElement para criar nossos elementos de forma dinâmica, não utilizando JSX.
Como vamos simular o styled-components. Nem tudo será exatamente igual a biblioteca. Nossa função principal precisará:
- Receber a string referente ao elemento que utilizar.
Styled("div"), por exemplo. - Receber um
TemplateStringsArray, que é o template literal que utilizamos para escrever o estilo do nosso componente. - Receber as props como qualquer componente React irá receber
/*
Recebemos as props como `Props` (simulando o .attrs()) e o elemento HTML
que nosso componente irá representar
*/
function Styled<Props = unknown, Element = Html>(tag: string) {
/*
Retornamos uma função que interpreta o nosso template literal string,
as strings passadas entre crases (template string)
*/
return ([first, ...placeholders]: TemplateStringsArray, ...a: StyledArgs<Element, Props>[]) => {
/*
Retornamos uma função que de fato será nosso componente JSX, recebendo as props do elemento HTML
e as props adicionais
*/
return ({ children, ...props }: Html & Props) => {
}
}
}
Agora precisamos fazer toda a criação do elemento, inserção do CSS no header, adição de classes, concatenação de props e renderização.
- Concatenar o template literals de acordo com as funções utilizadas dentro da string. Quando utilizamos um styled-components, fazemos algo parecido com:
const Paragraph = styled.p`
color: ${props => props.color ?? "white"}
`
Utilizando nosso styled-components customizado, seria o equivalente a:
const Paragraph = Styled("p")`
color: ${props => props.color ?? "white"}
`
Para conseguirmos "executar" a concatenação de string + função, precisamos do nosso seguinte snippet abaixo:
// Reutilizando os nomes apresentados acima
const template = (placeholders: StyledArgs<Element, Props>[]) => {
const final = placeholders.reduce((acc, el, i) => {
const curr = a[i];
if (typeof curr === "function") {
// as props recebidas pelo componente.
return acc + curr(props as never) + el;
}
return acc + a[i] + el;
}, first);
return final.trim();
}
- Inserir os estilos no header para utilizar as classes. Que outra maneira de inserir elementos HTML no nosso documento se não utilizar a DOM API? Nesse ponto, iremos utilizar o React apenas para observar as mudanças ocorridas na nossa
stringe então, executar nosso efeito novamente.
useEffect(() => {
const sheet = document.createElement("style");
sheet.innerHTML = `.${className} { ${string} }`;
sheet.id = className;
const el = document.getElementById(className);
if (!el) {
document.head.insertBefore(sheet, document.head.firstElementChild);
return;
}
el?.replaceWith(sheet);
}, [string]);
- Assim como o styled-components, nós precisamos simular também a eliminação de atributos não pertencentes ao HTML, para assim passarmos para nosso componente HTML de fato. Para isso, criaremos um
computedPropspara limpar as props do nosso componente customizado.
const computedProps = useMemo(() => {
const el = document.createElement(tag);
const newProps = {};
for (const prop in el) {
if (prop in props) {
newProps[prop] = props[prop];
}
}
el.remove();
return newProps;
}, [props, str]);
Pera um pouco. Tem um hook de brinde para você utilizar na hora de compor seu
className
import React, { useState, DependencyList, useMemo } from "react";
type ClassArray = ClassValue[];
type ClassDictionary = { [id: string]: any };
export type ClassValue = string | number | ClassDictionary
| ClassArray | undefined | null | boolean;
export const useClassNames = (dependency: DependencyList, ...classes: ClassValue[]) =>
useMemo(() => classNamesDedupe(...classes), dependency);
Pronto. Agora temos tudo necessário para a construção do nosso joked-components.
import classNamesDedupe from "classnames/dedupe";
import React, { useState, DependencyList, useMemo } from "react";
type ClassArray = ClassValue[];
type ClassDictionary = { [id: string]: any };
export type ClassValue = string | number | ClassDictionary | ClassArray | undefined | null | boolean;
export const useClassNames = (dependency: DependencyList, ...classes: ClassValue[]) =>
useMemo(() => classNamesDedupe(...classes), dependency);
type Html = React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
type StyledArgs<E, T> = ((args: E & T) => string | number) | string | number;
function Styled<ExtraProps = unknown, Element = Html>(tag: string) {
return ([first, ...placeholders]: TemplateStringsArray, ...a: StyledArgs<Element, ExtraProps>[]) => {
return ({ children, ...props }: Html & ExtraProps) => {
const className = useMemo(() => `${tag}-${Date.now()}`, []);
// aplicando a demonstração do método template, citado anteriormente
const str = useMemo(() => {
const final = placeholders.reduce((acc, el, i) => {
const curr = a[i];
if (typeof curr === "function") {
return acc + curr(props as never) + el;
}
return acc + a[i] + el;
}, first);
return final.trim();
}, [props]);
// utilizando a DOM API para inserir o <style>
useEffect(() => {
const sheet = document.createElement("style");
sheet.innerHTML = `.${className} { ${str} }`;
sheet.id = className;
const el = document.getElementById(className);
if (!el) {
document.head.insertBefore(sheet, document.head.firstElementChild);
return;
}
el?.replaceWith(sheet);
}, [str]);
// composição dos classNames
const classNames = useClassNames([props.className, str], props.className, className);
// limpando as props que não pertencem ao HTML
const computedProps = useMemo(() => {
const div = document.createElement(tag);
const newProps = {};
for (const prop in div) {
if (prop in props) {
newProps[prop] = props[prop];
}
}
div.remove();
return newProps;
}, [props, str]);
return React.createElement(tag, { ...computedProps, className: classNames }, children);
};
};
}
Olhando assim, não parece tão difícil né? E pra utilizar fica bem parecido com o styled-components original:
type DIV = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
type GridProps = { gap: number; rows: number };
// Dizendo as props customizadas do nosso componente e qual o tipo do elemento HTML que o mesmo será
const GridRow = Styled<GridProps, DIV>("div")`
display: grid;
grid-template-rows: repeat(${(props) => props.rows}, minmax(0, 1fr));
grid-auto-flow: column dense;
grid-gap: ${(props) => props.gap}rem;
gap: ${(props) => props.gap}rem;
`;
const App = () => {
const [zero, setZero] = useState(0);
return (
<GridRow gap={zero} rows={4}>
<button onClick={() => setZero((p) => p + 1)}>Add + {zero}</button>{" "}
<button onClick={() => setZero((p) => p + 1)}>Add + {zero}</button>
<button onClick={() => setZero((p) => p + 1)}>Add + {zero}</button>
<button onClick={() => setZero((p) => p + 1)}>Add + {zero}</button>
<button onClick={() => setZero((p) => p + 1)}>Add + {zero}</button>
</GridRow>
);
};
Conclusão
E então, o que achou? Claro que o styled-components faz algumas melhorias de performance durante a compilação do projeto, através das macros. Mas em projetos pequenos ou para fins de estudo, vale a pena você utilizar essa versão para observar o comportamento do React de forma mais profunda.
É isso aí amiguinhos, até a próxima