Pressione enter para ver os resultados ou esc para cancelar.

Como funciona o Styled-components por debaixo dos panos

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

Live demo

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;'
]);

Live demo

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

Live demo

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

Live Demo

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:

  1. this.props.className – opcional passado pelo componente pai.
  2. 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.
  1. 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();

 Live version

Toda vez que você clicar no button, o componentWillReceiveProps() é chamado sizeValue prop incrementado e executa as mesmas ações que o componentDidMount():

  1. Avalie o tagged template.
  2. Gere o novo nome da classe CSS.
  3. Pré-processe os styles com stylis.
  4. 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!