Overreacted

Como o setState sabe o que fazer?

9 de dezembro de 2018 • ☕️☕️ 10 min read

Translated by readers into: EspañolFrançaisPortuguês do BrasilTürkçe日本語简体中文한국어

Read the originalImprove this translationView all translated posts

Quando você chama setState dentro de um componente, o que você pensa que acontece?

import React from 'react';
import ReactDOM from 'react-dom';

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { clicked: false };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState({ clicked: true });  }
  render() {
    if (this.state.clicked) {
      return <h1>Thanks</h1>;
    }
    return (
      <button onClick={this.handleClick}>
        Click me!
      </button>
    );
  }
}

ReactDOM.render(<Button />, document.getElementById('container'));

Claro, React re-renderiza o componente com o próximo estado { clicked: true } e atualiza o DOM para corresponder o elemento <h1>Thanks</h1>.

Parece simples. Mas espere, React faz isso? Ou React DOM?

Atualizando o DOM parece algo de responsabilidade do React DOM. Mas nós estamos chamando this.setState() que não é algo oriundo do React DOM. E nossa classe base React.Component é definida dentro do próprio React.

Então como pode setState() dentro do React.Component atualizar o DOM?

Aviso: Assim como a maioria das outras postagens neste blog, você não precisa saber nada disso para ser produtivo com o React. Esse post é para quem gosta de ver o que está por trás da cortina. Completamente opcional!


Podemos pensar que a classe React.Component contém a lógica de atualização do DOM.

Mas se fosse esse o caso, como pode this.setState() funcionar em outros ambientes? Por exemplo, os componentes nos aplicativos React Native também estendem o React.Component. Eles chamam de this.setState() exatamente como fizemos acima, e ainda o React Native funciona com views nativas do Android e iOS em vez do DOM.

Você também pode estar familiarizado com o React Test Renderer ou Shallow Renderer. Ambas as estratégias de teste permitem renderizar componentes normais e chamar this.setState() dentro deles. Mas nenhum deles trabalha com o DOM.

Se você usou renderizadores como React Art, você também pode saber que é possivél usar mais de um renderizadores na página. (Por exemplo, Art components trabalham dentro da árvore do React DOM). Isso faz com que um sinalizador global ou variável seja insustentável.

Então de alguma forma React.Component delega a manipulação de atualização de estado para a plataforma específica do código. Depois nós podemos entender como isso acontece, vamos nos aprofundar em como os pacotes são separados e por quê.


Há um equívoco comum de que o “motor” do React vive dentro do package do React. E isso não é uma verdade.

Um fato, todo desde a separação no React 0.14, o pacote do react intencionalmente expõem apenas APIs para definir componentes. A maioria das implementações do React vive dentro dos “renderizadores”.

react-dom, react-dom/server, react-native, react-test-renderer, react-art são alguns dos exmplos de renderizadores (e você pode construir seu próprio).

É por isso que o pacote do react é útil independente de qual plataforma você segmentar. Todos suas exportações, como os React.Component, React.createElement, React.Children e (eventualmente) Hooks, são independentes de uma plataforma específica. Se você executar React DOM, React DOM Server, ou React Native, seus componente importariam e usariam eles da mesma forma.

Em contraste, os pacotes de renderizadores expõem APIs específicas da plataforma, como ReactDOM.render(), que permitem montar uma hierarquia React em um nó do DOM. Cada renderizador fornece uma API como essa. Idealmente, a maioria dos componentes não precisam importar nada de um renderizador. Isso os mantém mais portáteis.

O que a maioria das pessoas imaginam é como o “motor” do React está dentro de cada renderizador individual. Muitos renderizadores incluem uma cópia do mesmo código — nós o chamamos de “reconciliador”. Uma etapa de compilação suaviza o código do reconciliador junto com o código do renderizador em um único bundle altamente otimizado para melhor desempenho. (O código copiado geralmente não é bom para o tamanho do bundle, mas a grande maioria dos usuários do React precisa apenas de um renderizador por vez, como react-dom.)

A conclusão aqui é que o pacote react permite que você use os recursos do React, mas não sabe nada sobre como eles são implementados. Os pacotes renderizadores (react-dom, react-native, etc) fornecem a implementação dos recursos do React e da lógica específica da plataforma. Parte desse código é compartilhada (“reconciliador”), mas isso é um detalhe individual de implementação dos renderizadores .


Agora nós sabemos porque ambos pacotes react e react-dom precisam estar atualizados para novos recursos. Por exemplo, quando React 16.3 adicionou Context API, React.createContext() foi exposto no pacote do React.

Mas na realidade o recurso do contenxt React.createContext() não foi implementado. Por exemplo, a implementação precisaria ser diferente entre React DOM e React DOM Server. Então createContext() retorna um objeto simples:

// A bit simplified
function createContext(defaultValue) {
  let context = {
    _currentValue: defaultValue,
    Provider: null,
    Consumer: null
  };
  context.Provider = {
    $$typeof: Symbol.for('react.provider'),
    _context: context
  };
  context.Consumer = {
    $$typeof: Symbol.for('react.context'),
    _context: context,
  };
  return context;
}

