Redux Основы

Что такое Redux? 🧐

Redux - это библиотека для управления и обновления состояния приложения с использованием событий, называемых «actions». Он служит централизованным хранилищем состояния, которое необходимо использовать во всем приложении, с правилами, гарантирующими, что состояние может обновляться только предсказуемым образом.

Почему я должен использовать Redux? 🤔

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

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

Когда мне использовать Redux? 🤔🤯

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

Redux более полезен, когда:

  • У вас есть большое количество состояний приложения, которые необходимы во многих местах приложения.
  • Состояние приложения часто обновляется с течением времени
  • Логика обновления этого состояния может быть сложной.
  • Приложение имеет кодовую базу среднего или большого размера, и над ним могут работать многие люди.
note

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

Термины и концепции Redux ✍🏻

State Management

Начнем с небольшого счетчика React. Он отслеживает число в состоянии компонента и увеличивает число при нажатии кнопки: c

Value: 0

Это автономное приложение, состоящее из следующих частей:

  • Состояние (state), источник истины, который движет нашим приложением;
  • Представление (view), декларативное описание пользовательского интерфейса на основе текущего состояния
  • Действия (actions), события, которые происходят в приложении на основе ввода данных пользователем, и запускают обновления в состоянии

Это небольшой пример «одностороннего потока данных» (one-way data flow):

  • State описывает состояние приложения в определенный момент времени.
  • UI отображается на основе этого состояния
  • Когда что-то происходит (например, пользователь нажимает кнопку), состояние обновляется в зависимости от того, что произошло.
  • UI перерисовывается на основе нового состояния
redux data flow

Однако простота может нарушиться, когда у нас есть несколько компонентов, которые должны совместно использовать одно и то же состояние, особенно если эти компоненты расположены в разных частях приложения. Иногда это можно решить, «подняв состояние» до родительских компонентов, но это не всегда помогает.

Один из способов решить эту проблему - извлечь общее состояние из компонентов и поместить его в централизованное место за пределами дерева компонентов. Благодаря этому наше дерево компонентов становится большим «представлением», и любой компонент может получить доступ к состоянию или запускать действия, независимо от того, где они находятся в дереве!

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

Основная идея Redux

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

Неизменность (Immutability)

«Mutable» означает «изменчивый». Если что-то «immutable», это никогда не может быть изменено.

caution

Все объекты и массивы JavaScript по умолчанию изменяемы.

Если я создаю объект, я могу изменить содержимое его полей. Если я создам массив, я также могу изменить его содержимое:

const obj = { a: 1, b: 2 };
// still the same object outside, but the contents have changed
obj.b = 3;
const arr = ["a", "b"];
// In the same way, we can change the contents of this array
arr.push("c");
arr[1] = "d";

Это называется изменением объекта или массива. Это тот же объект или ссылка на массив в памяти, но теперь содержимое внутри объекта изменилось.

Для неизменного обновления значений ваш код должен делать копии существующих объектов / массивов, а затем изменять копии.

Мы можем сделать это вручную, используя операторы распространения массива / объекта в JavaScript, а также методы массива, которые возвращают новые копии массива вместо изменения исходного массива:

const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3,
},
b: 2,
};
const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42,
},
};
const arr = ["a", "b"];
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat("c");
// or, we can make a copy of the original array:
const arr3 = arr.slice();
// and mutate the copy:
arr3.push("c");

Redux ожидает, что все обновления состояния будут содержать новый обьект.

Терминология ✍🏻

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

Actions

Action - это простой объект JavaScript, имеющий поле типа. Вы можете думать о действии как о событии, которое описывает что-то, что произошло в приложении.

Поле типа должно быть строкой, которая дает этому действию описательное имя, например «todos / todoAdded». Обычно мы пишем эту строку типа как «domain / eventName», где первая часть - это функция или категория, к которой принадлежит это действие, а вторая часть - это конкретное событие, которое произошло.

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

Типичный объект действия может выглядеть так:

const addTodoAction = {
type: "todos/todoAdded",
payload: "Buy milk",
};

Action Creators

Создатель действия - это функция, которая создает и возвращает объект действия. Обычно мы используем их, поэтому нам не нужно каждый раз писать объект действия вручную:

const addTodo = (text) => {
return {
type: "todos/todoAdded",
payload: text,
};
};

Reducers

Reducer - это функция, которая получает текущее состояние и объект action, решает, как обновить состояние при необходимости, и возвращает новое состояние: (state, action) => newState.

tip

Функция Reducer получило свое название, потому что похожа на функцию обратного вызова, которую вы передаете методу Array.reduce().

Reducers всегда должны следовать определенным правилам:

  • Они должны рассчитывать только новое значение состояния на основе аргументов состояния и действия.
  • Им не разрешено изменять существующее состояние. Вместо этого они должны делать неизменяемые обновления, копируя существующее состояние и внося изменения в скопированные значения.
  • Они не должны выполнять асинхронную логику, вычислять случайные значения или вызывать другие «побочные эффекты».

