Como carregar dados em Reagir com redux-thunk, redux-saga, suspense e ganchos

Valerii Tereshchenko Segue Jan 31 · 11 min ler

Introdução

React é uma biblioteca JavaScript para criar interfaces com o usuário. Muitas vezes, usar React significa usar React with Redux . O Redux é outra biblioteca JavaScript para gerenciar o estado global. Infelizmente, mesmo com essas duas bibliotecas, não há uma maneira clara de como lidar com chamadas assíncronas para a API (backend) ou qualquer outro efeito colateral.

Neste artigo, estou tentando comparar diferentes abordagens para resolver esse problema. Vamos definir o problema primeiro.

O componente X é um dos muitos componentes do site (ou aplicativo móvel, ou desktop, também é possível). X consultas e mostra alguns dados carregados da API. X pode ser página ou apenas parte da página. Importante é que X é um componente separado que deve ser fracamente acoplado ao resto do sistema (tanto quanto possível). X deve mostrar o indicador de carregamento enquanto os dados estão sendo recuperados e o erro se a chamada falhar.

Este artigo pressupõe que você já tenha alguma experiência com a criação de aplicativos React / Redux.

Este artigo vai mostrar 4 maneiras de resolver este problema e comparar os prós e contras de cada um. Não é um manual detalhado sobre como usar thunk, saga, suspence ou hooks .

O código desses exemplos está disponível no GitHub .

Configuração inicial

Servidor Mock

Para fins de teste, vamos usar o json-server . É um projeto incrível que permite que você crie APIs REST falsas com muita rapidez. Para o nosso exemplo, parece com isso.

 const jsonServer = require ( 'json-server' ); 
servidor const = jsonServer .create ();
roteador const = jsonServer . roteador ( 'db.json' );
middleware const = jsonServer . padrões ();
server.use ((req, res, next) => {
setTimeout (() => next (), 2000);
});
server.use (middleware);
server.use (roteador);
server.listen (4000, () => {
console .log (o `JSON Server está rodando ...` );
});

Nosso arquivo db.json contém dados de teste no formato json.

 { 
"usuários" : [
{
"id" : 1,
"firstName" : "John" ,
"lastName" : "Doe" ,
"ativo" : verdadeiro
"posts" : 10,
"mensagens" : 50
}
...
{
"id" : 8,
"firstName" : "Clay" ,
"sobrenome" : "Chung" ,
"ativo" : verdadeiro
"posts" : 8,
"mensagens" : 5
}
]
}

Depois de iniciar o servidor, uma chamada para o http: // localhost: 4000 / users retorna a lista dos usuários com uma imitação de atraso – cerca de 2s.

Chamada de projeto e API

Agora estamos prontos para começar a codificação. Eu suponho que você já tenha um projeto React criado usando create-react-app com o Redux configurado e pronto para uso.

Se você tiver alguma dificuldade com isso, você pode verificar isso e isso .

O próximo passo é criar uma função para chamar a API ( api.js ):

 const API_BASE_ADDRESS = 'http: // localhost: 4000' ; classe padrão de exportação Api { 
getUsers estáticos () {
const uri = API_BASE_ADDRESS + "/ users" ;
retorno buscar (uri, {
método : 'GET'
});
}
}

Redux-thunk

O Redux-thunk é um middleware recomendado para a lógica básica de efeitos colaterais do Redux, como lógica assíncrona simples (como uma solicitação para a API). O Redux-thunk em si não faz muito. São apenas 14 !!! linhas do código . Apenas adiciona um pouco de “sintaxe de açúcar” e nada mais.

O fluxograma abaixo ajuda a entender o que vamos fazer.

Toda vez que uma ação é executada, o redutor muda de estado de acordo. O componente mapeia o estado para propriedades e usa essas propriedades no método render () para descobrir o que o usuário deve ver: um indicador de carregamento, dados ou mensagem de erro.

