Управление состоянием приложения: Redux, Vuex, NgRx | Полное руководство

Управление состоянием приложения
Изучите современные подходы к управлению состоянием в веб-приложениях. Подробный обзор Redux, Vuex и NgRx: концепции, архитектура, практическое применение. Оптимизируйте свои проекты с помощью эффективного стейт-менеджмента

Часть 1: Введение и основы управления состоянием приложения

В мире современной веб-разработки управление состоянием приложения стало ключевым фактором, определяющим успех и эффективность проектов. Независимо от того, работаете ли вы с React, Vue.js или Angular, понимание концепций и инструментов управления состоянием критически важно для создания масштабируемых и поддерживаемых приложений.

1. Введение

Определение состояния приложения

Состояние приложения — это совокупность всех данных, которые могут изменяться с течением времени и влияют на поведение и отображение пользовательского интерфейса. Это может включать в себя данные, полученные с сервера, пользовательский ввод, настройки интерфейса и многое другое.

Важность управления состоянием в современных веб-приложениях

По мере роста сложности веб-приложений, управление их состоянием становится все более сложной задачей. Эффективное управление состоянием позволяет:

  • Улучшить производительность приложения
  • Упростить отладку и тестирование
  • Обеспечить предсказуемое поведение интерфейса
  • Облегчить масштабирование и поддержку кода

Краткий обзор Redux, Vuex и NgRx

В этой статье мы рассмотрим три популярных решения для управления состоянием:

  1. Redux — библиотека для JavaScript-приложений, часто используемая с React.
  2. Vuex — паттерн и библиотека управления состоянием для Vue.js.
  3. NgRx — фреймворк управления состоянием для Angular, вдохновленный Redux.

Каждое из этих решений предлагает свой подход к организации и управлению данными в приложении, но все они основаны на схожих принципах.

2. Основы управления состоянием

Что такое состояние приложения?

Представьте себе современное веб-приложение как сложный механизм, где каждая шестеренка — это отдельный компонент или модуль. Состояние приложения — это масло, которое позволяет всем этим шестеренкам работать слаженно и эффективно.

Состояние может включать:

  • Данные пользователя
  • Настройки приложения
  • Результаты API-запросов
  • Текущий вид интерфейса
  • Промежуточные данные вычислений

Проблемы, возникающие при управлении состоянием

С ростом приложения управление его состоянием становится все более сложным. Вот некоторые распространенные проблемы:

  1. Сложность отслеживания изменений: Когда данные могут изменяться в разных частях приложения, становится трудно понять, где и почему произошло изменение.
  2. Несогласованность данных: Различные компоненты могут иметь разные версии одних и тех же данных, что приводит к ошибкам и неожиданному поведению.
  3. Сложность отладки: Без четкой структуры управления состоянием отладка становится настоящим кошмаром.
  4. Проблемы с производительностью: Неэффективное управление состоянием может привести к ненужным обновлениям компонентов и снижению производительности.

Паттерн Flux и его влияние на современные решения

Паттерн Flux, представленный Facebook в 2014 году, стал революционным подходом к управлению состоянием. Его ключевая идея — однонаправленный поток данных, который значительно упрощает понимание того, как данные изменяются в приложении.

Основные принципы Flux:

  • Единое хранилище данных
  • Данные могут изменяться только через явные действия (actions)
  • Изменения происходят через чистые функции (reducers)

Эти принципы легли в основу Redux, а затем повлияли на разработку Vuex и NgRx, адаптировав идеи Flux для конкретных фреймворков.

3. Ключевые концепции управления состоянием

Однонаправленный поток данных

Однонаправленный поток данных — это фундаментальный принцип современного управления состоянием. Он означает, что данные в приложении движутся только в одном направлении, что делает поток информации более предсказуемым и легким для понимания.

Схема работы:

  1. Действие инициируется (например, пользователь кликает кнопку)
  2. Действие обрабатывается и изменяет состояние
  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 базируется на трех ключевых принципах:

  1. Единственный источник правды
  2. Состояние только для чтения
  3. Изменения с помощью чистых функций

Основные принципы Redux

  1. Единственный источник правды: Все состояние приложения хранится в одном централизованном store. Это упрощает отладку и позволяет легко сериализовать состояние для сохранения или передачи.
  2. Состояние только для чтения: Единственный способ изменить состояние — это отправить action. Это гарантирует, что ни представления, ни сетевые колбэки никогда не изменят состояние напрямую.
  3. Изменения с помощью чистых функций: Для определения того, как действия трансформируют состояние, используются чистые функции, называемые 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, имеет свои уникальные особенности:

  1. Интеграция с Vue: Vuex тесно интегрирован с Vue.js, что обеспечивает более гладкий опыт разработки.
  2. Реактивность: Vuex использует реактивную систему Vue для автоматического обновления компонентов при изменении состояния.
  3. Модульность: Vuex поддерживает модульную архитектуру, что упрощает организацию кода в крупных приложениях.
  4. Простота использования: Vuex имеет более простой API и меньше шаблонного кода по сравнению с Redux.

Основные концепции Vuex

Vuex работает вокруг нескольких ключевых концепций:

  1. State: Единое состояние, являющееся «единственным источником истины».
  2. Getters: Вычисляемые свойства на основе состояния.
  3. Mutations: Синхронные функции для изменения состояния.
  4. Actions: Асинхронные операции, которые могут вызывать мутации.
  5. 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 и обладает рядом особенностей:

  1. Основан на RxJS: NgRx широко использует реактивное программирование с помощью RxJS.
  2. Строгая типизация: Благодаря TypeScript, NgRx обеспечивает строгую типизацию на всех уровнях.
  3. Инструменты разработки: NgRx предоставляет мощные инструменты для отладки и разработки.
  4. Модульность: NgRx поддерживает модульную архитектуру для больших приложений.

Основные концепции NgRx

NgRx работает вокруг следующих ключевых концепций:

  1. Store: Единое иммутабельное хранилище состояния приложения.
  2. Actions: Объекты, описывающие изменения в приложении.
  3. Reducers: Чистые функции, определяющие, как состояние изменяется в ответ на действия.
  4. Selectors: Функции для извлечения данных из состояния.
  5. 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) может изменить ландшафт.

Независимо от выбранного решения, понимание принципов управления состоянием критически важно для создания масштабируемых и поддерживаемых веб-приложений.

Понравилась запись? Оцените!
Оценок: 0 Среднее: 0
Telegram
WhatsApp
VK
Facebook
Email

Рекомендуем