Логика внутри функции reducer обычно следует той же серии шагов:

  • Проверить, обрабатывает ли reducer это действие.
    • Если да, сделать копию состояния, обновить копию новыми значениями и вернуть ее.
  • В противном случае вернуть существующее состояние без изменений

Вот небольшой пример функции reducer, показывающий шаги, которым должен следовать каждый reducer:

const initialState = { value: 0 };
function counterReducer(state = initialState, action) {
// Проверить, обрабатывает ли `reducer` это действие.
if (action.type === "counter/increment") {
// Если да, сделать копию состояния
return {
...state,
// обновить копию новыми значениями и вернуть ее.
value: state.value + 1,
};
}
//В противном случае вернуть существующее состояние без изменений
return state;
}

Reducers могут использовать любую внутреннюю логику, чтобы решить, каким должно быть новое состояние: if / else, switch, циклы и так далее.

Store

Текущее состояние приложения Redux находится в объекте, называемом store.

Хранилище создается путем передачи функциии reducer и имеет метод getState, который возвращает текущее значение состояния:

import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({ reducer: counterReducer });
console.log(store.getState());
// {value: 0}

Dispatch

В Redux есть метод, называемый Dispatch. Единственный способ обновить состояние - вызвать store.dispatch() и передать action объект. Store запустит свою функцию reducer и сохранит внутри новое значение состояния, и мы можем вызвать getState() для получения обновленного значения:

store.dispatch({ type: "counter/increment" });
console.log(store.getState());
// {value: 1}
tip

Вы можете думать о функции dispatch как о «запуске события» в приложении. Что-то случилось, и мы хотим, чтобы об этом узнал store.

Reducers действуют как прослушиватели событий, и, когда они слышат интересующее их действие, они обновляют состояние в ответ.

Обычно мы вызываем action creators, чтобы они отправили нужный action:

const increment = () => {
return {
type: "counter/increment",
};
};
store.dispatch(increment());
console.log(store.getState());
// {value: 2}

Selectors

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

const selectCounterValue = (state) => state.value;
const currentValue = selectCounterValue(store.getState());
console.log(currentValue);
// 2

Односторонний поток данных (One Way Data Flow) 🧐

Есть несколько шаблонов программирования для структурирования вашего реагирующего приложения при управлении состоянием. Односторонний поток данных ← - сокращение, и однонаправленный путь ← родитель к потомку.

Односторонний поток данных - Parent Child
В приложении React есть два разных шаблона потока данных: первый - родительский, а другой - Redux или некоторая библиотека управления этапами.

В React JS данные передаются в одном направлении, от родительского к дочернему. Это помогает компонентам быть простыми и предсказуемыми.

Поток данных приложения Redux 🧐

Ранее мы говорили об «одностороннем потоке данных», который описывает эту последовательность шагов для обновления приложения:

  • state описывает состояние приложения в определенный момент времени.
  • UI отображается на основе этого состояния
  • Когда что-то происходит (например, пользователь нажимает кнопку), состояние обновляется в зависимости от того, что произошло.
  • UI перерисовывается на основе нового состояния

В частности, для Redux мы можем разбить эти шаги более подробно:

  • Начальная настройка

    • Хранилище Redux создается с использованием функции rootReducer
    • Хранилище вызывает rootReducer один раз и сохраняет возвращаемое значение в качестве своего начального состояния.
    • Когда UI впервые отображается, компоненты UI получают доступ к текущему состоянию хранилища Redux и используют эти данные, чтобы решить, что отображать.
    • Они также подписываются на любые будущие обновления store, чтобы знать, изменилось ли состояние.
  • Обновления

    • Что-то происходит в приложении, например, пользователь нажимает кнопку
    • Код приложения отправляет actions в хранилище Redux, например отправку ({type: 'counter / increment'})
    • Store снова запускает функцию reducer с предыдущим состоянием и текущим action и сохраняет возвращаемое значение как новое состояние.
    • Store уведомляет все части пользовательского интерфейса, на которые подписан, о том, что store был обновлен.
    • Каждый компонент пользовательского интерфейса, которому нужны данные из хранилища, проверяет, изменились ли части состояния, которые им нужны.
    • Каждый компонент, который видит, что его данные изменились, принудительно выполняет повторный рендеринг с новыми данными, чтобы он мог обновить то, что показано на экране.
redux data flow

Практика 👩‍💻👨‍💻

Используя код выше, добавить следующие actions:

  • decrement - должен уменьшать значение счетчика
  • reset - должен сбрасывать значение счетчика в 0

Результат должен быть выведен в консоль.

Edit this page on GitHub