Para que isso funcione, precisamos fazer 5 coisas.

1. Instalar o tunk

 npm instalar redux-thunk 

2. Adicione o middleware de conversão ao configurar a loja (configureStore.js)

 import { applyMiddleware , compose , createStore} de 'redux' ; 
importar thunk de 'redux-thunk' ;
import rootReducer de './appReducers' ;
função de exportação configureStore (initialState) {
middleware const = [thunk];
const composeEnhancers = window .__ REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compor ;
const store = createStore (rootRedutor, initialState, composeEnhancers ( applyMiddleware (... middleware)));
loja de devolução ;
}

Nas linhas 12–13, também configuramos devtools do redux . Um pouco mais tarde ajudará a mostrar um dos problemas com essa solução.

3. Crie ações (redux-thunk / actions.js)

 import Api de "../api" export const LOAD_USERS_LOADING = 'REDUX_THUNK_LOAD_USERS_LOADING' ; 
export const LOAD_USERS_SUCCESS = 'REDUX_THUNK_LOAD_USERS_SUCCESS' ;
export const LOAD_USERS_ERROR = 'REDUX_THUNK_LOAD_USERS_ERROR' ;
export const loadUsers = () => envio => {
despacho ({ type : LOAD_USERS_LOADING});
Api. getUsers ()
.then (response => response.json ())
.então(
data => dispatch ({ type : LOAD_USERS_SUCCESS, data}),
error => dispatch ({ type : LOAD_USERS_ERROR, erro : error. message || 'Erro inesperado !!!' }})
)
};

Eu tentei fazer o componente o mais simples possível. Eu entendo que parece horrível 🙂

Indicador de carregamento

Dados

Erro

Lá você tem: 3 arquivos, 109 linhas de código (13 (ações) + 36 (redutor) + 60 (componente)).

Prós:

  • Abordagem “recomendada” para aplicações react / redux.
  • Nenhuma dependência adicional. Quase, thunk é minúsculo 🙂
  • Não há necessidade de aprender coisas novas.

Contras:

  • Muito código em lugares diferentes
  • Após a navegação para outra página, os dados antigos ainda estão no estado global (veja a figura abaixo). Esses dados são informações desatualizadas e inúteis que consomem memória.
  • No caso de cenários complexos (várias chamadas condicionais em uma ação, etc.), o código não é muito legível

Redux-saga

O Redux-saga é uma biblioteca de middleware redux projetada para tornar o manuseio de efeitos colaterais fácil e legível. Ele aproveita o ES6 Generators, o que nos permite escrever um código assíncrono que parece síncrono. Além disso, esta solução é fácil de testar.

De uma perspectiva de alto nível, essa solução funciona da mesma maneira que thunk. O fluxograma do exemplo de conversão ainda é aplicável.

Para que isso funcione, precisamos fazer 6 coisas.

1. Instale a saga

 npm instalar redux-saga 

2. Adicione o middleware saga e adicione todas as sagas (configureStore.js)

 import { applyMiddleware , compose , createStore} de 'redux' ; 
import createSagaMiddleware de 'redux-saga' ;
import rootReducer de './appReducers' ;
import usersSaga de "../redux-saga/sagas" ;
const sagaMiddleware = createSagaMiddleware (); função de exportação configureStore (initialState) {
middleware const = [sagaMiddleware];
const composeEnhancers = window .__ REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compor ;
const store = createStore (rootRedutor, initialState, composeEnhancers ( applyMiddleware (... middleware)));
sagaMiddleware.run ( usersSaga ); loja de devolução ;
}

As sagas da linha 4 serão adicionadas na etapa 4.

3. Criar ação (redux-saga / actions.js)

 export const LOAD_USERS_LOADING = 'REDUX_SAGA_LOAD_USERS_LOADING' ; 
