Часть 1: Введение и основы управления состоянием приложения
В мире современной веб-разработки управление состоянием приложения стало ключевым фактором, определяющим успех и эффективность проектов. Независимо от того, работаете ли вы с React, Vue.js или Angular, понимание концепций и инструментов управления состоянием критически важно для создания масштабируемых и поддерживаемых приложений.
1. Введение
Определение состояния приложения
Состояние приложения — это совокупность всех данных, которые могут изменяться с течением времени и влияют на поведение и отображение пользовательского интерфейса. Это может включать в себя данные, полученные с сервера, пользовательский ввод, настройки интерфейса и многое другое.
Важность управления состоянием в современных веб-приложениях
По мере роста сложности веб-приложений, управление их состоянием становится все более сложной задачей. Эффективное управление состоянием позволяет:
- Улучшить производительность приложения
- Упростить отладку и тестирование
- Обеспечить предсказуемое поведение интерфейса
- Облегчить масштабирование и поддержку кода
Краткий обзор Redux, Vuex и NgRx
В этой статье мы рассмотрим три популярных решения для управления состоянием:
- Redux — библиотека для JavaScript-приложений, часто используемая с React.
- Vuex — паттерн и библиотека управления состоянием для Vue.js.
- NgRx — фреймворк управления состоянием для Angular, вдохновленный Redux.
Каждое из этих решений предлагает свой подход к организации и управлению данными в приложении, но все они основаны на схожих принципах.
2. Основы управления состоянием
Что такое состояние приложения?
Представьте себе современное веб-приложение как сложный механизм, где каждая шестеренка — это отдельный компонент или модуль. Состояние приложения — это масло, которое позволяет всем этим шестеренкам работать слаженно и эффективно.
Состояние может включать:
- Данные пользователя
- Настройки приложения
- Результаты API-запросов
- Текущий вид интерфейса
- Промежуточные данные вычислений
Проблемы, возникающие при управлении состоянием
С ростом приложения управление его состоянием становится все более сложным. Вот некоторые распространенные проблемы:
- Сложность отслеживания изменений: Когда данные могут изменяться в разных частях приложения, становится трудно понять, где и почему произошло изменение.
- Несогласованность данных: Различные компоненты могут иметь разные версии одних и тех же данных, что приводит к ошибкам и неожиданному поведению.
- Сложность отладки: Без четкой структуры управления состоянием отладка становится настоящим кошмаром.
- Проблемы с производительностью: Неэффективное управление состоянием может привести к ненужным обновлениям компонентов и снижению производительности.
Паттерн Flux и его влияние на современные решения
Паттерн Flux, представленный Facebook в 2014 году, стал революционным подходом к управлению состоянием. Его ключевая идея — однонаправленный поток данных, который значительно упрощает понимание того, как данные изменяются в приложении.
Основные принципы Flux:
- Единое хранилище данных
- Данные могут изменяться только через явные действия (actions)
- Изменения происходят через чистые функции (reducers)
Эти принципы легли в основу Redux, а затем повлияли на разработку Vuex и NgRx, адаптировав идеи Flux для конкретных фреймворков.
3. Ключевые концепции управления состоянием
Однонаправленный поток данных
Однонаправленный поток данных — это фундаментальный принцип современного управления состоянием. Он означает, что данные в приложении движутся только в одном направлении, что делает поток информации более предсказуемым и легким для понимания.
Схема работы:
- Действие инициируется (например, пользователь кликает кнопку)
- Действие обрабатывается и изменяет состояние
- Компоненты обновляются в соответствии с новым состоянием
Этот подход значительно упрощает отладку, так как вы всегда знаете, откуда пришли изменения.
Иммутабельность
Иммутабельность — это принцип, согласно которому объекты после их создания не могут быть изменены. Вместо изменения существующих объектов создаются новые с обновленными данными.
Преимущества иммутабельности:
- Упрощает отслеживание изменений
- Улучшает производительность за счет быстрого сравнения объектов
- Обеспечивает предсказуемое поведение приложения
Централизованное хранилище
Централизованное хранилище (store) — это единое место для хранения состояния всего приложения. Это ключевая концепция в Redux, Vuex и NgRx.
Преимущества централизованного хранилища:
- Упрощает понимание структуры данных приложения
- Облегчает отладку и тестирование
- Позволяет легко реализовать такие функции, как отмена/повтор действий
Чистые функции и редьюсеры
Чистые функции — это функции, которые при одинаковых входных данных всегда возвращают одинаковый результат и не имеют побочных эффектов. Редьюсеры в контексте управления состоянием — это чистые функции, которые принимают текущее состояние и действие, и возвращают новое состояние.
Почему это важно:
- Предсказуемость: поведение редьюсеров легко прогнозировать и тестировать
- Отсутствие побочных эффектов: редьюсеры не изменяют данные за пределами своей области видимости
- Простота понимания: логика изменения состояния сконцентрирована в одном месте
Понимание этих ключевых концепций создает прочную основу для изучения конкретных решений управления состоянием, таких как Redux, Vuex и NgRx, которые мы рассмотрим в следующих частях нашего руководства.
В следующей части мы углубимся в Redux — популярное решение для управления состоянием в React-приложениях, и рассмотрим, как оно воплощает эти концепции на практике.
Часть 2: Redux — популярное решение для React
Redux стал синонимом эффективного управления состоянием в мире React-разработки. Давайте погрузимся в мир Redux и узнаем, почему эта библиотека завоевала сердца разработчиков по всему миру.
4. Введение в Redux
История создания и философия Redux
Redux был создан Дэном Абрамовым и Эндрю Кларком в 2015 году как ответ на сложности управления состоянием в крупных React-приложениях. Вдохновленный архитектурой Flux и функциональным программированием, Redux предложил простой и предсказуемый способ управления состоянием.
Философия Redux базируется на трех ключевых принципах:
- Единственный источник правды
- Состояние только для чтения
- Изменения с помощью чистых функций
Основные принципы Redux
- Единственный источник правды: Все состояние приложения хранится в одном централизованном store. Это упрощает отладку и позволяет легко сериализовать состояние для сохранения или передачи.
- Состояние только для чтения: Единственный способ изменить состояние — это отправить action. Это гарантирует, что ни представления, ни сетевые колбэки никогда не изменят состояние напрямую.
- Изменения с помощью чистых функций: Для определения того, как действия трансформируют состояние, используются чистые функции, называемые reducers.
Экосистема Redux
Redux обладает богатой экосистемой инструментов и библиотек, которые расширяют его возможности:
- Redux Toolkit: Официальный набор инструментов для эффективной Redux-разработки.
- Redux-Saga и Redux-Thunk: Middleware для обработки побочных эффектов.
- Reselect: Библиотека для создания мемоизированных селекторов.
- Redux DevTools: Мощное расширение браузера для отладки Redux-приложений.
5. Архитектура Redux
Store (хранилище)
Store в Redux — это объект, который содержит полное состояние приложения. Это единственный источник правды в Redux-приложении.
Ключевые особенности store:
- Хранит состояние приложения
- Предоставляет доступ к состоянию через
getState() - Позволяет обновлять состояние через
dispatch(action) - Регистрирует слушателей через
subscribe(listener)
import { createStore } from 'redux';
const store = createStore(rootReducer);
Actions (действия)
Actions — это простые JavaScript-объекты, которые описывают, что произошло в приложении. Они являются единственным способом передачи данных из приложения в store.
Структура action:
{
type: 'ADD_TODO',
payload: {
text: 'Изучить Redux'
}
}
Reducers (редьюсеры)
Reducers — это чистые функции, которые принимают текущее состояние и action, и возвращают новое состояние. Они определяют, как состояние приложения изменяется в ответ на actions.
Пример простого reducer:
function todoReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload];
default:
return state;
}
}
Middleware (промежуточное ПО)
Middleware в Redux предоставляет точку расширения между отправкой action и моментом, когда он достигает reducer. Middleware используется для логирования, обработки асинхронных действий, маршрутизации и многого другого.
Пример использования middleware:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(rootReducer, applyMiddleware(thunk));
6. Работа с Redux
Настройка Redux в проекте
Для начала работы с Redux в React-проекте, нужно установить необходимые пакеты:
npm install redux react-redux @reduxjs/toolkit
Затем создаем store и оборачиваем корневой компонент в Provider:
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
const store = configureStore({ reducer: rootReducer });
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Создание actions и reducers
С Redux Toolkit создание actions и reducers становится проще благодаря createSlice:
import { createSlice } from '@reduxjs/toolkit';
const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload);
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
}
}
});
export const { addTodo, toggleTodo } = todoSlice.actions;
export default todoSlice.reducer;
Подключение компонентов к store
Для работы с Redux в компонентах React используются хуки useSelector и useDispatch:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo } from './todoSlice';
function TodoList() {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const handleAddTodo = (text) => {
dispatch(addTodo({ id: Date.now(), text, completed: false }));
};
const handleToggleTodo = (id) => {
dispatch(toggleTodo(id));
};
// Рендеринг списка задач
}
Асинхронные операции в Redux
Для обработки асинхронных операций в Redux часто используется middleware, такой как Redux Thunk. С Redux Toolkit можно использовать createAsyncThunk:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const response = await fetch('/api/todos');
return response.json();
}
);
const todoSlice = createSlice({
name: 'todos',
initialState: { items: [], status: 'idle', error: null },
reducers: {
// ... другие reducers
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
7. Лучшие практики и оптимизация Redux
Структурирование кода
- Используйте подход «feature-first» или «ducks» для организации файлов Redux.
- Группируйте связанные actions, reducers и селекторы в одном файле или директории.
- Используйте Redux Toolkit для уменьшения шаблонного кода.
Нормализация данных
Нормализация данных в Redux store помогает избежать дублирования и упрощает обновление данных:
{
entities: {
todos: {
byId: {
1: { id: 1, text: 'Изучить Redux', completed: false },
2: { id: 2, text: 'Написать приложение', completed: true }
},
allIds: [1, 2]
}
}
}
Селекторы и мемоизация
Используйте селекторы для извлечения данных из store. Библиотека Reselect позволяет создавать мемоизированные селекторы для оптимизации производительности:
import { createSelector } from 'reselect';
const getTodos = state => state.todos;
const getFilter = state => state.visibilityFilter;
export const getVisibleTodos = createSelector(
[getTodos, getFilter],
(todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed);
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
);
Инструменты разработчика Redux
Redux DevTools — мощное расширение для браузера, которое позволяет:
- Просматривать историю действий и состояния
- Отменять и повторять действия
- Экспортировать и импортировать состояние
- Путешествовать во времени по состояниям приложения
Для подключения Redux DevTools используйте:
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production',
});
Redux предоставляет мощный и гибкий подход к управлению состоянием в React-приложениях. Понимание его ключевых концепций и следование лучшим практикам позволяет создавать масштабируемые и легко поддерживаемые приложения.
В следующей части нашего руководства мы рассмотрим Vuex — решение для управления состоянием в экосистеме Vue.js, и сравним его подход с Redux.
Часть 3: Vuex — управление состоянием для Vue.js
Vuex — это паттерн и библиотека управления состоянием для приложений на Vue.js. Он служит централизованным хранилищем для всех компонентов приложения с правилами, обеспечивающими предсказуемость изменения состояния. Давайте погрузимся в мир Vuex и узнаем, как он помогает разработчикам Vue.js создавать масштабируемые приложения.
8. Знакомство с Vuex
Особенности Vuex и его отличия от Redux
Vuex, вдохновленный концепциями Flux и Redux, имеет свои уникальные особенности:
- Интеграция с Vue: Vuex тесно интегрирован с Vue.js, что обеспечивает более гладкий опыт разработки.
- Реактивность: Vuex использует реактивную систему Vue для автоматического обновления компонентов при изменении состояния.
- Модульность: Vuex поддерживает модульную архитектуру, что упрощает организацию кода в крупных приложениях.
- Простота использования: Vuex имеет более простой API и меньше шаблонного кода по сравнению с Redux.
Основные концепции Vuex
Vuex работает вокруг нескольких ключевых концепций:
- State: Единое состояние, являющееся «единственным источником истины».
- Getters: Вычисляемые свойства на основе состояния.
- Mutations: Синхронные функции для изменения состояния.
- Actions: Асинхронные операции, которые могут вызывать мутации.
- Modules: Способ разделения хранилища на управляемые части.
Интеграция Vuex с Vue.js
Интеграция Vuex с Vue.js проста и элегантна. Вот базовый пример:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
new Vue({
el: '#app',
store,
// ...
})
9. Компоненты Vuex
State (состояние)
State в Vuex — это объект, содержащий все данные приложения. Он выступает в качестве «единственного источника истины»:
const store = new Vuex.Store({
state: {
todos: [],
user: null
}
})
Доступ к состоянию в компонентах Vue можно получить через this.$store.state или с помощью хелпера mapState:
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['todos', 'user'])
}
}
Getters (геттеры)
Getters позволяют получать производные данные из состояния. Они работают как вычисляемые свойства для хранилища:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: 'Изучить Vue', done: true },
{ id: 2, text: 'Изучить Vuex', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
Использование геттеров в компонентах:
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['doneTodos'])
}
}
Mutations (мутации)
Mutations — это единственный способ изменения состояния в Vuex. Они всегда синхронны:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
state.count++
},
incrementBy (state, payload) {
state.count += payload.amount
}
}
})
Вызов мутаций:
store.commit('increment')
store.commit('incrementBy', { amount: 10 })
Actions (действия)
Actions используются для выполнения асинхронных операций. Они не изменяют состояние напрямую, а вызывают мутации:
const store = new Vuex.Store({
state: {
todos: []
},
mutations: {
setTodos (state, todos) {
state.todos = todos
}
},
actions: {
async fetchTodos ({ commit }) {
const response = await fetch('/api/todos')
const todos = await response.json()
commit('setTodos', todos)
}
}
})
Вызов действий:
store.dispatch('fetchTodos')
Modules (модули)
Модули позволяют разделить хранилище Vuex на небольшие части:
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
10. Практическое использование Vuex
Настройка хранилища Vuex
Для начала работы с Vuex, установите его через npm:
npm install vuex
Затем создайте файл store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// Ваше начальное состояние
},
mutations: {
// Ваши мутации
},
actions: {
// Ваши действия
},
getters: {
// Ваши геттеры
}
})
Работа с состоянием и геттерами
В компонентах Vue вы можете получить доступ к состоянию и геттерам через this.$store или используя хелперы mapState и mapGetters:
<template>
<div>
<p>{{ count }}</p>
<p>{{ doubleCount }}</p>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
}
}
</script>
Изменение состояния через мутации
Для изменения состояния используйте мутации:
// В хранилище
mutations: {
INCREMENT (state) {
state.count++
}
}
// В компоненте
methods: {
increment () {
this.$store.commit('INCREMENT')
}
}
Асинхронные операции с помощью actions
Действия позволяют выполнять асинхронные операции:
// В хранилище
actions: {
fetchTodos ({ commit }) {
return fetch('/api/todos')
.then(response => response.json())
.then(todos => {
commit('SET_TODOS', todos)
})
}
}
// В компоненте
methods: {
fetchTodos () {
this.$store.dispatch('fetchTodos')
}
}
Модульная архитектура в крупных приложениях
Для крупных приложений рекомендуется использовать модули. Вот пример структуры:
store/
├── index.js # корневой модуль
├── actions.js # корневые действия
├── mutations.js # корневые мутации
└── modules/
├── cart.js # модуль корзины
└── products.js # модуль продуктов
11. Продвинутые техники Vuex
Плагины Vuex
Плагины позволяют расширять функциональность Vuex. Например, плагин для сохранения состояния в localStorage:
const localStoragePlugin = store => {
store.subscribe((mutation, state) => {
localStorage.setItem('store', JSON.stringify(state))
})
}
const store = new Vuex.Store({
// ...
plugins: [localStoragePlugin]
})
Строгий режим и горячая перезагрузка
Строгий режим помогает обнаружить неправильные мутации состояния:
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
Vuex поддерживает горячую перезагрузку мутаций, модулей, действий и геттеров во время разработки.
Тестирование Vuex-хранилища
Для тестирования действий и мутаций можно использовать Jest:
// Тестирование мутации
test('INCREMENT mutation', () => {
const state = { count: 0 }
mutations.INCREMENT(state)
expect(state.count).toBe(1)
})
// Тестирование действия
test('fetchTodos action', async () => {
const commit = jest.fn()
const todos = [{ id: 1, text: 'Test' }]
global.fetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve(todos)
})
await actions.fetchTodos({ commit })
expect(commit).toHaveBeenCalledWith('SET_TODOS', todos)
})
Типизация Vuex с использованием TypeScript
Vuex поддерживает TypeScript, что позволяет улучшить систему типов и повысить надежность кода:
import { ActionTree, MutationTree, GetterTree, Module } from 'vuex'
interface RootState {
count: number
}
const state: RootState = {
count: 0
}
const mutations: MutationTree = {
INCREMENT(state) {
state.count++
}
}
const actions: ActionTree = {
increment({ commit }) {
commit('INCREMENT')
}
}
const getters: GetterTree = {
doubleCount: (state) => state.count * 2
}
const module: Module = {
state,
mutations,
actions,
getters
}
export default module
Vuex предоставляет мощный и гибкий способ управления состоянием в приложениях Vue.js. Его интеграция с Vue, реактивная система и модульная архитектура делают его отличным выбором для проектов любого масштаба.
В следующей части нашего руководства мы рассмотрим NgRx — решение для управления состоянием в экосистеме Angular, и сравним его подходы с Redux и Vuex.
Часть 4: NgRx — решение для управления состоянием в Angular
NgRx — это фреймворк для управления состоянием в приложениях Angular, вдохновленный Redux. Он обеспечивает предсказуемость управления состоянием с помощью однонаправленного потока данных, неизменяемость и побочных эффектов. Давайте погрузимся в мир NgRx и узнаем, как он помогает разработчикам Angular создавать масштабируемые и поддерживаемые приложения.
12. Введение в NgRx
Особенности NgRx и его место в экосистеме Angular
NgRx тесно интегрирован с Angular и обладает рядом особенностей:
- Основан на RxJS: NgRx широко использует реактивное программирование с помощью RxJS.
- Строгая типизация: Благодаря TypeScript, NgRx обеспечивает строгую типизацию на всех уровнях.
- Инструменты разработки: NgRx предоставляет мощные инструменты для отладки и разработки.
- Модульность: NgRx поддерживает модульную архитектуру для больших приложений.
Основные концепции NgRx
NgRx работает вокруг следующих ключевых концепций:
- Store: Единое иммутабельное хранилище состояния приложения.
- Actions: Объекты, описывающие изменения в приложении.
- Reducers: Чистые функции, определяющие, как состояние изменяется в ответ на действия.
- Selectors: Функции для извлечения данных из состояния.
- Effects: Обработчики побочных эффектов для выполнения асинхронных операций.
Сравнение NgRx с Redux и Vuex
NgRx похож на Redux в своей основной концепции, но имеет некоторые отличия:
- NgRx более тесно интегрирован с Angular и использует возможности TypeScript.
- NgRx использует RxJS, что делает его более «реактивным».
- Effects в NgRx похожи на middleware в Redux, но реализованы через RxJS потоки.
По сравнению с Vuex, NgRx имеет более строгую структуру и больше шаблонного кода, но предоставляет более мощные инструменты для больших и сложных приложений.
13. Архитектура NgRx
Store и State
Store в NgRx — это единое иммутабельное хранилище состояния приложения:
import { createAction, createReducer, on } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export interface AppState {
count: number;
}
export const initialState: AppState = {
count: 0
};
export const counterReducer = createReducer(
initialState,
on(increment, state => ({ ...state, count: state.count + 1 })),
on(decrement, state => ({ ...state, count: state.count - 1 }))
);
Actions
Actions в NgRx описывают события, происходящие в приложении:
import { createAction, props } from '@ngrx/store';
export const addTodo = createAction(
'[Todo] Add Todo',
props<{ text: string }>()
);
export const removeTodo = createAction(
'[Todo] Remove Todo',
props<{ id: number }>()
);
Reducers
Reducers определяют, как состояние изменяется в ответ на действия:
import { createReducer, on } from '@ngrx/store';
import * as TodoActions from './todo.actions';
export interface TodoState {
todos: { id: number; text: string }[];
}
export const initialState: TodoState = {
todos: []
};
export const todoReducer = createReducer(
initialState,
on(TodoActions.addTodo, (state, { text }) => ({
...state,
todos: [...state.todos, { id: state.todos.length + 1, text }]
})),
on(TodoActions.removeTodo, (state, { id }) => ({
...state,
todos: state.todos.filter(todo => todo.id !== id)
}))
);
Selectors
Selectors используются для извлечения данных из состояния:
import { createSelector } from '@ngrx/store';
export const selectTodos = (state: AppState) => state.todos;
export const selectTodoCount = createSelector(
selectTodos,
todos => todos.length
);
Effects
Effects обрабатывают побочные эффекты, такие как запросы к API:
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { TodoService } from './todo.service';
@Injectable()
export class TodoEffects {
loadTodos$ = createEffect(() => this.actions$.pipe(
ofType('[Todos] Load Todos'),
mergeMap(() => this.todoService.getTodos()
.pipe(
map(todos => ({ type: '[Todos] Todos Loaded Success', payload: todos })),
catchError(() => EMPTY)
))
)
);
constructor(
private actions$: Actions,
private todoService: TodoService
) {}
}
14. Реализация NgRx в Angular-приложении
Установка и настройка NgRx
Для начала работы с NgRx установите необходимые пакеты:
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Затем настройте корневой модуль приложения:
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { counterReducer } from './counter.reducer';
import { TodoEffects } from './todo.effects';
@NgModule({
imports: [
StoreModule.forRoot({ count: counterReducer }),
EffectsModule.forRoot([TodoEffects]),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production
})
],
// ...
})
export class AppModule { }
Определение состояния и создание actions
Определите интерфейс состояния и создайте действия:
export interface AppState {
todos: Todo[];
}
export const addTodo = createAction('[Todo] Add Todo', props<{ text: string }>());
export const removeTodo = createAction('[Todo] Remove Todo', props<{ id: number }>());
Написание reducers и selectors
Создайте reducer для обработки действий:
export const todoReducer = createReducer(
initialState,
on(addTodo, (state, { text }) => ({ ...state, todos: [...state.todos, { id: state.todos.length + 1, text }] })),
on(removeTodo, (state, { id }) => ({ ...state, todos: state.todos.filter(todo => todo.id !== id) }))
);
export const selectTodos = (state: AppState) => state.todos;
Работа с effects для асинхронных операций
Используйте effects для обработки асинхронных операций:
@Injectable()
export class TodoEffects {
loadTodos$ = createEffect(() =>
this.actions$.pipe(
ofType('[Todos] Load Todos'),
mergeMap(() =>
this.todoService.getTodos().pipe(
map(todos => ({ type: '[Todos] Todos Loaded Success', payload: todos })),
catchError(() => EMPTY)
)
)
)
);
constructor(
private actions$: Actions,
private todoService: TodoService
) {}
}
Использование NgRx в компонентах
В компонентах используйте Store для доступа к состоянию и отправки действий:
@Component({
selector: 'app-todo-list',
template: `
<ul>
<li *ngFor="let todo of todos$ | async">
{{ todo.text }}
<button (click)="removeTodo(todo.id)">Remove</button>
<</li>
</ul>
<input #todoInput>
<button (click)="addTodo(todoInput.value)">Add Todo</button>
`
})
export class TodoListComponent {
todos$ = this.store.select(selectTodos);
constructor(private store: Store<AppState>) {}
addTodo(text: string) {
this.store.dispatch(addTodo({ text }));
}
removeTodo(id: number) {
this.store.dispatch(removeTodo({ id }));
}
}
15. Лучшие практики и дополнительные возможности NgRx
Организация кода в крупных проектах
Для крупных проектов рекомендуется использовать модульную структуру:
src/
├── app/
│ ├── store/
│ │ ├── index.ts
│ │ ├── app.state.ts
│ │ └── app.reducer.ts
│ ├── todos/
│ │ ├── store/
│ │ │ ├── todo.actions.ts
│ │ │ ├── todo.reducer.ts
│ │ │ ├── todo.effects.ts
│ │ │ └── todo.selectors.ts
│ │ ├── components/
│ │ └── services/
│ └── users/
│ └── store/
└── ...
Entity State Adapter
NgRx предоставляет EntityAdapter для эффективного управления коллекциями сущностей:
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
export interface TodoState extends EntityState<Todo> {
selectedTodoId: number | null;
}
export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();
export const initialState: TodoState = adapter.getInitialState({
selectedTodoId: null
});
export const todoReducer = createReducer(
initialState,
on(addTodo, (state, { todo }) => adapter.addOne(todo, state)),
on(removeTodo, (state, { id }) => adapter.removeOne(id, state))
);
Router Store
NgRx Router Store синхронизирует состояние маршрутизации с хранилищем:
import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store';
@NgModule({
imports: [
StoreModule.forRoot({ router: routerReducer }),
StoreRouterConnectingModule.forRoot()
]
})
export class AppModule { }
Meta-reducers
Meta-reducers позволяют перехватывать или изменять действия до того, как они достигнут reducer:
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
return function(state, action) {
console.log('state', state);
console.log('action', action);
return reducer(state, action);
};
}
@NgModule({
imports: [
StoreModule.forRoot(reducers, { metaReducers: [debug] })
]
})
export class AppModule {}
Инструменты разработчика NgRx
NgRx предоставляет мощные инструменты разработчика:
- Time-travel debugging
- Просмотр состояния и действий
- Логирование действий
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production
})
]
})
export class AppModule { }
16. Заключение
Сравнение Redux, Vuex и NgRx
- Redux: Универсальное решение, часто используемое с React. Предоставляет гибкость, но требует больше шаблонного кода.
- Vuex: Тесно интегрирован с Vue.js. Прост в использовании и имеет меньше шаблонного кода.
- NgRx: Оптимизирован для Angular. Использует RxJS и TypeScript для создания типобезопасных и реактивных приложений.
Выбор подходящего решения для вашего проекта
- Для React-проектов Redux остается популярным выбором.
- Vue.js разработчики найдут Vuex наиболее интуитивным и легким в освоении.
- Angular-разработчики получат максимальную выгоду от использования NgRx, особенно в больших проектах.
Будущее управления состоянием в веб-разработке
Управление состоянием продолжает эволюционировать:
- Серверный рендеринг и управление состоянием на стороне сервера становятся все более популярными.
- Возникают новые паттерны, такие как атомарное состояние (Recoil) и модель подписки (MobX).
- Нативное управление состоянием в фреймворках (например, Signals в Angular) может изменить ландшафт.
Независимо от выбранного решения, понимание принципов управления состоянием критически важно для создания масштабируемых и поддерживаемых веб-приложений.




