Escalando um aplicativo Redux com patos

Antecipando o início do curso, o "desenvolvedor React.js." preparou uma tradução de material útil.





Como o front-end do seu aplicativo é escalado? Como fazer seu código suportar seis meses depois?

Em 2015, o Redux invadiu o mundo do desenvolvimento front-end e se estabeleceu como um padrão além do React.

A empresa em que trabalho recentemente terminou de refatorar uma grande base de código no React, onde implementamos o redux em vez do reflux .

Tivemos que dar esse passo, porque seguir em frente não era possível sem um aplicativo bem estruturado e um conjunto claro de regras.

A base de código existe há mais de dois anos e o refluxo está presente desde o início. Tivemos que mudar o código, fortemente vinculado aos componentes React, que ninguém tocou por mais de um ano.
Com base na experiência do trabalho realizado, criei este repositório , que ajudará a explicar nossa abordagem para organizar o código no redux.
Quando você aprende mais sobre redux, ações e redutores , começa com exemplos simples. Muitos tutoriais disponíveis hoje não vão além deles. No entanto, se você estiver criando algo mais complicado no Redux do que uma lista de tarefas, precisará de uma maneira razoável de escalar sua base de código ao longo do tempo.

Alguém disse uma vez que na ciência da computação não há tarefa mais difícil do que dar nomes a coisas diferentes. Eu não poderia discordar. Nesse caso, a estrutura de pastas e a organização de arquivos estarão em segundo lugar.

Vamos ver como costumávamos abordar a organização do código.

Função vs Recurso


Existem duas abordagens geralmente aceitas para organizar aplicativos: função primeiro e recurso primeiro.
Na captura de tela à esquerda, a estrutura da pasta é organizada de acordo com o princípio da função primeiro, e à direita - recurso primeiro.



Função primeiro significa que os diretórios de nível superior são nomeados de acordo com os arquivos contidos. Então você tem: contêineres, componentes, ações, redutores , etc.

Isso não é escalável. À medida que o aplicativo cresce e novas funcionalidades aparecem, você adiciona arquivos às mesmas pastas. Como resultado, você precisará rolar o conteúdo de uma das pastas por um longo tempo para encontrar o arquivo desejado.

Outro problema é combinar pastas. Um dos threads do seu aplicativo provavelmente exigirá acesso aos arquivos de todas as pastas.

Uma das vantagens dessa abordagem é que ela pode isolar, no nosso caso, o React do Redux. Portanto, se você quiser alterar a biblioteca de gerenciamento de estado, saberá quais pastas serão necessárias. Se você precisar alterar a biblioteca de exibição, poderá deixar as pastas com o redux intacto.

O recurso primeiro significa que os diretórios de nível superior serão nomeados de acordo com a principal funcionalidade do aplicativo: produto, carrinho, sessão.

Essa abordagem é muito melhor, pois cada novo recurso está em uma nova pasta. No entanto, você não tem uma separação entre os componentes Redux e React. Alterar um deles a longo prazo não é uma tarefa fácil.

Além disso, você terá arquivos que não pertencerão a nenhuma função. Como resultado, tudo se resume à pasta comum ou compartilhada, porque você também deseja usar seu código em diferentes recursos do seu aplicativo.

Combinando o melhor dos dois mundos


Embora este não seja o tópico do artigo, quero dizer que os arquivos de gerenciamento de estado dos arquivos da interface do usuário precisam ser armazenados separadamente.

Pense no seu aplicativo a longo prazo. Imagine o que acontece com o seu código se você mudar de React para outra coisa. Ou pense em como sua base de código usará o ReactNative em paralelo com a versão da web.

No centro de nossa abordagem está o princípio do isolamento React code em uma pasta chamada views e code redux em outra pasta chamada redux.

Essa separação no nível inicial nos dá a flexibilidade de organizar as partes individuais do aplicativo de maneiras completamente diferentes.

Dentro da pasta viewsapoiamos a organização de arquivos com função primeiro. No contexto do React, isso parece natural: páginas, layouts, componentes, aprimoradores etc.

Para não enlouquecer com o número de arquivos em uma pasta, você pode usar a abordagem do recurso primeiro nessas pastas.

Enquanto isso, na pasta redux ...

Apresentando re-patos


Cada função de aplicativo deve corresponder a ações e redutores separados, para que faça sentido aplicar a abordagem do primeiro recurso.

A abordagem modular originais patos torna fácil trabalhar com redux e oferece uma maneira estruturada para adicionar novas funcionalidades para a sua aplicação.

No entanto, você queria entender o que acontece quando você dimensiona o aplicativo. Percebemos que a maneira de organizar um arquivo por recurso atrapalha o aplicativo e torna seu suporte problemático.

Então re-patos apareceu . A solução foi dividir a funcionalidade em pastas de patos.

duck/
├── actions.js
├── index.js
├── operations.js
├── reducers.js
├── selectors.js
├── tests.js
├── types.js
├── utils.js

A pasta duck deve:

  • Contenha toda a lógica do processamento de apenas UM conceito do seu aplicativo, por exemplo: produto, carrinho, sessão, etc.
  • Contenha o arquivo index.js, que é exportado de acordo com as regras do duck.
  • Armazene o código em um único arquivo que faça um trabalho semelhante, como redutores, seletores e ações.
  • Contenha testes relacionados ao pato.

Por exemplo, neste exemplo, não usamos abstrações criadas sobre o redux. Ao criar software, é importante começar com o mínimo de abstração. Assim, você verá que o valor de suas abstrações não excede os benefícios delas.

Se você quiser garantir que a abstração seja ruim, assista a este vídeo de Cheng Lou .

Vamos ver o conteúdo de cada arquivo.

Tipos