export const LOAD_USERS_SUCCESS = 'REDUX_SAGA_LOAD_USERS_SUCCESS' ;
export const LOAD_USERS_ERROR = 'REDUX_SAGA_LOAD_USERS_ERROR' ;
export const loadUsers = () => envio => {
despacho ({ type : LOAD_USERS_LOADING});
};

4. Crie sagas (redux-saga / sagas.js)

 import {colocar, tomar todo , tirarLatest} de 'redux-saga / effects' 
import {loadUsersSuccess, LOAD_USERS_ERROR, LOAD_USERS_LOADING, LOAD_USERS_SUCCESS} de "./actions" ;
Importar Api da
função assíncrona '../api' fetchAsync (func) {
resposta const = espera func ();
if (resposta. ok ) {
return aguardar response.json ();
}
throw new Error ( "Erro inesperado !!!" );
}
function * fetchUser () {
tente {
const us = yield fetchAsync (Api. getUsers );
rendimento put ({ type : LOAD_USERS_SUCCESS, data : users});
} pegar (e) {
yield put ({ type : LOAD_USERS_ERROR, erro : e. message });
}
}
função de exportação * usersSaga () {
// Permite buscas simultâneas de usuários
yield takeEvery (LOAD_USERS_LOADING, fetchUser );
// Não permite buscas simultâneas de usuários
// yield takeLatest (LOAD_USERS_LOADING, fetchUser);
}
exportar usuários padrãoSaga ;

Saga tem uma curva de aprendizado bastante íngreme, então se você nunca usou e nunca leu nada sobre esse framework, pode ser difícil entender o que está acontecendo aqui. Resumidamente, na função userSaga configuramos a saga para escutar a ação LOAD_USERS_LOADING e acionar a função fetchUsers . A função fetchUsers chama a API. Se a chamada for bem-sucedida, a ação LOAD_USER_SUCCESS será despachada, caso contrário, a ação LOAD_USER_ERROR será despachada.

5. Criar redutor (redux-saga / reducer.js)

 import {LOAD_USERS_ERROR, LOAD_USERS_LOADING, LOAD_USERS_SUCCESS} de "./actions" ; const initialState = { 
dados : [],
carregando : falso ,
erro : ''
};
função padrão de exportação reduxSagaReducer (state = initialState, action) {
switch (ação. tipo ) {
case LOAD_USERS_LOADING: {
return {
...Estado,
carregamento : verdadeiro
erro : ''
};
}
case LOAD_USERS_SUCCESS: {
return {
...Estado,
dados : ação. dados ,
carregamento : falso
}
}
case LOAD_USERS_ERROR: {
return {
...Estado,
carregando : falso ,
erro : ação. erro
};
}
padrão : {
estado de retorno ;
}
}
}

O redutor aqui é absolutamente o mesmo que no exemplo de conversão.

6. Criar componente conectado ao redux (redux-saga / UsersWithReduxSaga.js)

 import * como Reagir de 'reagir' ; 
import {connect} de 'react-redux' ;
import { loadUsers } de "./actions" ;
classe UsersWithReduxSaga extends React.Component {
componentDidMount () {
isso . adereços . loadUsers ();
};
render () {
if ( isto . props . loading ) {
retorno < div > Loading </ div >
}
if ( this . props . error ) {
return < div style = {{ color : 'red' }}> ERRO: { this . adereços . erro } </ div >
}
retorno (
< table >
< thead >
< tr >
< th > primeiro nome </ th >
< th > Sobrenome </ th >
< th > Ativo? </ th >
< th > Posts </ th >
< th > Mensagens </ th >
</ tr >
</ thead >
< tbody >
{ this . adereços . dados .map (u =>
< tr key = {u. id }>
< td > {u.firstName} </ td >
< td > {u.lastName} </ td >
< td > {u. ativo ? 'Sim' : 'Não' } </ td >
< td > {u.posts} </ td >
< td > {u. mensagens } </ td >
</ tr >
)}
</ tbody >
</ table >
);
}
}
const mapStateToProps = state => ({
dados : estado. reduxSaga . dados ,
carregamento : estado. reduxSaga . carregando ,
erro : estado. reduxSaga . erro ,
});
const mapDispatchToProps = {
loadUsers
};
exportar padrão conectar (
mapStateToProps ,
mapDispatchToProps
) (UsersWithReduxSaga);

