Quando nosso aplicativo cresce, precisamos de uma maneira de manter a complexidade ao lidar com o estado do aplicativo. Estado são os dados que são manipulados e refletidos por nosso aplicativo em execução. Existem muitas maneiras de lidar com o estado do aplicativo no React. Algumas das bibliotecas de gerenciamento de estado mais populares são Redux , API de contexto , React Query , Recoil e XState .

Neste artigo, usaremos o XState, pois é uma das maneiras mais fáceis e eficientes de lidar com o estado em qualquer aplicativo JavaScript, em vez de ser independente de estrutura. Ao usar o XState, podemos facilmente aproveitar as máquinas de estado para gerenciar e projetar o estado global ou do componente.

O que é uma máquina de estado finito?

máquinas de estado finito são um modelo matemático de computação, inicialmente desenvolvido no início dos anos 1940, que tem sido usado por décadas para construir hardware e software para uma ampla variedade de tecnologias. Isso nos ajuda a reduzir o número de estados possíveis e controlar a transição de mover o estado, o que resulta em um aplicativo previsível e confiável para trabalhar.

O outro benefício de trabalhar com uma máquina de estado é que eles também têm um visualizador para ver o estado em um diagrama, chamado XState Visualizer , antes e/ou depois de construí-lo.

Pré-requisitos

  • Familiaridade com JavaScript e uma compreensão básica da sintaxe ES6.
  • Node.js instalado em sua máquina
  • NPM instalado

Neste tutorial, construiremos uma livraria simples e implementaremos todo o estado necessário para construir essa loja. Esta loja envolverá toda a funcionalidade CRUD (criar, ler, atualizar, excluir) necessária para construir aplicativos gerais.

Para o endpoint da API, vamos aproveitar os recursos fornecidos pela Airtable e usá-los para fornecer dados para nosso aplicativo e estilizar nosso aplicativo com Tailwind CSS .

Adicionando máquina de estado de livros

Dentro de nosso XState Visualizer, primeiro escreveremos uma máquina de estado simples para criar um novo livro para nossa livraria:

 export const addbookMachine={ id:'addBooks', inicial:'addNew', estados: { adicionar novo: {}, adicionando: { invocar: { id:'addBook', src: addBook, onDone: { alvo:'sucesso', ações: atribuir ({campos: (_contexto, evento)=> evento.data}), }, onError: { alvo:'falhou', ações: atribuir ({erro: (_contexto, evento)=> evento.data}), }, }, }, sucesso: {}, fracassado: {}, },
};

Visualizando esta máquina de estado simples, podemos ver quando podemos preencher o formulário add a book , que faz a transição de seu estado de addNew para add e, com base nessa promessa específica, mudamos para sucesso e erro. Esta é uma máquina de estado simples que pode fazer a transição de seu estado com base no sucesso e na falha quando uma função é chamada.

XState V isualizer
XState Visualizer gerado a partir do XState Visualizer .

Também criaremos uma função para determinar se nossa chamada foi um sucesso ou um fracasso. Já o chamamos de addBook dentro de invoke em nossa máquina de estado antes.

 const addBook=(contexto, evento)=> nova promessa (assíncrona (resolver, rejeitar)=> { deixe o resultado=esperar addTheBooks (contexto, evento); if (result.status===200) { resolver (resultado); } senão { rejeitar ('livros'); } });

Esta promessa resolve ou rejeita a chamada de função que fornecemos para aguardar seu valor. Se a resposta for 200 , iremos para onDone ; caso contrário, faremos a transição para o estado OnError .

