Skaliranje vaše aplikacije Redux patkama

Kako se mjeri vaša prednja aplikacija? Kako osigurati održivost koda koji pišete za 6 mjeseci?

Redux je 2015. godine olujno obuzeo svijet front-end razvoja i uspostavio se kao standard - čak i izvan dometa React-a.

U tvrtki u kojoj radim nedavno smo završili s refaktoringom prilično velike baze React koda dodajući reduks umjesto refluksa.

Učinili smo to jer bi napredak bio nemoguć bez dobro strukturirane aplikacije i dobrog skupa pravila.

Baza kodova stara je više od dvije godine i refluks je bio tamo od početka. Morali smo promijeniti kod koji nije dodirivan više od godinu dana i bio je prilično zapetljan s React komponentama.

Na temelju rada koji smo obavili na projektu, sastavio sam ovaj repo, objašnjavajući naš pristup u organizaciji našeg redux koda.

Kad naučite o reduksu i ulogama radnji i reduktora, započinjete s vrlo jednostavnim primjerima. Većina danas dostupnih vodiča ne prelazi na sljedeću razinu. Ali ako s Reduxom gradite nešto što je složenije od popisa zadataka, trebat će vam pametniji način skaliranja vaše baze koda s vremenom.

Netko je jednom rekao da je imenovanje stvari jedan od najtežih poslova u računalnim znanostima. Ne bih se mogao više složiti. Ali strukturiranje mapa i organiziranje datoteka je blizu druge.

Istražimo kako smo pristupali organizaciji koda u prošlosti.

Funkcija vs značajka

Dva su uspostavljeni pristupi strukturiranju prijava: funkcija-prvi i značajka-prvi .

S lijeve strane dolje možete vidjeti strukturu mape koja ima prvu funkciju. S desne strane možete vidjeti pristup prvom značajkom.

Funkcija prva znači da su vaši direktoriji najviše razine imenovani prema namjeni datoteka u njima. Dakle, imate: spremnike , komponente , akcije , reduktore itd.

Ovo se uopće ne mjeri. Kako vaša aplikacija raste, a vi dodajete više značajki, dodajete datoteke u iste mape. Dakle, na kraju ćete morati pomaknuti se unutar jedne mape da biste pronašli svoju datoteku.

Problem je i u spajanju mapa. Za jedan protok kroz vašu aplikaciju vjerojatno će biti potrebne datoteke iz svih mapa.

Jedna od prednosti ovog pristupa je što izolira - u našem slučaju - reagira od reduksa. Dakle, ako želite promijeniti knjižnicu upravljanja državama, znate koje mape morate dodirnuti. Ako promijenite biblioteku prikaza, svoje redux mape možete zadržati netaknutima.

Značajka prvo znači da su direktoriji najviše razine nazvani prema glavnim značajkama aplikacije: proizvod , košarica , sesija .

Ovaj se pristup puno bolje skalira, jer svaka nova značajka dolazi s novom mapom. Ali, nemate razdvajanja između React komponenata i reduxa. Dugoročno mijenjanje jednog od njih vrlo je lukav posao.

Uz to imate datoteke koje ne pripadaju nijednoj značajci. Na kraju ćete dobiti zajedničku ili dijeljenu mapu jer želite ponovno koristiti kôd za mnoge značajke u svojoj aplikaciji.

Najbolje od dva svijeta

Iako nije u dosegu ovog članka, želim dodirnuti ovu jedinu ideju: uvijek odvojite datoteke Državnog upravljanja od UI datoteka.

Razmislite o svojoj prijavi na duge staze. Zamislite što se događa s kodnom bazom kada se prebacite s React-a na drugu knjižnicu. Ili razmislite kako bi vaša baza kodova koristila ReactNative paralelno s web verzijom.

Naš pristup polazi od potrebe izoliranja React koda u jednu mapu - koja se naziva pogledi - i redux koda u zasebnu mapu - koja se naziva redux.

Ova podjela na prvoj razini daje nam fleksibilnost da dva različita dijela aplikacije organiziramo potpuno različito.

Unutar mape pogleda, mi preferiramo pristup funkciji u strukturiranju datoteka. To se osjeća vrlo prirodno u kontekstu React-a: stranice , izgledi , komponente, poboljšači itd.

Da ne bismo poludjeli s brojem datoteka u mapi, možda ćemo u svakoj od ovih mapa imati podijeljenu na temelju značajki.

Zatim, unutar mape redux ...

Unesite re-patke

Svaka značajka aplikacije trebala bi se mapirati u odvojene radnje i reduktore, pa ima smisla odabrati pristup koji je prvi.

