React course
  • Компоненты и коллекции
  • TypeScript
  • Стилизация
  • События и состояния
  • Формы
  • Жизненный цикл
  • Функциональные vs классовые компоненты
  • Основы Redux
  • Redux Toolkit
  • Асинхронный Redux
  • Селекторы
  • React Router
  • Code splitting
  • Паттерны и контекст
  • Анимация
Powered by GitBook
On this page
  • 1. Введение
  • 2. Основные принципы Redux
  • 2.1. Поток данных
  • 3. Store
  • 3.1. Функция createStore
  • 4. Actions
  • 5. Reducers
  • 6. React и Redux
  • 6.1. Provider
  • 6.2. connect()
  • 7. Redux DevTools
  • 8. Организация хранилища
  • 8.1. Дополнительные материалы
  • 9. Композиция редюсеров
  • 10. Дополнительные материалы

Was this helpful?

Основы Redux

1. Введение

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

За всем этим необходимо следить и обрабатывать, и это не просто. В один момент можно просто перестать улавливать связь между изменениями, так как контроль над тем, когда, почему и как изменилось состояние потерян из-за сложности самого состояния.

Идеальный вариант, это когда интерфейс вообще не знает о бизнес-логике. В этом нам помогают библиотеки управления состоянием. Redux и Mobx самые популярные, а для бекендов основаных на GraphQL есть Apollo.

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

  • Предсказуемость результата - существует всегда один источник правды, хранилище (store), хранящее в себе состояние приложения и методы для работы с ним.

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

  • Инструменты разработчика - все происходящее можно отслеживать в режиме реального времени.

  • Простота тестирования - первое правило написания тестируемого кода - писать небольшие функции, которые выполняют только одну вещь и независимы. Redux - это в основном функции: маленькие, чистые и изолированные.

2.1. Поток данных

Без использования библиотеки управления состоянием процесс обновления данных выглядит следующим образом: компонент инициализирует изменение состояния вызвав метод полученный пропом, после чего измененное состояние пробрасывается пропами вниз по дереву.

Поток данных в Redux всегда однонаправленный, и очень простой. За данные отвечает хранилище, компоненты только вызывают методы обновления данных и потом получают обновленные данные.

  • Пользователь, работая с интерфейсом, инициализирует отправку действий (actions)

  • Хранилище (store) вызывает все объявленные редьюсеры (reducers), передавая им текущее состояние (state) и действие (action)

  • Хранилище (store) сохраняет обновленное дерево состояния (state) возвращенное из редьюсеров (reducers)

  • При обновлении состояния (state) вызываются все подписчики для обновления интерфейса

3. Store

Хранилище (store) - js-объект, который содержит состояние приложения и методы доступа к нему, отправки действий и регистрации слушателей.

  • Хранит состояние (state) приложения как один объект

  • Позволяет получить доступ к состоянию через метод getState()

  • Напрямую состояние доступно только для чтения

  • Единственный способ изменить состояние - отправить действие (action), объект, описывающий, что произошло

  • Для отправки дейсвтий есть метод dispatch(action)

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

  • Регистрирация слушателей делается методом subscribe(listener)

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

const state = {
  notes: [],
  filter: '',
  session: {
    user: {
      name: null,
      avatar: null,
    },
    token: null,
  },
};

3.1. Функция createStore

Для того чтобы создать хранилище, используется функция createStore, которая принимает набор параметров и возвращает созданное хранилище.

createStore(reducer, [preloadedState], [enhancer])
  • reducer - функция, которая возвращает следующее дерево состояния, учитывая текущее дерево состояния и действие для обработки.

  • preloadedState - начальное состояние, к примеру сериализаванное состояние последнего пользовательского сеанса. Это должен быть объект той же формы, что и, как минимум, часть состояния.

  • enhancer - расширяет возможности хранилища при помощи прослоек (middleware).

import { createStore } from 'redux';

// Используем редюсер-болванку
const reducer = (state = {}, action) => state;

const store = createStore(reducer);

4. Actions

Действия (actions) - это объекты, которые помогают доставить данные из компонентов в хранилище.

  • Хранят минимально необходимый набор информации.

  • Должны иметь свойство type, которое указывает тип выполняемого действия.

  • Помимо поля type, структура действия может быть произвольной.

const action = {
  type: 'ADD_NOTE',
  payload: {
    text: 'Redux is awesome!',
  },
};

Действия создаются функциями (action creators), которые могут быть асинхронными и иметь побочные эффекты. В базовом варианте они просто возвращают объект-дейтсвие.

const addNote = text => ({
  type: 'ADD_NOTE',
  payload: {
    id: Date.now(),
    text,
  },
});

const deleteNote = id => ({
  type: 'DELETE_NOTE',
  payload: {
    id,
  },
});

5. Reducers

Редюсеры (reducers) - это чистые функции, которые принимают предыдущее состояние приложения и действие, а затем возвращают новое следующее состояние.

Они определяют, как изменяется состояние приложения в ответ на действия, отправленные в хранилище. Помните, что действия описывают только то, что произошло, а не как изменяется состояние приложения.

(previousState, action) => newState

Вещи, которые нельзя делать внутри редюсера:

  • Мутировать аргументы

  • Выполнять побочные эффекты, такие как API-запросы и т. п.

  • Вызывать нечистые функции, например Date.now()

