O CSS-in-JS está se tornando cada vez mais comum no desenvolvimento moderno de front-end, especialmente na comunidade React. O styled-components se destaca na lista porque ele adota tagged templates e permite criar componentes React normais definindo apenas estilos. Ele também resolve problemas importantes, como modularidade CSS, fornece recursos non-CSS, como aninhamento, e todos esses recursos fornecidos com configuração zero. Os desenvolvedores não precisam pensar em nomes únicos para classes CSS, nem precisam pensar em classes. Mas como esse poder é alcançado?
Note: Se você não estiver familiarizado com styled-components, leia primeiro a documentação oficial.
Syntaxe Mágica
Vamos criar um botão simples com estilos estáticos usando styled-components:
const Button = styled.button` color: coral; padding: 0.25rem 1rem; border: solid 2px coral; border-radius: 3px; margin: 0.5rem; font-size: 1rem; `;
Aqui styled.button é apenas um atalho para o styled(‘button’) e uma das muitas funções criadas dinamicamente a partir da lista de elementos html disponíveis. Se você está familiarizado com tagged templates, sabe que esse button é apenas uma função e pode ser chamada com uma string simples como parâmetro do array. Vamos debugar este código:
const Button = styled('button')([ 'color: coral;' + 'padding: 0.25rem 1rem;' + 'border: solid 2px coral;' + 'border-radius: 3px;' + 'margin: 0.5rem;' + 'font-size: 1rem;' ]);
Agora, como você pode ver, o styled é apenas um component factory e podemos imaginar como sua implementação poderia ser.
Reinventar styled-components
const myStyled = (TargetComponent) => ([style]) => class extends React.Component { componentDidMount() { this.element.setAttribute('style', style); } render() { return ( this.element = element } /> ); } }; const Button = myStyled('button')` color: coral; padding: 0.25rem 1rem; border: solid 2px coral; border-radius: 3px; margin: 0.5rem; font-size: 1rem; `;
Essa implementação é muito fácil – a factory cria um novo componente baseado em uma tag name salva na closure e define inline styles após a montagem. Mas e se nosso componente tiver estilos baseado nas props?
const primaryColor = 'coral'; const Button = styled('button')` background: ${({ primary }) => primary ? 'white ' : primaryColor}; color: ${({ primary }) => primary ? primaryColor : 'white'}; padding: 0.25rem 1rem; border: solid 2px ${primaryColor}; border-radius: 3px; margin: 0.5rem; `;
Nós precisamos atualizar nossa implementação para avaliar as interpolations nos estilos quando um componente é montado ou as props são atualizadas.
const myStyled = (TargetComponent) => (strs, ...exprs) => class extends React.Component { interpolateStyle() { const style = exprs.reduce((result, expr, index) => { const isFunc = typeof expr === 'function'; const value = isFunc ? expr(this.props) : expr; return result + value + strs[index + 1]; }, strs[0]); this.element.setAttribute('style', style); } componentDidMount() { this.interpolateStyle(); } componentDidUpdate() { this.interpolateStyle(); } render() { return this.element = element } /> } }; const primaryColor = 'coral'; const Button = myStyled('button')` background: ${({ primary }) => primary ? primaryColor : 'white'}; color: ${({ primary }) => primary ? 'white' : primaryColor}; padding: 0.25rem 1rem; border: solid 2px ${primaryColor}; border-radius: 3px; margin: 0.5rem; font-size: 1rem; `;
A parte mais complicada é obtermos o style string:
const style = exprs.reduce((result, expr, index) => { const isFunc = typeof expr === 'function'; const value = isFunc ? expr(this.props) : expr; return result + value + strs[index + 1]; }, strs[0]);
Concatenamos todos os pedaços de string com o resultado das expressões, uma por uma, e se uma expressão é uma function, ela é chamada com as propriedades passadas no componente.
A API desta simples factory é semelhante à que o styled-components fornece, mas a implementação original é mais interessante por debaixo dos panos: ela não usa inline styles. Vamos ver mais de perto o que acontece quando você importa o styled-components e cria um componente.
styled-components por debaixo dos panos
Import styled-components
Quando você importa a biblioteca pela primeira vez em seu app, ela cria variável de contagem interna counter para contar todos os componentes criados por meio da styled factory.
Chamada styled.tag-name factory
const Button = styled.button` font-size: ${({ sizeValue }) => sizeValue + 'px'}; color: coral; padding: 0.25rem 1rem; border: solid 2px coral; border-radius: 3px; margin: 0.5rem; &:hover { background-color: bisque; } `;
Quando styled-components cria um novo componente, ele também cria o identificador interno componentId. Aqui está como o identificador foi computado.
counter++; const componentId = 'sc-' + hash('sc' + counter);
O componentId para o primeiro componente em um app é sc-bdVaJa.
Atualmente styled-components usa o algoritmo MurmurHash para criar o identificador único e, em seguida, converte o número de hash para o nome alfabético.
Assim que o identificador é criado, o styled-components insere um novo elemento
Quando o novo componente é criado, a target component é passado para o target factory ( no nosso caso ‘button’ ) e o componente é salvo nos campos estáticos:
StyledComponent.componentId = componentId; StyledComponent.target = TargetComponent;
Como você pode ver, não sobrecarga de desempenho quando você cria um styled component. Mesmo se você definir centenas de componentes e não usá-los, tudo que você tem é um ou mais elementos
Como você pode ver o styled-components também injeta componentId (.sc-bdVaja) como uma classe CSS sem nenhuma regra.
render()
Como terminou com CSS, agora o styled-components precisa apenas criar um elemento com className correspondente:
const TargetComponent = this.constructor.target; // In our case just 'button' string. const componentId = this.constructor.componentId; const generatedClassName = this.state.generatedClassName; return ( );
Styled-components renderiza um elemento com 3 nomes de classe:
- this.props.className – opcional passado pelo componente pai.
- componentId – Identificador único de um componente, mas não de uma instância de um componente. Esta classe não possui regra CSS, mas é usada em selectors quando é necessário fazer referência a outro componente.
- generatedClassName – única para cada instância do componente que possui regras CSS reais.
É isso! O elemento HTML renderizado no final é:
< button class="sc-bdVaJa jsZVzX">I'm a button
componentWillReceiveProps()
Agora vamos tentar mudar nosso button props quando ele estiver montando. Para fazer isso, precisamos criar um exemplo mais interativo para nosso button.
let sizeValue = 24;
const updateButton = () => {
ReactDOM.render(
< button>
Font size is {sizeValue}px
< /button>,
document.getElementById('root')
);
sizeValue++;
}
updateButton();
Toda vez que você clicar no button, o componentWillReceiveProps() é chamado sizeValue prop incrementado e executa as mesmas ações que o componentDidMount():
- Avalie o tagged template.
- Gere o novo nome da classe CSS.
- Pré-processe os styles com stylis.
- Injetar o CSS pré-processado na página.
Se você verificar os styles gerados nas ferramentas de desenvolvimento (dev tools), depois de alguns cliques, verá:
Sim, a única diferença para cada classe CSS é a propriedade font-size e as classe CSS não usadas, não são removidas. Mas por que ? Só porque removê-los, adiciona sobrecarga de desempenho, mantendo não faz (Veja os comentários de Max’s Storiber sobre isso).
Há uma pequena otimização aqui: componentes sem interpolações na string style sao marcado como isStatic e esta flag é marcado em componentWillReceiveProps() para pular cálculos desnecessários do mesmo styles.
Dicas de desempenho
Sabendo como styled-components funcionam sob o capuz, podemos nos concentrar melhor no desempenho.
Há um ovo de páscoa no exemplo com o botão (Dica: Tente clicar no botão mais de 200 vezes e você verá a mensagem oculta do styled-components no console. Sem brincadeira 😉 ).
Se você está muito ansioso, aqui está a mensagem:
Mais de 200 classes foram geradas para o componente styled.button. Considere o uso do método attrs, junto com um objeto de estilo para styles alterado.
Exemplo:
const Component = styled.div.attrs({ style: ({ background }) => ({ background, }), })`width: 100%;` < Component />
Aqui está como o Button aparece após a refatoração:
const Button = styled.button.attrs({ style: ({ sizeValue }) => ({ fontSize: sizeValue + 'px' }) })` color: coral; padding: 0.25rem 1rem; border: solid 2px coral; border-radius: 3px; margin: 0.5rem; &:hover { background-color: bisque; } `;
Mas você deveria usar essa técnica para todos os seus estilos dinâmicos? Não. Mas minha regra pessoal é usar o style attr para todos os estilos dinâmicos com o número de resultados prejudicado. Por exemplo, se você tiver um componente com font-size personalizável como uma nuvem de palavras uma lista de tags carregadas de um servidor com cores diferentes, é melhor usar o style attr. Mas se você tiver vários botões como default, primary, warn e etc. Em um componente, não há problema em usar interpolação com condições na string styles.
Nos exemplos abaixo eu uso a versão de desenvolvimento, mas em produção o bundle você deve sempre usar a compilação de produção do styled-components porque é mais rápido. Assim como no React, build de produção do styled-components desabilita muitos dev warning mas, o mais importante é usar CSSStyleSheet.insertRule() para injetar styles gerados em uma página enquanto a versão de desenvolvimento usa Node.appendChild() (Aqui Evan Scott mostra como insertRule é realmente rápido).
Considere também usar babel-plugin-styled-component Pode minify ou até processar styles antes de carregar.
Conclusão
O fluxo de trabalho do styled-component é muito simples, cria o CSS necessário antes dos componentes renderizarem e é rápido suficiente, apesar de avaliar as strings marcadas e pré-processar o CSS diretamente no browser (navegador)
Este artigo não cobre todos os aspectos do styled-components, mas tentei concentrar nos principais.
Para escrever esse post usei o styled-components v3.3.3. Desde que o artigo é sobre under-hood muitos deles podem mudar nas versões futuras.
O post original se encontra here!