Vamos também conectar à nossa API Airtable para fazer esta chamada. Vamos criar uma nova pasta e chamá-la de API , onde colocaremos todas as chamadas de API necessárias para nosso aplicativo. Lá dentro, faremos um novo arquivo e o chamaremos de addBook .

 const addTheBooks=async (_context, event)=> { const {Nome, Autor, Publicado, Moeda, Categoria}=evento; const formater={ registros: [ { campos: {Nome, Autor, Publicado, Moeda, Categoria}, }, ], }; const res=aguarda busca (process.env.REACT_APP_BASE_URL +'Livros', { método:'POST', cabeçalhos: novos cabeçalhos ({ Autorização: process.env.REACT_APP_API_KEY, 'Content-Type':'application/json', }), corpo: JSON.stringify (formater), }); return res;
}; exportar addTheBooks padrão;

Aqui, chamamos um endpoint de API de nosso Airtable para POST os livros que adicionamos. Também adicionamos uma constante de formatador para estruturar o formato de dados que a API assume. Os campos do formulário serão enviados como uma carga JSON para a constante do formatador, que usa os valores dos campos como um par de valores-chave.

Por último, para adicionar livros em nossa livraria, criamos um componente para adicionar o livro. Isso se comunica com a máquina de estado para fluir nos estados especificados do aplicativo.

 import React, {useContext, useRef} de'react';
import {Link} de'react-router-dom';
importar {MachineContext} de'../state/index'; //eslint-disable-next-line
function Addbook ({}) { livro const=useRef (); const authorName=useRef (); const data=useRef (); preço const=useRef (); categoria const=useRef (); const [máquina, sendToMachine]=useContext (MachineContext); const addAbook=async ()=> { const Name=book.current.value; const Author=authorName.current.value; const Published=date.current.value; moeda const=parseFloat (price.current.value); const Categoria=categoria.corrente.valor; sendToMachine ('ADD_BOOK', {Nome, Autor, Publicado, Moeda, Categoria}); book.current.value=''; authorName.current.value=''; date.current.value=''; preço.corrente.valor=''; categoria.corrente.valor=''; }; Retorna ( 
Volte

Preencha os detalhes para adicionar um novo livro.

selecione um
{machine.matches ('addbookMachine.adding') && ( )}
{machine.matches ('addbookMachine.success') && (
Seu livro foi adicionado!
)} {machine.matches ('addbookMachine.failed') && (
Desculpe, não podemos adicionar um novo livro!
)}
); } exportar Addbook padrão;

Obviamente, precisamos que nosso aplicativo seja vinculado a um provedor XState. Para isso, você ainda pode verificar o repositório de código compartilhado abaixo.

Atualizando uma máquina de estado de livro

Para atualizar os detalhes do nosso livro, precisamos criar duas chamadas de API: a primeira para obter os detalhes de um único livro e a segunda para enviar uma solicitação PATCH para editar esse livro específico.

Vamos começar criando um arquivo fetchsinglebook.js e escrever nosso código de máquina lá:

 export const fetchOneBookMachine={ id:'fetchonebook', inicial:'iniciar', estados: { começar: {}, buscando: { invocar: { id:'getOneBook', src: getOneBook, onDone: { alvo:'sucesso', ações: atribuir ({lista: (_contexto, evento)=> evento.data}), }, onError: { alvo:'falhou', ações: atribuir ({erro: (_contexto, evento)=> evento.data}), }, }, }, edição: { invocar: { id:'editOneBook', src: editOneBook, onDone: { alvo:'sucesso', ações: atribuir ({lista: (_contexto, evento)=> evento.data}), }, onError: { alvo:'falhou', ações: atribuir ({erro: (_contexto, evento)=> evento.data}), }, }, }, sucesso: {}, fracassado: {}, },
};

Isso é o que eu realmente amo nas máquinas de estado. Com um simples olhar dessas estruturas de objetos aninhados, podemos entender o que está acontecendo.

Simplesmente adicionamos cinco estados agora, inicialmente no estado inicial. Quando invocamos a função FETCH_A_BOOK , ela faz a transição para buscar dentro de fetchOneBookMachine .