Как выполнять побочные эффекты мы рассмотрим далее, пока что просто помните - редюсер должен быть чистым. Получая те же аргументы, он должен вычислить следующее состояние и вернуть его. Без сюрпризов. Никаких побочных эффектов. Никаких мутаций. Просто рассчет.

Вот редьюсер, который принимает текущее состояние и действие как аргументы, а затем возвращает следующее состояние.

const initialState = [];

function notesReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_NOTE':
      return [...state, action.payload];

    case 'DELETE_NOTE':
      return state.filter(note => note.id !== action.payload.id);

    default:
      return state;
  }
}

Обратите внимание:

  • Мы создаем копию state, а не мутируем его.

  • Мы возвращаем предыдущее состояние по умолчанию. Важно вернуть предыдущее состояние для любого неизвестного действия.

6. React и Redux

Для того чтобы использовать React и Redux вместе, необходимо добавить пакет react-redux. Это набор компонентов связывающих React-компоненты и Redux-хранилище через контекст Context.

npm install react-redux

6.1. Provider

Компонент <Provider>, оборачивает все дерево компонентов приложения и, используя контекст, предоставляет store и его методы.

import { createStore } from 'redux';
import { Provider } from 'react-redux';

// Болванка под reducer
const reducer = (state = {}, action) => state;

const store = createStore(reducer);

<Provider store={store}>
  <App />
</Provider>;

6.2. connect()

Если какой-либо компонент хочет получить доступ к store, он должен быть обернут в функцию connect(), которая свяжет компонент и store. Предоставляет доступ к state и disaptch().

connect это HOC, он не изменяет переданный ему компонент, а возвращает новый компонент связанный с хранилищем.

connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(Component)

mapStateToProps(state, [ownProps]) - функция соединяющая часть состояния с пропами компонента. Таким образом, связанный компонент будет иметь доступ к необходимой ему части состояния.

  • Получает state как параметр и позволяет выбрать из всего state только необходимые компоненту слайсы (части).

  • Возвращает объект, свойства которого дополнят props компонента.

  • Вызывается каждый раз когда хранилище обновляется.

  • Если нет необходимости подписываться на обновления, передаем null.

  • Если функция объявлена ​​как принимающая два параметра, первым будет передана ссылка на state, а вторым ссылка на пропы самого компонента.

const mapStateToProps = (state, props) => ({
  notes: state.notes,
  currentFilter: state.filter,
});

mapDispatchToProps(dispatch, [ownProps])- функция соединяющая отправку действий с пропами компонента. Таким образом, связанный компонент сможет отправлять действия посредством вызова методов указанных в возвращаемом объекте.

  • Получает ссылку на метод dispatch как параметр и позволяет объявить методы для отправки действий.

  • Возвращает объект, свойства которого дополнят props компонента.

  • Если функция объявлена ​​как принимающая два параметра, первым будет передана ссылка на dispatch, а вторым ссылка на пропы самого компонента.

const addNote = text => ({
  type: 'ADD_NOTE',
  payload: { text },
});

const mapDispatchToProps = dispatch => ({
  addNote: text => dispatch(addNote(text)),
});

Если аргументы действия совпадают с параметрами объявляемого метода, можно вместо функции передать объект. В таком случае connect пройдет по ключам объекта и обернет их в dispatch.

const addNote = text => ({
  type: 'ADD_NOTE',
  payload: { text },
});

const mapDispatchToProps = { addNote };
Copy

7. Redux DevTools

Чтобы упростить работу с Redux существуют Redux DevTools. Они помогают отслеживать изменение состояния с течением времени, наблюдать за отправкой дейтсвий и т. п. Естеcтвенно, это предназначено только для разработки.

8. Организация хранилища

Необходимо хранить все состояние приложения в Redux? Можно ли использовать setState()? Как разработчик, ваша задача - определить, из какого набора данных состоит приложение, и где их лучше хранить.

Некоторые общие правила для определения того, какие данные должны быть помещены в Redux. Если ответ да, то есть смысл использовать хранилище, а не локальное состояние.

  • Необходимы ли эти данные другим частям приложения?

  • Необходимо ли на основе этих данных создавать дополнительные производные?

  • Используются ли одни и те же данные для управления несколькими компонентами?

  • Есть ли необходимость в кешировании?

8.1. Дополнительные материалы

9. Композиция редюсеров

Еще одна полезная функция Redux - возможность делать композицию редьюсеров, то есть совмещать много в один. Это позволяет удобно поддерживать гораздо более сложные состояния в больших приложениях. Для этого используется функция combineReducers().

import { combineReducers } from 'redux';
import notesReducer from './notes';
import filterReducer from './filter';

const rootReducer = combineReducers({
  notes: notesReducer,
  filter: filterReducer,
});

export default rootReducer;

10. Дополнительные материалы

PreviousФункциональные vs классовые компонентыNextRedux Toolkit

Last updated 3 years ago

Was this helpful?

Why Redux need reducers to be "pure functions"
Документация react-redux
Документация Provider
Документация connect
Документация redux-devtools-extension
Where to Hold React Component Data: state, store, static, and this
The 5 Types Of React Application State
Документация Redux
Документация Redux на русском
Three Rules For Structuring (Redux) Applications
A cartoon intro to Redux
Getting Started with Redux by Dan Abramov
Building React Applications with Idiomatic Redux by Dan Abramov
Redux: шаг за шагом
Using Redux-Actions — Why and How?
React-Redux workflow - graphical cheat sheet