We call a state everything that we store. But not all states are equally helpful. Here's the classification introduced by the Redux documentation:
- Domain data – application data that needs to be displayed, used, and modified. For example, a list of users downloaded from the server
- App state – data that determines the application's behavior. For example, the currently open URL
- UI state – data that determines how the UI looks, like displaying a list in the tile view
The store is the application's core, so we should describe the data inside it in terms of domain data and app state rather than as a tree of UI components. For example, we should not generate a state like state.leftPane.todoList.todos. It's rare for a component tree to reflect directly on a state structure, and that's okay. The view depends on the data, not the other way around.
A typical state structure looks like this:
{
domainData1 : {}, // Todos
domainData2 : {}, // Comments
appState1 : {},
appState2 : {},
uiState1 : {},
uiState2 : {},
}
You can find more details about working with UI states in the corresponding lesson.
We mentioned in the JS: React course that the state structure should resemble a database. Everything should be as flat and normalized as possible:
{
todos: [
{ id: 1, name: 'why?' },
{ id: 3, name: 'who?' },
],
comments: [
{ id: 23, todoId: 3, text: 'great!' },
],
}
With this structure, it's easy to write a reaction to actions, update, add, and delete data. There's a little nesting, so everything is nice and clear.
But there is one other problem which we'll always have to deal with. As the number of entities grows, the reducer gets quite hefty. It becomes a giant piece of code that does everything. To solve this problem, Redux has a built-in mechanism. It allows you to create multiple reducers and combine them.
It works like this: each top-level property gets its reducer, then we combine them into a root reducer using the combineReducers()
function. Then we use it to create the store:
import { combineReducers, createStore } from 'redux';
const todosReducer = (state = [], action) => {
// data from todos will go here
};
const commentsReducer = (state = [], action) => {
// data from comments will go here
};
const rootReducer = combineReducers({
todos: todosReducer,
comments: commentsReducer,
});
const store = createStore(rootReducer);
// If we call reducers as properties in the state, we can shorten the code:
// const todos = (state = [], action) => { ... };
// const comments = (state = [], action) => { ... };
// const rootReducer = combineReducers({ todos, comments });
The state comes to each reducer, but not the store's entire state, only the part in the corresponding property. Make sure to keep this in mind.
Reducers can even be nested without any special tools. They are ordinary functions that take data as input and return new data. If you use this approach, you will find a feature that seems frightening at first glance. Since each reducer has access only to its part of the state, actions that cause changes in several places at once will be in different reducers:
const todos = (state = {}, action) => {
switch (action.type) {
case 'TODO_REMOVE':
// ...
}
};
const comments = (state = {}, action) => {
switch (action.type) {
// When deleting a ToDo, make sure to remove all of its comments
case 'TODO_REMOVE':
// ...
}
};
In other words, we should repeat the part of the case
in the necessary reducers rather than try to get the missing parts of the state.
Recommended materials
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.