Imediatamente um pequeno spoiler - organizar um estado no mobx não é diferente de organizar um estado geral sem usar o mobx em uma reação pura. A resposta para a pergunta natural é por que, então, esse mobx é necessário? Você o encontrará no final do artigo.Enquanto isso, o artigo se concentrará na organização do estado em um aplicativo de reação limpa, sem bibliotecas externas.

A reação fornece uma maneira de armazenar e atualizar o estado dos componentes usando a propriedade state em uma instância de um componente de classe e o método setState. No entanto, entre a comunidade de reagentes, são usadas várias bibliotecas e abordagens adicionais para trabalhar com o estado (fluxo, redux, reduxações, efetoras, mobx, cerebral, várias delas). Mas é possível construir um aplicativo suficientemente grande com um monte de lógica de negócios com um grande número de entidades e relacionamentos de dados complexos entre componentes usando apenas o setState? Existe a necessidade de bibliotecas adicionais para trabalhar com o estado? Vamos descobrir.
Portanto, definimos setState e atualiza o estado e chama o representante do componente. Mas e se os mesmos dados forem exigidos por muitos componentes que não estão interconectados? Na estação oficial de reação, há uma seção "levantando o estado" com uma descrição detalhada - simplesmente elevamos o estado ao ancestral comum a esses componentes, passando por dados e funções de adereços (e componentes intermediários, se necessário) para alterá-lo. Para pequenos exemplos, isso parece razoável, mas a realidade é que, em aplicativos complexos, pode haver muitas dependências entre componentes e a tendência de transferir estados para um componente comum do ancestral leva ao fato de que todo o estado será cada vez mais alto e terminará no componente raiz do aplicativo, juntamente com a lógica para atualizar esse estado para todos os componentes. Como resultado, setState ocorrerá apenas para atualizar o componente de dados local ou no componente raiz do aplicativo, no qual toda a lógica estará concentrada.
Mas é possível armazenar processo e renderizar estado em um aplicativo de reação sem usar o setState ou quaisquer bibliotecas adicionais e fornecer acesso geral a esses dados a partir de qualquer componente?
Os objetos javascript mais comuns e certas regras para organizá-los vêm em nosso auxílio.
Mas primeiro você precisa aprender como decompor aplicativos em tipos de entidade e seus relacionamentos.
Para começar, apresentamos um objeto que armazena dados globais que se aplicam a todo o aplicativo como um todo - (podem ser as configurações de estilos, localização, tamanhos de janelas etc.) em um único objeto do AppState e apenas o colocamos em um arquivo separado.
// src/stores/AppState.js export const AppState = { locale: "en", theme: "...", .... }
Agora, em qualquer componente, você pode importar e usar os dados de nossa loja.
import AppState from "../stores/AppState.js" const SomeComponent = ()=> ( <div> {AppState.locale === "..." ? ... : ...} </div> )
Vamos mais longe - quase todos os aplicativos têm a essência do usuário atual (não importa como ele é criado ou proveniente do servidor etc.), portanto, o objeto único do nosso usuário também estará no estado do aplicativo. Também pode ser movido para um arquivo separado e também importado, ou pode ser armazenado imediatamente dentro do objeto AppState. E agora a principal coisa - você precisa determinar o diagrama das entidades que compõem o aplicativo. Em termos de banco de dados, serão tabelas com relacionamentos um para muitos ou muitos para muitos e toda essa cadeia de relacionamentos começa na essência principal do usuário. Bem, no nosso caso, o objeto do usuário simplesmente armazena uma variedade de outros objetos-entidades-lojas, onde cada loja de objetos, por sua vez, armazena matrizes de outras lojas de entidades.
Aqui está um exemplo - existe uma lógica comercial que é expressa como "o usuário pode criar / editar / excluir pastas, projetos em cada pasta, em cada projeto de tarefa e em cada tarefa de subtarefa" (parece algo como um gerenciador de tarefas) e aparecerá no diagrama de estado algo como isto:
export const AppStore = { locale: "en", theme: "...", currentUser: { name: "...", email: "" folders: [ { name: "folder1", projects: [ { name: "project1", tasks: [ { text: "task1", subtasks: [ {text: "subtask1"}, .... ] }, .... ] }, ..... ] }, ..... ] } }
Agora, o componente raiz do aplicativo pode simplesmente importar esse objeto e renderizar algumas informações sobre o usuário e, em seguida, transferir o objeto do usuário para o componente do painel
.... <Dashboard user={appState.user}/> ....
e ele pode renderizar a lista de pastas
... <div>{user.folders.map(folder=><Folder folder={folder}/>)}</div> ...
e cada componente da pasta exibirá uma lista de projetos
.... <div>{folder.projects.map(project=><Project project={project}/>)}</div> ....
e cada componente do projeto pode listar tarefas
.... <div>{project.tasks.map(task=><Task task={task}/>)}</div> ....
e, finalmente, cada componente da tarefa pode renderizar uma lista de subtarefas passando o objeto desejado para o componente da subtarefa
.... <div>{task.subtask.map(subtask=><Subtask subtask={subtask}/>)}</div> ....
Naturalmente, em uma página, ninguém exibirá todas as tarefas de todos os projetos de todas as pastas, elas serão divididas por painéis laterais (por exemplo, uma lista de pastas), por páginas, etc., mas a estrutura geral é aproximadamente a mesma - o componente pai faz com que o componente incorporado passe um objeto com adereços dados. Um ponto importante deve ser observado - qualquer objeto (por exemplo, um objeto de uma pasta, projeto, tarefa) não é armazenado dentro do estado de qualquer componente - o componente simplesmente o recebe por meio de adereços como parte de um objeto mais geral. E, por exemplo, quando o componente do projeto passa o objeto da tarefa ( <div>{project.tasks.map(task=><Task task={task}/>)}</div>
) para o componente filho da Task, devido ao fato de os objetos serem armazenados em um único objeto você sempre pode alterar esse objeto de tarefa de fora - por exemplo, AppState.currentUser.folders [2] .projects [3] .tasks [4] .text = "tarefa editada" e fazer com que o componente raiz seja atualizado (ReactDOM.render (<App /> ) e, dessa maneira, obtemos o estado atual do aplicativo.
Suponha ainda que desejemos criar uma nova subtarefa ao clicar no botão "+" no componente Tarefa. Tudo é simples
onClick = ()=>{ this.props.task.subtasks.push({text: ""}); updateDOM() }
como o componente Tarefa recebe como suporte o objeto de tarefa e esse objeto não é armazenado dentro de seu estado, mas faz parte do armazenamento global do AppState (ou seja, o objeto de tarefa é armazenado na matriz de tarefas do objeto de projeto mais geral e que, por sua vez, faz parte do objeto de usuário e o usuário já está armazenado no AppState ) e, graças a essa conectividade, após adicionar um novo objeto de tarefa à matriz de subtarefas, você pode chamar a atualização do componente raiz e, assim, atualizar e atualizar a casa para todas as alterações de dados (não importa onde elas tenham acontecido) simplesmente chamando a função upd ateDOM, que por sua vez simplesmente atualiza o componente raiz.
export function updateDOM(){ ReactDom.render(<App/>, rootElement); }
E não importa quais dados de quais partes do AppState e de quais lugares alteramos (por exemplo, você pode encaminhar um objeto de pasta através de adereços através dos componentes intermediários de Projeto e Tarefa para o componente Subtarefa e pode apenas atualizar o nome da pasta (this.props.folder.name = "new name ") - devido ao fato de os componentes receberem dados por meio de adereços, a atualização do componente raiz atualizará todos os componentes aninhados e o aplicativo inteiro.
Agora vamos tentar adicionar alguma comodidade ao trabalho com o lado. No exemplo acima, você pode observar que a criação de um novo objeto de entidade sempre (por exemplo, project.tasks.push({text: "", subtasks: [], ...})
se o objeto tiver muitas propriedades com parâmetros padrão, sempre para listá-los e você pode cometer um erro e esquecer algo, etc. A primeira coisa que vem à mente é colocar a criação de um objeto em uma função na qual os campos padrão serão atribuídos e, ao mesmo tempo, redefini-los com novos dados.
function createTask(data){ return { text: "", subtasks: [], ... //many default fields ...data } }
mas se você olhar do outro lado, essa função é o construtor de uma determinada entidade e as classes javascript são ótimas para essa função
class Task { text: ""; subtasks: []; constructor(data){ Object.assign(this, data) } }
e criar o objeto simplesmente criará uma instância da classe com a capacidade de substituir alguns campos padrão
onAddTask = ()=>{ this.props.project.tasks.push(new Task({...}) }
Além disso, você pode perceber que, da mesma maneira, criando classes para objetos de projeto, usuários, subtarefas, obtemos duplicação de código dentro do construtor
constructor()
mas podemos tirar proveito da herança e colocar esse código no construtor da classe base.
class BaseStore { constructor(data){ Object.update(this, data); } }
Além disso, você notará que toda vez que atualizamos algum estado, alteramos manualmente os campos do objeto
user.firstName = "..."; user.lastName = "..."; updateDOM();
e torna-se difícil rastrear, negociar e entender o que está acontecendo no componente e, portanto, é necessário determinar um canal comum através do qual as atualizações de qualquer dado serão processadas e, em seguida, podemos adicionar registros e todo tipo de outras comodidades. Para fazer isso, a solução é criar um método de atualização na classe que pegue um objeto temporário com novos dados e se atualize e estabeleça a regra de que os objetos podem ser atualizados apenas através do método de atualização e não por atribuição direta
class Task { update(newData){ console.log("before update", this); Object.assign(this, data); console.log("after update", this); } }
Bem, para não duplicar o código em cada classe, também movemos esse método de atualização para a classe base.
Agora você pode ver que, quando atualizamos alguns dados, precisamos chamar manualmente o método updateDOM (). Mas é possível por conveniência executar essa atualização automaticamente sempre que uma chamada para o método de atualização ({...}) da classe base ocorrer.
Acontece que a classe base será mais ou menos assim
class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); ReactDOM.render(<App/>, rootElement) } }
Bem, para que durante a chamada sucessiva do método update () não haja atualizações desnecessárias, você pode atrasar a atualização do componente para o próximo ciclo de eventos
let TimerId = 0; class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); if(TimerId === 0) { TimerId = setTimeout(()=>{ TimerId = 0; ReactDOM.render(<App/>, rootElement); }) } } }
Além disso, você pode aumentar gradualmente a funcionalidade da classe base - por exemplo, para não precisar enviar manualmente uma solicitação ao servidor todas as vezes, além de atualizar o estado, você pode enviar uma solicitação para o método update ({..}) em segundo plano. Você pode organizar um canal de atualização ao vivo para soquetes da Web adicionando uma conta de cada objeto criado no mapa de hash global sem alterar os componentes e trabalhar com dados de qualquer maneira.
Ainda há muito a ser feito, mas quero mencionar um tópico interessante - muitas vezes passando um objeto com dados para o componente necessário (por exemplo, quando um componente do projeto renderiza um componente da tarefa -
<div>{project.tasks.map(task=><Task task={task}/>)}</div>
o próprio componente da tarefa pode precisar de algumas informações que não são armazenadas diretamente na tarefa, mas localizadas no objeto pai.
Suponha que você queira colorir todas as tarefas em uma cor armazenada no projeto e que seja comum a todas as tarefas. Para fazer isso, além dos props da tarefa, o componente do projeto também deve transmitir os seus props <Task task={task} project={this.props.project}/>
. E se você precisar repentinamente colorir a tarefa em uma cor comum a todas as tarefas em uma pasta, será necessário transferir o objeto de pasta atual do componente Pasta para o componente Tarefa, encaminhando-o pelo componente intermediário do Projeto.
Uma dependência frágil parece que o componente deve saber o que seus componentes aninhados exigem. Além disso, a possibilidade de um contexto de reação, embora simplifique a transferência através de componentes intermediários, ainda exigirá uma descrição do provedor e conhecimento de quais dados são necessários para os componentes filhos.
Mas o principal problema é que toda vez que você edita um design ou altera a lista de desejos de um cliente quando um componente precisa de novas informações, você precisa alterar os componentes superiores, seja encaminhando objetos de suporte ou criando fornecedores de contexto. Eu gostaria que o componente que recebesse por meio de adereços um objeto com dados acessasse de alguma forma qualquer parte do estado do aplicativo. E aqui, o javascript é um bom ajuste (diferente de qualquer linguagem funcional, como olmo ou abordagens imutáveis, como redux) - para que os objetos possam armazenar links circulares entre si. Nesse caso, o objeto de tarefa deve ter um campo task.project com um link para o objeto do projeto pai no qual está armazenado, e o objeto do projeto, por sua vez, deve ter um link para o objeto de pasta etc. para o objeto AppState raiz. Portanto, o componente, por mais profundo que seja, sempre pode percorrer os objetos-pai através do link e obter todas as informações necessárias e não precisa lançá-lo por vários componentes intermediários. Portanto, introduzimos uma regra - sempre que você cria um objeto, é necessário adicionar um link ao objeto pai. Por exemplo, agora a criação de uma nova tarefa será parecida com esta
... const {project} = this.props; const newTask = new Task({project: this.props.project}) this.props.project.tasks.push(newTask);
Além disso, com um aumento na lógica de negócios, você pode observar que o bolterplate está associado ao suporte de backlink (por exemplo, atribuir um link ao objeto pai ao criar um novo objeto ou, por exemplo, ao transferir um projeto de uma pasta para outra, você precisará não apenas atualizar a propriedade project.folder = newFolder e excluir você mesmo da matriz do projeto da pasta anterior e adicionando uma nova pasta à matriz do projeto) começa a se repetir e também pode ser movido para a classe base, de modo que, quando você cria o objeto, basta especificar a new Task({project: this.porps.project})
pai new Task({project: this.porps.project})
new Task({project: this.porps.project})
e a classe base adicionariam automaticamente um novo objeto à matriz project.tasks
e, ao transferir a tarefa para outro projeto, bastaria apenas atualizar o campo task.update({project: newProject})
e a classe base task.update({project: newProject})
automaticamente a tarefa de uma matriz de tarefas do projeto anterior e adicionada a uma nova. Mas isso já exigirá a declaração de relacionamentos (por exemplo, em propriedades ou métodos estáticos) para que a classe base saiba quais campos atualizar.
Conclusão
De uma maneira tão simples, usando apenas js-objects, chegamos à conclusão de que você pode ter toda a conveniência de trabalhar com o estado geral do aplicativo sem introduzir no aplicativo a dependência de uma biblioteca externa para trabalhar com o estado.
A questão é: por que precisamos de bibliotecas para gerenciar o estado e, em particular, o mobx?
O fato é que, na abordagem descrita para a organização do estado geral, ao usar objetos js "vanilla" nativos comuns (ou objetos de classe), há uma grande desvantagem - quando uma pequena parte do estado ou mesmo um campo é alterada, os componentes serão atualizados ou "renderizados" que não são conectados de forma alguma e não depende dessa parte do estado.
E em aplicativos grandes com interface do usuário em negrito, isso leva a freios, porque a reação simplesmente não tem tempo para comparar recursivamente a casa virtual de todo o aplicativo, já que, além de comparar cada renderizador, uma nova árvore de objetos será gerada cada vez que descreve o layout de absolutamente todos os componentes.
Mas esse problema, apesar da importância, é puramente técnico - existem bibliotecas semelhantes à reação do vitual dom que otimizam melhor o renderizador e podem aumentar o limite do componente.
Existem técnicas de renovação residencial mais eficazes do que a criação de uma nova árvore virtual e a comparação recursiva subsequente é passada com a árvore anterior.
E, finalmente, existem bibliotecas que tentam resolver o problema de atualizações lentas por meio de uma abordagem diferente - a saber, para rastrear quais partes do estado estão conectadas a quais componentes e ao alterar alguns dados, calculam e atualizam apenas os componentes que dependem desses dados e não tocam nos componentes restantes. O Redux também é uma biblioteca desse tipo, mas requer uma abordagem completamente diferente da organização do estado. Mas a biblioteca mobx, pelo contrário, não traz nada de novo e podemos acelerar o renderizador praticamente sem alterar nada no aplicativo - basta adicionar o decorador @observable
aos campos da classe e o decorador @observable
aos componentes que renderizam esses campos. cortar apenas o código de atualização desnecessário para o componente raiz no método update () de nossa classe base e obteremos um aplicativo totalmente funcional, mas agora alterar uma parte do estado ou mesmo um campo atualizará apenas esses componentes maturada assinado (método girando dentro render ()) para um domínio específico de um determinado estado do objecto.