De maneira semelhante, também podemos adicionar solicitações PUT API assim que entrarmos no estado de edição. Todo o clima do estado se move para o estado de sucesso ou falha. Também precisamos criar uma promessa para determinar se a função está no estado onDone ou onError .

 const getOneBook=(contexto, evento)=> nova promessa (assíncrona (resolver, rejeitar)=> { let result=await fetchSingleBook (contexto, evento); if (result.status===200) { resolver (resultado); } senão { rejeitar ('livro'); } }); const editOneBook=(contexto, evento)=> { nova promessa (assíncrona (resolver, rejeitar)=> { deixe o resultado=esperar editTheBook (contexto, evento); if (result.status===200) { resolver (resultado); } senão { rejeitar ('livro'); } });
};

Também vamos escrever nossas chamadas de API para editar nossos livros. Aqui, também precisamos de duas funções: uma para pegar os valores de um único livro e, segundo, adicionar os valores de uma forma não destrutiva em nosso banco de dados.

 const fetchSingleBook=async (props)=> { const {id}=adereços; const res=await fetch (process.env.REACT_APP_BASE_URL +'Books/'+ id, { método:'GET', cabeçalhos: novos cabeçalhos ({ Autorização: process.env.REACT_APP_API_KEY, 'Content-Type':'application/json', }), }). então ((x)=> x.json ()); return res;
}; exportar fetchSingleBook padrão;

Finalmente, criaremos nosso componente editbook . Dentro dele, usaremos o gancho useEffect para obter um único livro. Esse livro é o mesmo ID que clicamos e visitamos em nossa rota, que filtraremos de nosso estado (atualizado quando obtivermos toda a lista de livros).

 import React, {useContext, useEffect, useState} de'react';
importar {MachineContext} de'../state/index';
import {useHistory} de"react-router-dom"; import {Link} de'react-router-dom'; função Editbook (rota) { const [máquina, sendToMachine]=useContext (MachineContext); const id=route.match.params.id; const [selectedBook, setSeletedBook]=useState ({ eu ia:'', Campos: { Categoria:'', Nome:'', Publicados:'', Moeda:'', Autor:'', }, createdTime:'', }); let history=useHistory (); useEffect (()=> { sendToMachine ('FETCH_A_BOOK', {id}); const {livros, erro}=máquina.contexto; lista const=books.records; const filterObj=list.filter ((lista)=> list.id===id); setSeletedBook (filterObj [0]); }, []); const handleOnChange=(userKey, value)=> { setSeletedBook ({... selectedBook, fields: {[userKey]: value}}); }; const editAbook=async ()=> { sendToMachine ('EDIT_A_BOOK', selectedBook); history.push ("/"); }; Retorna ( 
Volte

Preencha os detalhes para editar os detalhes do livro.

{machine.matches ('fetchOneBookMachine.fetching') && ( <> )}
handleOnChange ('Nome', e.target.value)} className="w-full bg-white arredondado border-gray-300 focus: border-indigo-500 focus: ring-2 focus: ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 principais-8 cores de transição duração-200 facilidade de entrada" />
handleOnChange ('Autor', e.target.value)} />
Data de publicação handleOnChange ('Publicado', e.target.value)} />
handleOnChange ('Moeda', e.target.value)} />
handleOnChange ('Categoria', e.target.value)} className="rounded border appear-none border-gray-300 py-2 w-full placeholder-gray-500 text-gray-900 focus: outline-none focus: ring-2 focus: ring-indigo-200 focus: border-indigo-500 text-base pl-3 pr-10 sm: text-sm" > {selectedBook.fields.Category}
); } exportar Editbook padrão;

Excluindo uma máquina de estado de livro

Vamos começar com a máquina para excluir nosso livro. Isso se parecerá com muitas outras máquinas de estado que criamos antes. Para excluir um livro, precisamos de uma lista de livros obtidos, portanto, criaremos dois arquivos.