O componente também é quase o mesmo aqui como no exemplo de conversão.

Então aqui temos 4 arquivos, 136 linhas de código (7 (ações) + 36 (redutor) + sagas (33) + 60 (componente)).

Prós:

  • Código mais legível (async / await)
  • Bom para lidar com cenários complexos (várias chamadas condicionais em uma ação, a ação pode ter vários ouvintes, cancelar ações etc.)
  • Teste fácil de unidade

Contras:

  • Muito código em lugares diferentes
  • Após a navegação para outra página, os dados antigos ainda estão no estado global. Esses dados são informações desatualizadas e inúteis que consomem memória.
  • Dependência adicional
  • Muitos conceitos para aprender

Suspense

Suspense é um novo recurso no React 16.6.0. Isso nos permite adiar a parte de renderização do componente até que alguma condição seja atendida (por exemplo, dados da API carregada).

Para fazer isso funcionar, precisamos fazer 4 coisas (definitivamente está ficando melhor :)).

1. Criar cache (suspense / cache.js)

Para o cache, vamos usar um provedor de cache simples, que é um provedor de cache básico para aplicativos de reação.

 import { createCache } de 'simple-cache-provider' ; exportação deixa cache; função initCache () { 
cache = createCache ( initCache );
}
initCache ();

2. Criar limite de erros (suspense / ErrorBoundary.js)

Este é um limite de erro para capturar erros lançados pelo Suspense.

 importar Reagir de 'reagir' ; classe de exportação ErrorBoundary estende React.Component { 
estado = {};
componentDidCatch (error) {
this .setState ({ error : error. message || "Erro inesperado" });
}
render () {
if ( this . state . error ) {
return < div style = {{ color : 'red' }}> ERRO: { this . estado . erro || 'Erro inesperado' } </ div >;
}
retornar isso . adereços . crianças ;
}
}
padrão de exportação ErrorBoundary;

3. Criar tabela de usuários (suspense / UsersTable.js)

Para este exemplo, precisamos criar um componente adicional que carregue e mostre dados. Aqui estamos criando um recurso para obter dados da API.

 import * como Reagir de 'reagir' ; 
import { createResource } de "simple-cache-provider" ;
import {cache} de "./cache" ;
importe Api de "../api" ;
deixe UsersResource = createResource ( async () => {
resposta const = aguardar Api. getUsers ();
const json = aguardar response.json ();
return json;
});
classe UsersTable estende React.Component {
render () {
deixe usuários = UsersResource.read (cache);
retorno (
< table >
< thead >
< tr >
< th > primeiro nome </ th >
< th > Sobrenome </ th >
< th > Ativo? </ th >
< th > Posts </ th >
< th > Mensagens </ th >
</ tr >
</ thead >
< tbody >
{users.map (u =>
< tr key = {u. id }>
< td > {u.firstName} </ td >
< td > {u.lastName} </ td >
< td > {u. ativo ? 'Sim' : 'Não' } </ td >
< td > {u.posts} </ td >
< td > {u. mensagens } </ td >
</ tr >
)}
</ tbody >
</ table >
);
}
}
exportar UsersTable padrão ;

4. Criar componente (suspense / UsersWithSuspense.js)

 import * como Reagir de 'reagir' ; 
import UsersTable de "./UsersTable" ;
import ErrorBoundary de "./ErrorBoundary" ;
classe UsersWithSuspense estende React.Component {
render () {
retorno (
< ErrorBoundary >
< Fallback do React.Suspense = {< div > Loading </ div >}>
< UsersTable />
</ React.Suspense >
</ ErrorBoundary >
);
}
}
exportar padrão UsersWithSuspense;