O arquivo de tipos contém os nomes das ações que você executa no seu aplicativo. Como boa prática, você deve tentar cobrir o espaço para nome correspondente à função à qual eles pertencem. Essa abordagem ajudará na depuração de aplicativos complexos.

const QUACK = "app/duck/QUACK";
const SWIM = "app/duck/SWIM";

export default {
    QUACK,
    SWIM
};

Ações


Este arquivo contém todas as funções do criador da ação .

import types from "./types";

const quack = ( ) => ( {
    type: types.QUACK
} );

const swim = ( distance ) => ( {
    type: types.SWIM,
    payload: {
        distance
    }
} );

export default {
    swim,
    quack
};

Observe que todas as ações são representadas por funções, mesmo que não sejam parametrizadas. Uma abordagem consistente é a maior prioridade para uma grande base de código.

Operações


Para representar a cadeia de operações ( as Operações ), você precisará do middleware redux , para melhorar a função do envio . Exemplos populares são redux-thunk , redux-saga ou redux-observable .

No nosso caso, redux-thunk é usado . Precisamos separar thunks dos criadores de ação, mesmo com o custo de escrever código extra. Portanto, definiremos a operação como um invólucro sobre as ações .

Se a operação envia apenas uma ação , ou seja, na verdade não usa redux-thunk , enviamos a função criador da ação. Se a operação usar thunk , poderá enviar várias ações e vinculá-las usando promessas .

import actions from "./actions";

// This is a link to an action defined in actions.js.
const simpleQuack = actions.quack;

// This is a thunk which dispatches multiple actions from actions.js
const complexQuack = ( distance ) => ( dispatch ) => {
    dispatch( actions.quack( ) ).then( ( ) => {
        dispatch( actions.swim( distance ) );
        dispatch( /* any action */ );
    } );
}

export default {
    simpleQuack,
    complexQuack
};

Chame-os de operações, troncos , sagas, épicos, como quiser. Basta identificar os princípios de nomeação e cumpri-los.

No final, falaremos sobre o índice e veremos que as operações fazem parte da interface pública do duck. As ações são encapsuladas, as operações tornam-se acessíveis a partir do exterior.

Redutores


Se você tem uma função mais multifacetada, definitivamente deve usar vários redutores para lidar com estruturas de estado complexas. Além disso, não tenha medo de usar quantos combineReducers forem necessários. Isso permitirá trabalhar com estruturas de objetos de estado mais livremente.

import { combineReducers } from "redux";
import types from "./types";

/* State Shape
{
    quacking: bool,
    distance: number
}
*/

const quackReducer = ( state = false, action ) => {
    switch( action.type ) {
        case types.QUACK: return true;
        /* ... */
        default: return state;
    }
}

const distanceReducer = ( state = 0, action ) => {
    switch( action.type ) {
        case types.SWIM: return state + action.payload.distance;
        /* ... */
        default: return state;
    }
}

const reducer = combineReducers( {
    quacking: quackReducer,
    distance: distanceReducer
} );

export default reducer;

Em um aplicativo grande, a árvore de estados consistirá em pelo menos três níveis. As funções do redutor devem ser as menores possíveis e manipular apenas construções de dados simples. A função combineReducers é tudo o que você precisa para criar uma estrutura de estado flexível e sustentável.

Confira o exemplo completo do projeto e veja como usar o combineReducers corretamente , especialmente em arquivos reducers.jse store.jsonde estamos construindo a árvore de estados.

Seletores


Junto com o seletor de operações ( seletor ) fazem parte do duck da interface pública. A diferença entre operações e seletores é semelhante ao padrão CQRS .

As funções do seletor pegam uma fatia do estado do aplicativo e retornam alguns dados com base nele. Eles nunca fazem alterações no estado do aplicativo.

function checkIfDuckIsInRange( duck ) {
    return duck.distance > 1000;
}

export default {
    checkIfDuckIsInRange
};

Índice


Este arquivo indica o que será exportado da pasta duck.
É ele:

  • Exporta a função redutora do duck por padrão.
  • Exporta seletores e operações como exportações registradas.
  • Exporta tipos, se necessário, em outros patos.

import reducer from "./reducers";

export { default as duckSelectors } from "./selectors";
export { default as duckOperations } from "./operations";
export { default as duckTypes } from "./types";

export default reducer;

Testes


A vantagem de usar o Redux com a estrutura de patos é que você pode escrever testes logo após o código que deseja testar.

Testar seu código no Redux é bem simples:

import expect from "expect.js";
import reducer from "./reducers";
import actions from "./actions";

describe( "duck reducer", function( ) {
    describe( "quack", function( ) {
        const quack = actions.quack( );
        const initialState = false;

        const result = reducer( initialState, quack );

        it( "should quack", function( ) {
            expect( result ).to.be( true ) ;
        } );
    } );
} );

Dentro deste arquivo, você pode escrever testes para redutores , operações, seletores etc.
Eu poderia escrever um artigo inteiro sobre os benefícios do teste de código, mas eles já são suficientes, então apenas teste seu código!

Isso é tudo


A boa notícia sobre re-ducks é que você pode usar o mesmo modelo para todo o seu código redux.

O em funcionalidades abordagem de particionamento baseada em seu código redux ajuda a sua estadia aplicação flexível e escalável à medida que cresce. Uma abordagem de separação baseada em funções funcionará bem ao criar pequenos componentes comuns a diferentes partes do aplicativo.

Você pode dar uma olhada na base de código completa do exemplo react-redux- aqui . Lembre-se de que o repositório está funcionando ativamente.

Como você organiza seus aplicativos redux? Aguardo ansiosamente comentários sobre a abordagem descrita.

Vejo você no curso .

All Articles