Quando você usa <MyContext.Provider> ou <MyContext.Consumer> no código, o renderizador que decide como lidar com eles. React DOM pode rastrear valores de context de uma maneira, mas o React DOM Server pode fazer isso de maneira diferente.

Então se você atualizar o react para 16.3+, mas não atualizar o react-dom, você estaria usando um renderizador que ainda não reconhece os tipos especial Provider e Consumer. Isso é o porque um aviso do react-dom aparecerá dizendo que esses tipos são invalidos.

O mesmo embargo se aplica para o React Native. Contudo, ao contrário do React DOM, uma nova versão do React não “força” imediatamente uma nova versão do React Native. Eles tem um calendário de lançamentos independentes. O código do renderizador atualizado é sincronizado separadamente no repositório do React Native em algumas semanas posterior. É por isso que os recursos ficam disponíveis no React Native em um cronograma diferente do que no React DOM.


Ok, então agora nós sabemos que o pacote do react não contém nada interessante, e que a implementação vive dentro dos renderizadores como react-dom, react-native, e assim por diante. Mas isso não responde nossa questão. Como o setState() dentro do React.Component “conversa” como renderizador certo?

A resposta é que cada renderizador seta um campo especial quando a classe foi criada. Esse campo é chamado de updater. Isso não é algo que você definiria — em vez disso, isso é algo que o React DOM, React DOM Server ou React Native é setado depois de ter criado uma instância de uma classe:

// Inside React DOM
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;
// Inside React DOM Server
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;
// Inside React Native
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;

Olhando para a implementação do setState dentro do React.Component, todo trabalho é delego para o renderizador que criou essa instância do componente:

// A bit simplified
setState(partialState, callback) {
  // Use the `updater` field to talk back to the renderer!
  this.updater.enqueueSetState(this, partialState, callback);
}

React DOM Server pode querer ignorar a atualização do state e avisar você, enquanto React DOM e React Native deixariam suas cópias do reconciliador lidar com isso.

E assim é como this.setState() pode atualizar o DOM mesmo que esteja definido no pacote React. Ele lê this.updater, que foi definido pelo React DOM, e permite que o React DOM agende e cuide da atualização


Nós agora sabemos sobre classes, mas o que acontecer com Hooks?

Quando as pessoas dão suas primeiras olhadas para a proposta da API do Hooks, elas muitas vezes se perguntam: como o useState “ sabe o que fazer “? A suposição é de que é algo mais “mágico” do que uma classe React.Component com this.setState().

Mas nós vimos hoje, a classe que implementa o setState() pode parecer uma ilusão. Ela não faz nada, exceto encaminhar a chamada para o renderizador atual. E useState Hook faz exatamente a mesma coisa.

Em vez de um campo updater, os Hooks usam um objeto “dispatcher”. Quando você chama React.useState(), React.useEffect(), ou outro Hook interno, essas chamadas são encaminhadas para o dispatcher atual.

// In React (simplified a bit)
const React = {
  // Real property is hidden a bit deeper, see if you can find it!
  __currentDispatcher: null,

  useState(initialState) {
    return React.__currentDispatcher.useState(initialState);
  },

  useEffect(initialState) {
    return React.__currentDispatcher.useEffect(initialState);
  },
  // ...
};

E renderizadores individuais setão o dispatcher antes de renderizar seu componente:

// In React DOM
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;let result;
try {
  result = YourComponent(props);
} finally {
  // Restore it back  React.__currentDispatcher = prevDispatcher;}

Por exemplo, a implementação do React DOM Server está aqui, e a implementação do reconciliador compartilhada pelo React DOM e React Native está aqui.

É por isso que um renderizador como react-dom precisa acessar o mesmo pacote react que você chama de Hooks. Caso contrário, seu componente não “verá” o expedidor! Isso pode não funcionar quando você tem várias cópias do React na mesma árvore de componentes. No entanto, isso sempre levou a bugs obscuros, fazendo com que os Hooks o obrigassem a resolver a duplicação de pacotes antes que isso lhe custasse caro.

Embora não encorajamos isso, você pode substituir tecnicamente o dispatcher para casos avançados de uso de ferramentas. (Eu menti sobre o nome __currentDispatcher mas você pode encontrar o nome real no repositório React). Por exemplo, o React DevTools usará um dispatcher especial para introspecção da árvore de Hooks e capturando rastreamentos de pilha JavaScript. Não repita isso em casa.

Isso também significa que os Hooks não estão inerentemente ligados ao React. Se, no futuro, mais bibliotecas quiserem reutilizar o Hooks, em teoria, o despachante pode passar para um pacote separado e ser exposto como uma API de primeira classe com um nome menos “assustador”. Na prática, preferimos evitar a abstração prematura até que seja necessário.

Tanto o campo updater quanto o objeto __currentDispatcher são formas de um princípio genérico de programação chamado injeção de dependência. Em ambos os casos, os renderizadores “injetam” implementações de recursos como ‘setState’ no pacote genérico React para manter seus componentes mais declarativos.

Você não precisa pensar em como isso funciona quando você for programar com React. Gostaríamos que os usuários do React passassem mais tempo pensando no código do aplicativo do que conceitos abstratos, como a injeção de dependência. Mas se você já se perguntou como this.setState() ou useState() sabe o que fazer, espero que isso ajude.