4 arquivos, 106 linhas de código (9 (cache) + 19 (ErrorBoundary) + UsersTable (33) + 45 (componente)).

3 arquivos, 87 linha de código (9 (cache) + UsersTable (33) + 45 (component)) se assumirmos que ErrorBoundary é um componente reutilizável.

Prós:

  • Nenhum redux é necessário. Essa abordagem pode ser usada sem redux. Componente é totalmente independente.
  • Nenhuma dependência adicional (o provedor de cache simples faz parte do React)
  • Atraso de exibição do indicador de carregamento definindo a propriedade dellayMs
  • Menos linhas de código que nos exemplos anteriores

Contras:

  • O cache é necessário mesmo quando não precisamos realmente de cache.
  • Alguns novos conceitos precisam ser aprendidos (que fazem parte do React).

Ganchos

No momento de escrever este artigo, os ganchos ainda não foram oficialmente lançados e estão disponíveis apenas na versão “seguinte”. Ganchos são indiscutivelmente um dos recursos mais revolucionários que podem mudar muito no mundo React muito em breve. Mais detalhes sobre ganchos podem ser encontrados aqui e aqui .

Para fazê-lo funcionar para o nosso exemplo, precisamos fazer uma coisa (!) :

1. Crie e use ganchos (hooks / UsersWithHooks.js)

Aqui estamos criando 3 ganchos (funções) para “enganchar” no estado Reagir.

 import React, {useState, useEffect} de ' react ' ; 
importe Api de "../api" ;
function UsersWithHooks () {
const [data, setData] = useState ([]);
const [carregando, setLoading] = useState ( true );
const [error, setError] = useState ( '' );
useEffect ( async () => {
tente {
resposta const = aguardar Api. getUsers ();
const json = aguardar response.json ();
setData (json);
} pegar (e) {
setError (e. mensagem || 'Erro inesperado' );
}
setLoading ( false );
}, []);
if (loading) {
retorno < div > Loading </ div >
}
se (erro) {
return < div style = {{ color : 'vermelho' }}> ERRO: {error} </ div >
}
retorno (
< table >
< thead >
< tr >
< th > primeiro nome </ th >
< th > Sobrenome </ th >
< th > Ativo? </ th >
< th > Posts </ th >
< th > Mensagens </ th >
</ tr >
</ thead >
< tbody >
{data.map (u =>
< tr key = {u. id }>
< td > {u.firstName} </ td >
< td > {u.lastName} </ td >
< td > {u. ativo ? 'Sim' : 'Não' } </ td >
< td > {u.posts} </ td >
< td > {u. mensagens } </ td >
</ tr >
)}
</ tbody >
</ table >
);
}
exportar padrão UsersWithHooks ;

E é isso – apenas 1 arquivo, 56 linhas de código !!!

Prós:

  • Nenhum redux é necessário. Essa abordagem pode ser usada sem redux. Componente é totalmente independente.
  • Nenhuma dependência adicional
  • Aproximadamente 2 vezes menos código que em outras soluções

Contras:

  • À primeira vista, o código parece estranho e difícil de ler e entender. Levará algum tempo para se acostumar com ganchos.
  • Alguns novos conceitos precisam ser aprendidos (que são parte do React)
  • Não lançado oficialmente ainda

Conclusão

Vamos organizar essas métricas como uma tabela primeiro.

  • O Redux ainda é uma boa opção para gerenciar o estado global (se você tiver)
  • Cada opção tem prós e contras. Qual abordagem é melhor depende do projeto: sua complexidade, casos de uso, conhecimento da equipe, quando o projeto está indo para a produção, etc.
  • Saga pode ajudar com casos de uso complexos
  • Suspense e Ganchos valem a pena considerar (ou pelo menos aprender) especialmente para novos projetos

É isso aí – aproveite e feliz codificação!