O primeiro é para buscar todos os livros e o outro é para excluir um livro. Vamos adicionar alguns estados para nossa máquina para conseguir isso.

 export const removebookMachine={ id:'removebook', inicial:'iniciar', estados: { começar: {}, excluindo: { invocar: { id:'deletingBooks', src: deletingBooks, onDone: { alvo:'sucesso', ações: atribuir ({campos: (_contexto, evento)=> evento.data}), }, onError: { alvo:'falhou', ações: atribuir ({erro: (_contexto, evento)=> evento.data}), }, }, }, sucesso: {}, fracassado: {}, },
};

Este snippet é para remover máquinas de livros. Também adicionaremos código de máquina para buscar todas as máquinas.

 export const appMachine=Machine ({ id:'app', inicial:'init', contexto: { livros: [], erro: indefinido, Campos:'', }, estados: { iniciar: {}, addbookMachine, removebookMachine, fetchOneBookMachine, lista: { estados: { carregando: { invocar: { id:'fetchAllBooks', src: fetchAllBooks, onDone: { alvo:'sucesso', ações: atribuir ({books: (_context, event)=> event.data}), }, onError: { alvo:'falhou', ações: atribuir ({erro: (_contexto, evento)=> evento.data}), }, }, }, sucesso: {}, fracassado: {}, }, }, }, sobre: ​​{ LOAD_BOOKS: { destino:'list.loading', }, DELETE_BOOK: { alvo:'removebookMachine.deleting', ações: atribuir ((_ ctx, evt)=> ({ id: evt.id, })), } },
});

Como você deve ter notado, também importamos outro código de máquina com suas exportações nomeadas. Isso nos ajuda a separar a máquina de estado uma da outra.

Agora, vamos prosseguir para o componente Booklist , onde podemos chamar nossa máquina para invocar uma ação para nós. Exclua book e load books para as respectivas ações.

 import React, {useContext, useEffect} de'react';
importar {MachineContext} de'../state/index'; import {Deleteicon} de'./Deleteicon';
import {Link} de'react-router-dom'; function Booklist () { const [máquina, sendToMachine]=useContext (MachineContext); const {livros, erro}=máquina.contexto; lista const=books.records; useEffect (()=> { sendToMachine ('LOAD_BOOKS'); //eslint-disable-next-line }, []); const removeBook=(id)=> { sendToMachine ('DELETE_BOOK', {id}); }; Retorna ( <> {machine.matches ('list.loading') && (                     )}  {list && list.length> 0 && ( 
{list.map ((b)=> (
{b.fields.Name} {b.fields.Published} {b.fields.Author} {b.fields.Currency} {b.fields.Category} removeBook (b.id)}/>
))}
)}
{machine.matches ('list.failed') && ( Os dados não podem ser carregados {error.toString ()} )}
{machine.matches ('removebookMachine.deleting') && ( )}
{machine.matches ('removebookMachine.success') && (
Seu livro foi removido!
)} {machine.matches ('removebookMachine.failed') && (
Seu livro não foi removido!
)} ); } exportar lista de livros padrão;

Com essa abordagem, podemos criar um aplicativo de livraria CRUD simples e totalmente funcional. Nele, podemos armazenar, editar e excluir as informações sobre nossos livros dentro do Airtable.

Fazendo uso de XState, podemos determinar o número finito de estados do aplicativo e ter um melhor controle geral.

Conclusão

Como desenvolvedores, precisamos de bibliotecas externas para manter o estado do aplicativo. Usar apenas o estado interno do React usando useState não será uma solução, pois traz uma abordagem de código baseada em eventos e há muitas falhas que enfrentaremos ao usá-lo .

Afinal, estamos tornando a máquina de estado responsável pelas transições para tornar o desempenho de seu aplicativo mais robusto. Isso garante que você tenha apenas um estado por vez, que seja um dos seus estados predefinidos e que só seja possível fazer a transição de um determinado estado para outro se isso for habilitado explicitamente. Portanto, chamamos isso de máquina de estado finito.

Você pode encontrar o código-fonte do artigo no repositório github aqui . Boa programação!

A postagem Usando máquinas de estado com XState e React apareceu primeiro no LogRocket Blog .

Source link