Izvorni modularni pristup patkama lijepo je pojednostavljenje reduksa i nudi strukturirani način dodavanja svake nove značajke u vašu aplikaciju.

Ipak, željeli smo malo istražiti što se događa kada se aplikacija skalira. Shvatili smo da jedna datoteka za značajku postaje previše pretrpana i teško ju je dugoročno održavati.

Tako su rođene re-patke . Rješenje je bilo podijeliti svaku značajku u mačju patku .

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

Mapa patka MORA:

  • sadrže cjelokupnu logiku za rukovanje samo JEDNIM konceptom u vašoj aplikaciji, npr. proizvod , košarica , sesija itd.
  • imati index.jsdatoteku koja se izvozi prema izvornim pravilima o patkama.
  • zadržati kôd slične namjene u istoj datoteci, poput reduktora , selektora i radnji
  • sadrže testove povezane s patkom.

U ovom primjeru nismo koristili apstrakciju izgrađenu na vrhu reduksa. Kada gradite softver, važno je započeti s najmanje apstrakcija. Na ovaj način osiguravate da troškovi apstrakcija ne nadmašuju koristi.

Ako se trebate uvjeriti da apstrakcije mogu biti loše, pogledajte ovaj sjajni govor Cheng Lou-a.

Pogledajmo što ide u svaku datoteku.

Vrste

Datoteka vrsta sadrži nazive radnji koje šaljete u svojoj aplikaciji. Kao dobru praksu, pokušajte obuhvatiti nazive na temelju značajke kojoj pripadaju. To pomaže pri otklanjanju pogrešaka u složenijim programima.

const QUACK = "app/duck/QUACK"; const SWIM = "app/duck/SWIM"; export default { QUACK, SWIM };

Akcije

Ova datoteka sadrži sve funkcije kreatora radnji.

import types from "./types"; const quack = ( ) => ( { type: types.QUACK } ); const swim = ( distance ) => ( { type: types.SWIM, payload: { distance } } ); export default { swim, quack };

Notice how all the actions are represented by functions, even if they are not parametrized. A consistent approach is more than needed in a large codebase.

Operations

To represent chained operations you need a redux middleware to enhance the dispatch function. Some popular examples are: redux-thunk, redux-saga or redux-observable.

In our case, we use redux-thunk. We want to separate the thunks from the action creators, even with the cost of writing extra code. So we define an operation as a wrapper over actions.

If the operation only dispatches a single action — doesn’t actually use redux-thunk — we forward the action creator function. If the operation uses a thunk, it can dispatch many actions and chain them with promises.

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 };

Call them operations, thunks, sagas, epics, it’s your choice. Just find a naming convention and stick with it.

At the end, when we discuss the index, we’ll see that the operations are part of the public interface of the duck. Actions are encapsulated, operations are exposed.

Reducers

If a feature has more facets, you should definitely use multiple reducers to handle different parts of the state shape. Additionally, don’t be afraid to use combineReducers as much as needed. This gives you a lot of flexibility when working with a complex state shape.

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;

In a large scale application, your state tree will be at least 3 level deep. Reducer functions should be as small as possible and handle only simple data constructs. The combineReducers utility function is all you need to build a flexible and maintainable state shape.

Check out the complete example project and look how combineReducers is used. Once in the reducers.js files and then in the store.js file, where we put together the entire state tree.

Selectors

Together with the operations, the selectors are part of the public interface of a duck. The split between operations and selectors resembles the CQRS pattern.

Selector functions take a slice of the application state and return some data based on that. They never introduce any changes to the application state.

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

Index

This file specifies what gets exported from the duck folder. It will:

  • export as default the reducer function of the duck.
  • export as named exports the selectors and the operations.
  • export the types if they are needed in other ducks.
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;

Tests

A benefit of using Redux and the ducks structure is that you can write your tests next to the code you are testing.

Testing your Redux code is fairly straight-forward:

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 ) ; } ); } ); } );

Inside this file you can write tests for reducers, operations, selectors, etc.

I could write a whole different article about the benefits of testing your code, there are so many of them. Just do it!

So there it is

The nice part about re-ducks is that you get to use the same pattern for all your redux code.

The feature-based split for the redux code is much more flexible and scalable as your application codebase grows. And the function-based split for views works when you build small components that are shared across the application.

You can check out a full react-redux-example codebase here. Just keep in mind that the repo is still under active development.

How do you structure your redux apps? I’m looking forward to hearing some feedback on this approach I’ve presented.

If you found this article useful, click on the green heart below and I will know my efforts are not in vain.