Redux is among us.
It is one of the most popular state managers.
It is predictable and simple to use. It helps to organize storing and mutating an app's data. If we take into account that actions and reducers are the part of redux, we can say that redux is a storage of all domain logic (aka business logic) of an app.
Is everything as rosy as it seems?
Despite all clarity and simplicity, we can face problems while using redux.
An app's data are placed in a common javascript object inside redux's state. It can be obtained in the usual way, we just need to know the path.
How can we structure that data? There are only two options: a flat list and a hierarchy model.
A flat list is a perfect option for an app that has, for example, only a simple counter. For a more complex app, we need to use a hierarchy structure where each next stage of depth has limited information about the whole data. It's ok but, as soon as data become more hierarchical the path to it become more complex.
const dataHierarchy = {
user: {
id,
name,
experience,
achievements: {
firstTraining,
threeTrainingsInRow,
},
},
training: {
currentSetId,
status,
totalAttemptsCount,
attemptsLeft,
mechanics: {
//...
},
},
};
At this example, a user's data is stored with user
path and it has sub-data achievements
. And achievements should not know anything about all user's data.
The same way with certain mechanics
- it shouldn't know how much user's attempts have left, it's a training's data.
A hierarchical data structure without a modular way to use it leads to the necessity to know a full path to a target data in each place where we wanna use it. In other words, it makes coupling of a data structure and a data displaying system and leads to difficulties with refactoring or/and restructuring that data.
Someone may say there is some powerful IDE and it can change a lot of paths by just a command. But it's hard to change complex paths or path that has some own part in a variable.
One more thing: tests. There is a large article about tests in the redux documentation. But what if we wanna test not simple units like a reducer or an action creator?
Data, actions, and reducers often are relative to each other. A single tree of logical relative data often servicing by several reducers that have to be tested both one by one and all together.
Add selectors into this list. Its results depend on reducers among other things.
So... we can test it all as independent units, but in this case, we'll need to work with objects without any references besides logic or conventions.
Ok, what if we made a structure with, i.e. a user data that includes friends list, favorite songs and something else and a way to control this data via actions and reducers. And, maybe we did write a lot of tests for the whole of this functionality.
And now we need the same at a sibling project...
How to share the code simple?
Looking for a solution
Before deciding how to keep advantages and to get rid of the disadvantages we need to research dependencies in a data lifecycle:
- Actions reports about events caused by a user or not
- Reducers react on actions and mutate or not mutate a state of data
- Data mutation is an event in itself and can be a reason for another data mutation
The controller here is an abstraction handling both user interactions and the state's data mutation. It is must not be a separate entity, it usually spread on components.
Consolidate all redux units into a black box
What if actions, reducers, and selectors will be united into a single module and teach it do not depend on a certain path to its own data?
What if all (for example) user
's actions will be done by calling an instance's method: user.addFriend(friendId)
? And data will be got by getter: user.getFriendsCount()
?
What if it'll be able to import the whole functionality of user
by simple import?
const userModule from ‘node_modules/user-module’;
Seems good? Especially considering that you won't need to write a lot of scaffolds:
npm package redux-module-creator provides all functionality for creation not coupled, reusable and testable redux-modules.
Each module consists of a controller, reducer, and actions and has the following advantages:
It can be integrated into a store wherever you want by calling method-integrator. And for changing the position we need to change only that integrator call and its parameter:
A controller has a ref with its own part of data in the store by storing the path that was once passed into the
integrator()
. This eliminates the need to know the path for data usage:
A controller is a container for all selectors, adapters, etc;
There is an opportunity to subscribe to changes of 'own' data;
Reducers can use
this
- the module instance for receiving the module's action types. This eliminates the need to import a lot of actions and reduce the probability of mistake;Actions got a context of usage so far as it became a part of the module: it's no longer
trainigFinished
, now it'sreadingModule.actions.trainingFinished
;Actions now inside a namespace, it makes possible to use the same names for actions in different modules;
Each module can be instantiated several times and each instance can be integrated into different parts on the store;
Actions from different instances of the same module have different action types, so it's possible to react to actions of certain instances.
As a result, we got a black box - a module that can manage its own data by itself and has an API to interact with outer code.
But it still the same redux with its one-way data flow, clarity, and predictability.
And since it still the same redux and still the same reducers, you can combine it whatever you want to build up any structure that your app needs.
Top comments (0)