Estruturando sua aplicação React + Typescript + Redux

Ruben Marcus
5 min readFeb 28, 2020

--

O Problema

Ao criar uma aplicação React + Redux, os desenvolvedores (incluindo eu) geralmente sentem que precisam fazer muita divisão e abstração de código para tentar minimizar a redundância de código.

Geralmente, eles otimizam muito cedo e adicionam complexidades que dificultam o gerenciamento do aplicativo.
Estruturar um aplicativo React + Redux pode ser muito desafiador em geral, e quando você coloca o TypeScript na mistura, você pode facilmente desperdiçar muito tempo e energia, tentando otimizá-lo

Uma Solução

Gostaria de apresentar a você uma estrutura simples e arquitetura geral que funcionam bem com aplicativos de qualquer tamanho. Eu uso esse esquema em um aplicativo que está em produção e atualmente possui 20mil linhas de código.
Existem algumas otimizações que você pode optar por criar para aplicativos maiores, mas eu só queria apresentar minha abordagem simples baseada em Elm.

Estrutura de pastas

Aqui está a estrutura principal das pastas do aplicativo.

Observe como são apenas quatro arquivos relacionados ao Redux.
É isso mesmo, todo o gerenciamento de estado e actions pode ser encontrado em apenas quatro arquivos e apenas um reducer.

types.ts

Nosso arquivo de tipos deve definir como devem ser os dados e as ações do aplicativo.
É importante observar que não devemos colocar nenhum javascript em tempo de execução nesse arquivo.
Essa é uma maneira declarativa e definitiva de acessar todos os modelos de dados de todo o aplicativo em um arquivo.
Confie em mim, é muito conveniente.
Descrevi um exemplo simples que carrega usuários de uma API externa e os armazena em nosso repositório redux usando actions separadas de solicitação, sucesso e erro.
Falaremos mais sobre isso no reducer.

A idéia é que possamos mapear como tudo em nossa store redux deve ser e mudar no aplicativo.

import { Action } from 'redux'; interface User {  name: string;  age: number;} export interface LoadingState { 
users: boolean;
}
export interface ApplicationState
{ loading: LoadingState;
users: User[];
}
export interface LoadUsersRequest extends Action {
type: 'loadUsersRequest';
}
export interface LoadUsersSuccess extends Action {
type: 'loadUsersSuccess';
users: User[];
}
export interface LoadUsersError extends Action {
type: 'loadUsersError';
}
export type ApplicationAction = | LoadUsersRequest | LoadUsersSuccess | LoadUsersError;

actions.ts

Nosso arquivo de actions simplesmente executa uma tarefa: mapear os tipos de actions.
Você pode pensar que é um pouco redundante ter que definir o tipo de actions e depois passar para o arquivo de actions para adicionar uma nova action, mas conforme seu aplicativo cresce, torna-se conveniente e fácil ver como todas essas peças funcionam juntas.

Além disso, separar tipos de javascript em tempo de execução ajuda a manter seus arquivos de tipos a única fonte pura de verdade para todo o aplicativo.

import { 
User,
LoadUsersRequest,
LoadUsersSuccess,
LoadUsersError } from './types';
export const loadUsersRequest = (): LoadUsersRequest => ({
type: 'loadUsersRequest',
});
export const loadUsersSuccess = (users: User[]): LoadUsersSuccess => ({ type: 'loadUsersSuccess', users,
});
export const loadUsersError = (): LoadUsersError => ({
type: 'loadUsersError',
});

reducer.ts

Nosso arquivo reducer contém apenas uma única função reducer do redux. Não há necessidade de combinar redutores.

Um grande redutor, mantém todas as mudanças de estado em um só lugar. Isso é muito conveniente, pois cada action pode mudar qualquer parte do estado e está tudo em um só lugar.

Por exemplo, digamos que você tenha dois reducers, um para carros e outro para usuários.
Se você precisar conectar esses dados, precisará lidar com a única action nos dois reducers. Você pode encontrar mais informações sobre essa discussão no post oficial do Redux sobre ter vários reducers aqui:
https://redux.js.org/recipes/structuring-reducers/using-combinereducers.

Prefiro atualizar apenas em um só lugar.
É mais declarativo e mais fácil lidar com as mudanças posteriormente, pois a refatoração no Redux pode rapidamente se tornar um pesadelo.

Observe que usamos o immer aqui para lidar com alterações de estado no reducer. O Immer fornece um rascunho imutável que você pode alterar diretamente e retorna o novo estado.
Você pode encontrá-lo aqui:
https://github.com/immerjs/immer.

import produce from 'immer';
import { ApplicationState, ApplicationAction } from './types';
export const initialState: ApplicationState = {
loading: {
users: false,
},
users: [],
}
const reducer = (state = initialState, action: ApplicationAction) => { switch (action.type) {
case "loadUsersRequest":
return produce(state, draft => {
draft.loading.users = true;
});
case "loadUsersSuccess":
return produce(state, draft => {
draft.loading.users = false;
draft.users = action.users;
});
case "loadUsersError":
return produce(state, draft => {
draft.loading.users = false;
});
}}

export default reducer;

effects.ts

Nosso arquivo de effects é relevante apenas se seu aplicativo precisar executar actions assíncronas, como a comunicação com uma API externa ou qualquer coisa do tipo.
Por uma questão de exemplo, vamos usar redux-thunk para despachar assincronamente actions para o nosso reducer.
O trabalho do arquivo de efeitos é conectar nossas chamadas externas à API às actions do redux para garantir que os dados sejam transmitidos corretamente e na ordem correta.

import { ThunkAction } from 'redux-thunk';
import { ApplicationState, ApplicationAction } from './types';
import { loadUsersRequest,
loadUsersSuccess,
loadUsersError } from './actions';
import * as userService from '../services/userService'; type Effect = ThunkAction<any, ApplicationState, any, ApplicationAction>; export const loadUsers = (): Effect => (dispatch, getState) => { dispatch(loadUsersRequest()); // assume userService.loadUsers returns a Promise<User[]> return userService.loadUsers()
.then(users => dispatch(loadUsersSuccess(users)))
.catch(() => dispatch(loadUsersError()));
};

Juntando tudo

Agora, apenas precisamos conectar nossa store redux aos nossos componentes React!
Também incluí um exemplo de como você pode usar o redux-devtools-extension.
Você pode encontrar a extensão aqui: https://github.com/zalmoxisus/redux-devtools-extension.

import React from 'react';
import ReactDOM from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import reducer, { initialState } from './store/reducer';
import App from './components/App';
// Se estiver interessado em usar o redux dev tools
import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools({});const store = createStore(reducer, initialState,
composeEnhancers(applyMiddleware(thunk)));

const ConnectedApp = () => (
<Provider store={store}>
<App />
</Provider>
);
ReactDOM.render(<ConnectedApp />, document.getElementById('root'));

Seguir essa estrutura garante que seu aplicativo seja totalmente seguro quanto usar TypeScript e forneça uma única fonte de verdade para o estado e a forma dos dados. Tudo isso resulta em menos bugs, mais produtividade e um aplicativo React + Redux + TypeScript melhor!

Créditos

How To Structure Your TypeScript + React + Redux App escrito por Jake Richards

--

--

Ruben Marcus
Ruben Marcus

No responses yet