
Atualmente, a maior parte dos aplicativos da Web baseados na estrutura React está sendo desenvolvida usando a biblioteca Redux. Essa biblioteca é a implementação mais popular da arquitetura FLUX e, apesar de várias vantagens óbvias, possui desvantagens muito significativas, como:
- a complexidade e a "verbosidade" dos padrões recomendados para escrever e organizar o código, o que implica um grande número de clichês;
- falta de controles internos para comportamento assíncrono e efeitos colaterais, o que leva à necessidade de escolher a ferramenta certa a partir de uma variedade de complementos criados por desenvolvedores de terceiros.
Para resolver essas deficiências, os desenvolvedores do Redux introduziram a biblioteca do Redux Toolkit. Essa ferramenta é um conjunto de soluções e métodos práticos projetados para simplificar o desenvolvimento de aplicativos usando o Redux. Os desenvolvedores desta biblioteca tiveram como objetivo simplificar casos típicos de uso do Redux. Essa ferramenta não é uma solução universal em cada um dos casos possíveis de uso do Redux, mas permite simplificar o código que o desenvolvedor precisa escrever.
Neste artigo, falaremos sobre as principais ferramentas incluídas no Redux Toolkit e também, usando um exemplo de fragmento de nosso aplicativo interno, mostraremos como usá-las no código existente.
Brevemente sobre a biblioteca
Resumo do Redux Toolkit:
- antes do lançamento, a biblioteca era chamada redux-starter-kit;
- o lançamento ocorreu no final de outubro de 2019;
- A biblioteca é oficialmente suportada pelos desenvolvedores do Redux.
Segundo os desenvolvedores , o Redux Toolkit executa as seguintes funções:
- Ajuda você a começar rapidamente usando o Redux
- simplifica o trabalho com tarefas típicas e código Redux;
- permite que você use as melhores práticas do Redux por padrão;
- oferece soluções que reduzem a desconfiança dos boilerplates.
O Redux Toolkit fornece um conjunto de ferramentas especialmente projetadas e adiciona uma série de ferramentas comprovadas que são comumente usadas com o Redux. Essa abordagem permite que o desenvolvedor decida como e quais ferramentas usar em seu aplicativo. No decorrer deste artigo, observaremos quais empréstimos essa biblioteca usa. Para obter mais informações e dependências do Redux Toolkit, consulte a descrição do pacote @ reduxjs / toolkit .
Os recursos mais significativos fornecidos pela biblioteca Redux Toolkit são:
- #configureStore - uma função projetada para simplificar o processo de criação e configuração de armazenamento;
- #createReducer - uma função que ajuda a descrever e criar concisa e claramente um redutor;
- #createAction - retorna a função do criador da ação para a sequência especificada do tipo de ação;
- #createSlice - combina a funcionalidade de createAction e createReducer;
- createSelector é uma função da biblioteca Reselect , reexportada para facilitar o uso.
Também é importante notar que o Redux Toolkit está totalmente integrado ao TypeScript. Para obter mais informações, consulte a seção Uso com TypeScript da documentação oficial.
Aplicação
Considere usar a biblioteca Redux Toolkit como um exemplo de um fragmento de um aplicativo React Redux realmente usado.
Nota Mais adiante neste artigo, o código fonte será apresentado sem o Redux Toolkit e com o mesmo, o que permitirá avaliar melhor os aspectos positivos e negativos do uso desta biblioteca.
Desafio
Em um de nossos aplicativos internos, havia a necessidade de adicionar, editar e exibir informações sobre as versões de nossos produtos de software. Para cada uma dessas ações, funções API separadas foram desenvolvidas, cujos resultados precisam ser adicionados ao repositório Redux. Como forma de controlar o comportamento assíncrono e os efeitos colaterais, usaremos o Thunk .
Criação de armazenamento
A versão inicial do código-fonte que cria o repositório tinha a seguinte aparência:
import { createStore, applyMiddleware, combineReducers, compose, } from 'redux'; import thunk from 'redux-thunk'; import * as reducers from './reducers'; const ext = window.__REDUX_DEVTOOLS_EXTENSION__; const devtoolMiddleware = ext && process.env.NODE_ENV === 'development' ? ext() : f => f; const store = createStore( combineReducers({ ...reducers, }), compose( applyMiddleware(thunk), devtoolMiddleware ) );
Se você observar atentamente o código acima, poderá ver uma sequência bastante longa de ações que devem ser concluídas para que o armazenamento esteja totalmente configurado. O Redux Toolkit contém uma ferramenta projetada para simplificar esse procedimento, a saber, a função configureStore.
Essa ferramenta permite combinar automaticamente redutores, adicionar middleware Redux (o padrão inclui redux-thunk) e também usar a extensão Redux DevTools. A função configureStore aceita um objeto com as seguintes propriedades como parâmetros de entrada:
- redutor - um conjunto de redutores personalizados,
- middleware - um parâmetro opcional que especifica uma matriz de middleware projetada para conectar-se ao repositório,
- devTools - um parâmetro do tipo lógico que permite ativar a extensão Redux DevTools instalada no navegador (o valor padrão é true),
- preloadedState - um parâmetro opcional que define o estado inicial do repositório,
- aprimoradores - um parâmetro opcional que define um conjunto de amplificadores.
Para obter a lista mais popular de middleware, você pode usar a função especial getDefaultMiddleware, que também faz parte do Redux Toolkit. Essa função retorna uma matriz com o middleware ativado por padrão na biblioteca do Redux Toolkit. A lista desses middlewares difere dependendo do modo em que seu código é executado. No modo de produção, uma matriz consiste em apenas um elemento - thunk. No modo de desenvolvimento, no momento da redação, a lista é reabastecida com o seguinte middleware:
- serializableStateInvariant - uma ferramenta especialmente desenvolvida para uso no Redux Toolkit e projetada para verificar a árvore de estados quanto à presença de valores não serializáveis, como funções, Promessa, Símbolo e outros valores que não são simples dados JS;
- immutableStateInvariant - middleware do pacote redux-immutable-state-invariant , projetado para detectar mutações nos dados armazenados no repositório.
Para especificar uma lista crescente de middleware, a função getDefaultMidlleware aceita um objeto que define a lista de middleware e as configurações incluídas para cada um deles. Mais informações sobre essas informações podem ser encontradas na seção correspondente da documentação oficial.
Agora, reescreveremos a seção de código responsável pela criação do repositório usando as ferramentas descritas acima. Como resultado, obtemos o seguinte:
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import * as reducers from './reducers'; const middleware = getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, thunk: true, }); export const store = configureStore({ reducer: { ...reducers }, middleware, devTools: process.env.NODE_ENV !== 'production', });
Usando o exemplo desta seção de código, você pode ver claramente que a função configureStore resolve os seguintes problemas:
- a necessidade de combinar redutores, chamando automaticamente combineReducers,
- a necessidade de combinar o middleware, chamando automaticamente o applyMiddleware.
Ele também permite que você ative mais convenientemente a extensão Redux DevTools usando a função composeWithDevTools do pacote redux-devtools-extension . Todos os itens acima indicam que o uso desta função permite tornar o código mais compacto e compreensível.
Isso conclui a criação e configuração do repositório. Transferimos para o provedor e continuamos.
Ações, criadores de ações e redutor
Agora, vejamos os recursos do Redux Toolkit em termos de desenvolvimento de ações, criadores de ações e redutor. A versão inicial do código sem usar o Redux Toolkit foi organizada como arquivos actions.js e reducers.js. O conteúdo do arquivo actions.js ficou assim:
import * as productReleasesService from '../../services/productReleases'; export const PRODUCT_RELEASES_FETCHING = 'PRODUCT_RELEASES_FETCHING'; export const PRODUCT_RELEASES_FETCHED = 'PRODUCT_RELEASES_FETCHED'; export const PRODUCT_RELEASES_FETCHING_ERROR = 'PRODUCT_RELEASES_FETCHING_ERROR'; … export const PRODUCT_RELEASE_UPDATING = 'PRODUCT_RELEASE_UPDATING'; export const PRODUCT_RELEASE_UPDATED = 'PRODUCT_RELEASE_UPDATED'; export const PRODUCT_RELEASE_CREATING_UPDATING_ERROR = 'PRODUCT_RELEASE_CREATING_UPDATING_ERROR'; function productReleasesFetching() { return { type: PRODUCT_RELEASES_FETCHING }; } function productReleasesFetched(productReleases) { return { type: PRODUCT_RELEASES_FETCHED, productReleases }; } function productReleasesFetchingError(error) { return { type: PRODUCT_RELEASES_FETCHING_ERROR, error } } … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched(productReleases)) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError(error)) }); } } … export function updateProductRelease( id, productName, productVersion, releaseDate ) { return dispatch => { dispatch(productReleaseUpdating()); return productReleasesService.updateProductRelease( id, productName, productVersion, releaseDate ).then( productRelease => dispatch(productReleaseUpdated(productRelease)) ).catch(error => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseCreatingUpdatingError(error)) }); } }
Conteúdo do arquivo reducers.js antes de usar o Redux Toolkit:
const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', updatingState: 'none', error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case productReleases.PRODUCT_RELEASES_FETCHING: return { ...state, fetchingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASES_FETCHED: return { ...state, productReleases: action.productReleases, fetchingState: 'success', }; case productReleases.PRODUCT_RELEASES_FETCHING_ERROR: return { ...state, fetchingState: 'failed', error: action.error }; … case productReleases.PRODUCT_RELEASE_UPDATING: return { ...state, updatingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASE_UPDATED: return { ...state, updatingState: 'success', productReleases: state.productReleases.map(productRelease => { if (productRelease.id === action.productRelease.id) return action.productRelease; return productRelease; }) }; case productReleases.PRODUCT_RELEASE_UPDATING_ERROR: return { ...state, updatingState: 'failed', error: action.error }; default: return state; } }
Como podemos ver, é aqui que a maior parte do clichê está contida: constantes do tipo de ação, criadores de ações, constantes novamente, mas no código redutor, leva tempo para escrever todo esse código. Você pode se livrar parcialmente desse padrão usando as funções createAction e createReducer, que também fazem parte do Redux Toolkit.
Função CreateAction
Na seção de código fornecida, o método padrão para definir uma ação no Redux é usado: primeiro, uma constante é definida separadamente que determina o tipo de ação e, em seguida - a função do criador da ação desse tipo. A função createAction combina essas duas declarações em uma. Na entrada, ele executa um tipo de ação e retorna o criador da ação para esse tipo. O criador da ação pode ser chamado sem argumentos ou com algum argumento (carga útil), cujo valor será colocado no campo de carga útil da ação criada. Além disso, o criador da ação substitui a função toString (), para que o tipo de ação se torne sua representação de sequência.
Em alguns casos, pode ser necessário escrever uma lógica adicional para ajustar o valor da carga, por exemplo, aceitar vários parâmetros para o criador da ação, criar um identificador aleatório ou obter o carimbo de data / hora atual. Para fazer isso, createAction usa um segundo argumento opcional - uma função que será usada para atualizar o valor da carga útil. Mais informações sobre este parâmetro podem ser encontradas na documentação oficial.
Usando a função createAction, obtemos o seguinte código:
export const productReleasesFetching = createAction('PRODUCT_RELEASES_FETCHING'); export const productReleasesFetched = createAction('PRODUCT_RELEASES_FETCHED'); export const productReleasesFetchingError = createAction('PRODUCT_RELEASES_FETCHING_ERROR'); … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched({ productReleases })) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })) }); } } ...
Função CreateReducer
Agora considere o redutor. Como no nosso exemplo, os redutores geralmente são implementados usando a instrução switch, com um registro para cada tipo de ação processada. Essa abordagem funciona bem, mas não está isenta de clichês e está sujeita a erros. Por exemplo, é fácil esquecer de descrever o caso padrão ou não definir o estado inicial. A função createReducer simplifica a criação de funções redutoras, definindo-as como tabelas de procura de função para processar cada tipo de ação. Ele também permite que você simplifique significativamente a lógica de atualizações imutáveis escrevendo código em um estilo "mutável" dentro de redutores.
Um estilo robusto de manipulação de eventos está disponível através do uso da biblioteca Immer . A função do manipulador pode "mudar" o estado passado para alterar as propriedades ou retornar um novo estado, como quando se trabalha no estilo imutável, mas graças a Immer, a mutação real do objeto não é realizada. A primeira opção é muito mais fácil para o trabalho e a percepção, especialmente ao alterar um objeto com aninhamento profundo.
Cuidado: o retorno de um novo objeto a partir de uma função substitui as alterações "mutáveis". O uso simultâneo de ambos os métodos de atualização de estado não funcionará.
A função createReducer aceita os seguintes argumentos como parâmetros de entrada:
- estado inicial de armazenamento
- um objeto que estabelece uma correspondência entre tipos de ações e redutores, cada um dos quais processa um determinado tipo.
Usando o método createReducer, obtemos o seguinte código:
const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const counterReducer = createReducer(initialState, { [productReleasesFetching]: (state, action) => { state.fetchingState = 'requesting' }, [productReleasesFetched.type]: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, [productReleasesFetchingError]: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … [productReleaseUpdating]: (state) => { state.updatingState = 'requesting' }, [productReleaseUpdated]: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, [productReleaseUpdatingError]: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, });
Como podemos ver, o uso das funções createAction e createReducer basicamente resolve o problema de escrever código extra, mas o problema de criar constantes antes ainda permanece. Portanto, consideramos uma opção mais poderosa que combina a geração de criadores e redutores de ação - a função createSlice.
Função CreateSlice
A função createSlice aceita um objeto com os seguintes campos como parâmetros de entrada:
- name - namespace das ações criadas (
${name}/${action.type}
); - initialState - estado inicial do redutor;
- redutores - um objeto com manipuladores. Cada manipulador assume uma função com argumentos estado e ação, ação contém dados na propriedade carga útil e o nome do evento na propriedade name. Além disso, é possível alterar preliminarmente os dados recebidos do evento antes que ele entre no redutor (por exemplo, adicione um ID aos elementos da coleção). Para fazer isso, em vez de uma função, você deve passar um objeto com o redutor e preparar os campos, em que redutor é a função do manipulador de ações e prepare é a função do manipulador de carga útil que retorna a carga útil atualizada;
- extraReducers - um objeto que contém redutores de outra fatia. Este parâmetro pode ser necessário se for necessário atualizar um objeto pertencente a outra fatia. Você pode aprender mais sobre essa funcionalidade na seção correspondente da documentação oficial.
O resultado da função é um objeto chamado "fatia", com os seguintes campos:
- nome - nome da fatia,
- redutor - redutor,
- ações - um conjunto de ações.
Usando esta função para resolver nosso problema, obtemos o seguinte código-fonte:
const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const productReleases = createSlice({ name: 'productReleases', initialState, reducers: { productReleasesFetching: (state) => { state.fetchingState = 'requesting'; }, productReleasesFetched: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, productReleasesFetchingError: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … productReleaseUpdating: (state) => { state.updatingState = 'requesting' }, productReleaseUpdated: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, productReleaseUpdatingError: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }, });
Agora, extrairemos os criadores e redutores de ação da fatia criada.
const { actions, reducer } = productReleases; export const { productReleasesFetched, productReleasesFetching, productReleasesFetchingError, … productReleaseUpdated, productReleaseUpdating, productReleaseUpdatingError } = actions; export default reducer;
O código-fonte dos criadores da ação que contém as chamadas da API não foi alterado, exceto pelo método de passar parâmetros ao enviar ações:
export const fetchProductReleases = () => (dispatch) => { dispatch(productReleasesFetching()); return productReleasesService .getProductReleases() .then((productReleases) => dispatch(productReleasesFetched({ productReleases }))) .catch((error) => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })); }); }; … export const updateProductRelease = (id, productName, productVersion, releaseDate) => (dispatch) => { dispatch(productReleaseUpdating()); return productReleasesService .updateProductRelease(id, productName, productVersion, releaseDate) .then((productRelease) => dispatch(productReleaseUpdated({ productRelease }))) .catch((error) => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseUpdatingError({ error })); });
O código acima mostra que a função createSlice permite que você se livre de uma parte significativa do clichê ao trabalhar com o Redux, o que permite não apenas tornar o código mais compacto, conciso e compreensível, como também gastar menos tempo escrevendo-o.
Sumário
No final deste artigo, gostaria de dizer que, apesar de a biblioteca Redux Toolkit não adicionar nada de novo ao gerenciamento de armazenamento, ela fornece vários meios muito mais convenientes para escrever código do que antes. Essas ferramentas permitem não apenas tornar o processo de desenvolvimento mais conveniente, compreensível e rápido, mas também mais eficaz devido à presença na biblioteca de várias ferramentas comprovadas. Inobitek, planejamos continuar a usar essa biblioteca no desenvolvimento de nossos produtos de software e monitorar novos desenvolvimentos promissores no campo das tecnologias da Web.
Obrigado pela atenção. Esperamos que nosso artigo seja útil. Mais informações sobre a biblioteca Redux Toolkit podem ser obtidas na documentação oficial.