React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Redux Toolkit

Документация Redux очень настойчиво советует использовать в разработке Redux Toolkit, так что я более не могу его игнорировать. Последняя часть руководства Redux Fundamentals как раз посвящена этому инструменту.

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

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

По сути, Redux Toolkit - это набор утилит, которые позволяют сократить объем шаблонного кода:

Вот эти утилиты:

- configureStore
- createSlice
- createAsyncThunk
- createEntityAdapter

Будем разбираться с каждой отдельно и сравнивать новый и старый код.

#redux #управлениесостоянием #документация
👍6
Redux Toolkit. Создание хранилища

Первая функция из Redux Toolkit - configureStore - предназначена для создания хранилища.

Вспоминаем, как это происходит в обычном redux:

https://codesandbox.io/s/redux-toolkit-demo-react-junior-4ihz98?file=/src/store.js

1. Объединяем несколько отдельных редьюсеров (слайсов) в один общий rootReducer, который будет обрабатывать каждый экшен (с помощью функции combineReducers).
2. Подключаем thunk, applyMiddleware и composeWithDevTools, чтобы собрать полностью укомплектованное возможностями хранилище.
3. Совмещаем это все в функции createStore.

* Логика внутри слайсов, а также разделение кода по файлам сейчас не имеет значения. Разбираемся только в создании хранилища.

Как выглядит создание хранилища с configureStore:

https://codesandbox.io/s/redux-toolkit-configure-store-react-junior-iv0vpk?file=/src/store.js

1. Передаем функции отдельные редьюсеры - получаем готовое хранилище.

Под капотом происходит объединение редьюсеров-слайсов в один корневой редьюсер, подключение thunk и интеграция с redux-devtools-extension. Помимо этого, добавляются дополнительные миддлвары, которые позволяют отслеживать распространенные ошибки, например, мутации состояния.

Таким образом, мы заменяем три пакета (redux, redux-devtools-extension, redux-thunk) на один (@reduxjs/toolkit).

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
👍2🐳1
Redux Toolkit. Создание хранилища. Защита от мутаций

Функция configureStore делает много полезных вещей под капотом, например, подключает redux-thunk и redux-devtools-extensions. Кроме того, она добавляет миддлвар, который отслеживает мутацию стейта в редьюсере.

Если вместо копирования, мы попробуем мутировать текущее состояние, то будет ошибка

// Мутация состояния, Ошибка

state.active = !state.active;
return state;

// Копирование, нет ошибок

return {
...state,
active: !state.active
};


#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
Redux Toolkit. Слайсы и экшены

Переходим к отдельным редьюсерам, или слайсам (срезам). Будем рассматривать их вместе с экшенами, так как они тесно связаны, ведь редьюсеры обрабатывают экшены.

В обычном redux это довольно многословная часть кода: здесь и конструкция switch, и создатели экшенов.

https://codesandbox.io/s/redux-toolkit-demo-react-junior-4ihz98?file=/src/features/filters/slice.js

В демо-примере эта логика разделена на два файла для каждой фичи - actions.js с типами экшенов и функциями-создателями экшенов и slice.js с редьюсером и селекторами.

1. Определяем типы экшенов.
2. Пишем функции-создатели экшенов.
3. Для каждого типа экшена пишем отдельный кейс в редьюсере.
4. Не забываем передать в редьюсер исходное состояние для инициализации.

Redux Toolkit предлагает функцию createSlice, чтобы упростить эту задачу. Ей нужно передать строку-префикс для типов ("todos" из "todos/addTodo"), начальное состояние и функцию-обработчик для каждого кейса (экшена).

Вместо:

function (state = initialState, action) {
switch(action.type) {
case 'action1'

case 'action2'

default
}
}


Будет вот такой код:

createSlice({
name: 'todos',
initialState,
reducers: {
action1(state, action) {

},
action2(state, action) {

}
}
})


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

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

Функция createSlice возвращает объект с несколькими полями. Готовый редьюсер находится в поле reducer.

Экшены

Более того, функция createSlice самостоятельно создает action creators для всех указанных типов экшенов. Они находятся в поле actions. Этим функциям-создателям нужно передать то, что должно находиться у экшена в поле payload.

Селекторы

Redux Toolkit самостоятельно импортирует модуль reselect (как хорошую практику работы с redux), поэтому функцию createSelector можно брать прямо из @reduxjs/toolkit.

Обновленный код

https://codesandbox.io/s/redux-toolkit-create-slice-react-junior-v37gp0?file=/src/features/filters/slice.js

В обновленном варианте мы можем избавиться от файла actions.js, так как создатели экшенов генерируются автоматически.

Вот обновленный вариант для слайса todos: https://codesandbox.io/s/redux-toolkit-create-slice-react-junior-v37gp0?file=/src/features/todos/slice.js

Здесь кроме обычных экшенов у нас есть еще один thunk - функция addTodoThunk. Она пока перенесена в старом виде, скоро мы ее заменим.

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
👍1
Redux Toolkit. Создатели экшенов

Создатели экшенов автоматически генерируются для всех экшенов, переданных в функцию createSlice.

const slice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {},
todoToggled(state, action) {}
}
});

// создатели экшенов находятся здесь
slice.actions; // { todoAdded, todoToggled }

По умолчанию это самые простые функции. Они принимают один аргумент - payload и формируют объект экшена с нужным типом.

// создание экшена 
todoToggled(2); // { type: 'todos/todoToggled', payload: 2 }

Сложные создатели экшенов

Но что если нам нужна функция посложнее, которая может принимать несколько аргументов и собирать из них payload.

Например, для создания новой задачи в списке сейчас мы пишем: todoAdded(data), а хотели бы писать: todoAdded(id, text).

Это можно устроить, немного развернув синтаксис функции createSlice.

Вместо простой функции с аргументами state и action редьюсер можно оформить в виде объекта. В поле reducer передаем эту функцию без изменений, а в поле prepare передаем функцию для формирования тела экшена.

reducers: {
todoAdded: {
reducer(state, action) {},
prepare(id, text) {
return {
payload: {
id,
text
}
}
}
}
}

Кроме payload тут можно указать еще поля meta или error - в соответствии с соглашением Flux Standart Action.

todoAdded(2, 'todo text'); // { type: 'todos/todoAdded', payload: { id: 2, text: 'todo text' }}


#redux #паттерны #управлениесостоянием #документация #примерыкода
👍3👏1
👏1
Redux Toolkit. Создание thunks

Логика создания thunks в Redux Toolkit немного меняется.

Как было?

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

export function addTodoThunk(text) {
return (dispatch) => {
// обработка данных
return createTodo(text)
.then((data) => {
// обращение к хранилищу
dispatch(addTodo(data));
});
};
}


Как стало?

Redux Toolkit позволяет отделить логику обработки данных и логику обращения к хранилищу (ее она берет на себя).

Функция createAsyncThunk принимает два аргумента - имя, на основе которого будут формироваться типы экшенов, и собственно функцию для обработки данных. Эта функция должна возвращать промис, то есть внутри можно производить любые асинхронные действия. Можно использовать async-функции, которые всегда возвращают промис.

export const saveNewTodo = createAsyncThunk(
"todos/saveNewTodo",
async function (text) {
return await createTodo(text);
}
);


Результат вызова createAsyncThunk - это обычный thunk creator. Вызываем его и передаем то, что вернулось, в dispatch.

dispatch(saveNewTodo("SOME TEXT"))


При этом под капотом в хранилище будут диспатчиться дополнительные экшены с типами:

- todos/saveNewTodo/pending (перед вызовом обработчика)
- todos/saveNewTodo/fulfilled (если вызов завершится удачно)
- todos/saveNewTodo/rejected (если будет ошибка)

Для каждого из них автоматически создается action creator:

- saveNewTodo.pending
- saveNewTodo.fulfilled
- saveNewTodo.rejected

Теперь нужно каким-то образом добавить обработку этих новых экшенов в редьюсере - то есть в функцию createSlice.

Добавление асинхронных экшенов в редьюсер

Самая громоздкая и трудная для понимания часть всего процесса.

В функцию createSlice мы передаем обработчики для каждого типа экшена (в поле reducers). И для всех этих типов создаются action creators. Но тут у нас уже есть готовые action creators.

Для таких ситуаций есть еще поле extraReducers. Это функция-коллбэк, которая принимает в качестве аргумента некий builder. И вот в этот билдер с помощью метода addCase нужно добавлять новые экшены с их обработчиками. Первым параметром нужно передать создатель экшена (функцию), вторым - обработчик (тоже функцию).

createSlice({
// ...
extraReducers: function(builder) {
builder.addCase(
saveNewTodo.fulfilled,
function(state, action) {
// изменение state
};
}
})


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

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
Redux Toolkit. Создание thunks - 2

https://codesandbox.io/s/redux-toolkit-create-async-thunk-react-junior-i9qc3c?file=/src/features/todos/slice.js

1. Асинхронная логика

Итак, сначала пишем собственно асинхронную логику и передаем ее функции createAsyncThunk. Тут у нас запрос на сервер для сохранения новой задачи и получения ее идентификатора:

export const saveNewTodo = createAsyncThunk(
"todos/saveNewTodo",
async (text) => {
return await createTodo(text);
}
);

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

В saveNewTodo теперь лежит функция - thunk creator.

Также у нее есть свойства: saveNewTodo.pending, saveNewTodo.fulfilled и saveNewTodo.rejected. В каждом из них лежит простая функция action creator, которая создает экшены с соответствующим типом (todos/saveNewTodo/pending, todos/saveNewTodo/fulfilled).

Нам об этом думать не нужно, эти экшены будут создаваться и диспатчиться автоматически. В .../fulfilled экшен будет также передан результат работы нашей асинхронной функции.

2. Dispatch

Работаем с saveNewTodo как с обычным thunk creator.

dispatch(saveNewTodo(text))


3. Обработка в редьюсере

Чтобы обрабатывать новые экшены в редьюсере, добавляем в createSlice поле extraReducers:

const todosSlice = createSlice({
//...
extraReducers: function(builder) {
builder.addCase(
saveNewTodo.fulfilled,
function(state, action) {
state[action.payload.id] = {
...action.payload,
checked: false
}
}
)
}
})


Тут используем метод builder.addCase(actionCreator, reducer). Можно выстраивать вызовы addCase в цепочку, если требуется обработать несколько экшенов:

builder
.addCase(saveNewTodo.pending, function(state, action) {})
.addCase(saveNewTodo.fulfilled, function(state, action) {})


* В демо примере убран экшен todoAdded, так как вся логика добавления нового элемента в список находится теперь внутри extraReducers.

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
Было и стало.

Нельзя, конечно, сказать, что стало короче :) Но зато логика разделена более правильно (+ комментарии).

Обратите внимание, я тут не использую экшен '../pending', потому что состояние запроса отслеживается самим компонентом. Но его вполне можно вынести в общее хранилище и обрабатывать здесь.

#redux #управлениесостоянием #примерыкода #паттерны
👍2
Redux Toolkit. Операции с нормализованным состоянием

И наконец, последняя функция из Redux Toolkit, которую нужно разобрать в этом руководстве - createEntityAdapter. Она упрощает работу с нормализованным состоянием (когда мы храним не массив элементов, а объект, где ключами служат идентификаторы элементов).

Пользоваться очень просто: вызов функции возвращает объект (адаптер) с кучей полезных методов.

const todosAdapter = createEntityAdapter();
/*
{
getInitialState,
addOne,
addMany,
removeOne,
...
}
*/


Демо-проект: https://codesandbox.io/s/redux-toolkit-create-entity-adapter-react-junior-fipune?file=/src/features/todos/slice.js

Начальное состояние

Для получения начального состояния адаптера используем метод adapter.getInitialState. Он вернет объект с полями entities (объект-словарь) и ids (массив идентификаторов).
Чтобы добавить дополнительные поля, нужно просто передать их в функцию:

const initialState = todosAdapter.getInitialState({
status: 'idle'
})


Например, здесь я добавляю статус запроса к серверу. В итоге начальное состояние будет иметь три поля: entities, ids и status.

Манипуляции с данными

Адаптер предоставляет множество полезных методов для работы с коллекцией элементов:

- addOne / addMany
- upsertOne / upsertMany
- updateOne / updateMany
- removeOne / removeMany
- setAll

Их можно использовать в редьюсерах, например, для кейса todoRemoved.

Для наглядности в демо-пример добавлены еще два экшена: completedTodosRemoved и listCleared, которые используют методы адаптера.

Селекторы

Кроме того, адаптер предоставляет два готовых селектора: selectAll и selectById. Чтобы их получить, нужно вызвать метод adapter.getSelectors. Так как этот метод работает с целым состоянием, нужно передать ему функцию для выделения нужно части состояния (в нашем случае state.todos):

const { selectAll, selectById } = todosAdapter.getSelectors(
function(state) {
return state.todos;
}
)


#redux #управлениесостоянием #примерыкода #паттерны #документация
👍1🔥1
Фух, закончилось руководство Redux Fundamentals, разобрались в "низкоуровневых" концепциях redux. Думаю, теперь имеет смысл пройти Redux Essentials. Документация уверяет, что в этом руководстве больше внимания уделяется тому, как пишутся приложения "in real world".

Вероятно, какая-то информация там будет повторяться, так что, может, получится двигаться быстрее)

#redux #управлениесостоянием #документация
👏4👍1
Create React App + Redux

CRA предоставляет готовый официальный шаблон с уже подключенным Redux Toolkit. Для этого нужно использовать опцию --template:

npx create-react-app my-app --template redux

То есть ничего дополнительно подключать не нужно, эта функциональность уже есть в create-react-app из коробки.
Можно даже с typescript, если вы уже его используете (я пока нет):

npx create-react-app my-app --template redux-typescript

В созданном проекте уже есть папка features и даже готовый пример features/counter с примерами использования методов createSlice, createAsyncThunk и configureStore.

UPD: да, и конечно тут уже есть интеграция с Redux DevTools

#redux #управлениесостоянием #инструменты
👍6
После довольно долгого перерыва будет полезно быстро вспомнить, что мы уже знаем о Redux Toolkit.

Сделаем это на примере простого демо-проекта Счетчика из официальной документации Redux:

🗂 Разделение на фичи

Хорошая практика - делить общее глобальное хранилище на несколько частей, соответствующих логическому разделению функционала приложения. Например, в одном из предыдущих проектов (TODO-лист) мы разделили хранилище на две части: todos и filters. В первой были все операции, связанные со списком (CRUD), во втором - вся логика фильтрации. Они частично пересекаются (отфильтрованный список), но в целом это две разные функциональности.

А в Счетчике фича всего одна - собственно логика счетчика, это тоже нормально.

Документация рекомендует разносить такие фичи по отдельным папкам - в каждой собственный слайс (редьюсер + экшены) и свои компоненты.

🔪 Создание слайсов

Слайс создается с помощью функции createSlice из Redux Toolkit.

Пример для счетчика в файле counterSlice.js.

В Redux Toolkit мы уже не мыслим отдельными понятиями редьюсер и экшены, тут у нас все сразу вместе (и если подумать, это действительно удобнее). По сути мы перечисляем типы экшенов и для каждого указываем логику обработки - такой маленький редьюсер для одного экшена.

При этом под капотом генерируются action creators. Они доступны в поле counterSlice.actions.

Thunks

Для thunk creators вообще-то существует отдельная функция createAsyncThunk, но в этом вводном демо-проекте авторы используют привычный способ - просто создают функцию incrementAsync, которая возвращает thunk, без дополнительной интеграции в слайс.

Но при желании с помощью createAsyncThunk мы можем подключиться к событиям "начала" и "конца" выполнения асинхронной операции.

🗄 Создание хранилища

Хранилище создается в файле app/store.js.

Здесь импортируются все необходимые редьюсеры (тут только один) и объединяются с помощью функции configureStore. Под капотом тут подключаются разные полезные миддлвары, включая redux-thunk и redux-devtools.

🧲 Подключение хранилища к приложению

За "мост" между Redux и React отвечает пакет react-redux, у которого есть компонент Provider, который обеспечивает проброс хранилища до всех заинтересованных компонентов (используется контекст).

Доступ к хранилищу из компонентов

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

А в компонентах мы используем хук useSelector (из пакета react-redux), которому передаем нужный селектор.

Пример в компоненте Counter.

Redux Toolkit в целом много оптимизует под капотом, но селекторы можно оптимизировать дополнительно с помощью функции createSelector (в Счетчике она не используется).

🪃 Отправка экшенов и thunks

Для отправки экшенов используем хук useDispatch (из пакета react-redux). По сути это обращение к методу store.dispatch. В него передаем или объект экшена, или thunk.

#документация #управлениесостоянием #redux #примерыкода
👍2
Новый проект

Наконец-то начинаю работу над новым проектом в рамках руководства Redux Essentials, которое должно научить меня лучшим практикам Redux.

Готовый проект выглядит так: https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps

Тут три вкладки:
- Posts - лента постов + форма для создания новой записи. Отсюда можно перейти на страницу отдельного поста и отредактировать его.
- Users - список пользователей. У каждого пользователя есть своя страница со списком его записей.
- Notifications - лента уведомлений, которую можно обновлять.

В демо-версии кое-что не работает или работает через раз, но это не имеет значения, все равно будем все переписывать с нуля :)

Первый разбор

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

Все данные приходят с сервера, а изменения синхронизируются с ним, значит, придется использовать какой-то api, слать запросы и асинхронно их обрабатывать. Тут нам вероятно понадобятся thunks. Спойлер: для api используется, очевидно, RTK Query, вот и повод с ним познакомиться.

Посты

Есть форма для добавления поста, ее состояние будет храниться локально. При отправке будем отправлять thunk. Понадобится также отслеживать начало и конец выполнения, чтобы отображать состояние отправки.

Есть список постов, а у постов есть реакции, скорее всего, их следует хранить в данных самого поста, так как они несамостоятельны.

Можно просматривать каждый пост, значит, нужно получать его данные по какому-то идентификатору.

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

Пользователи

Есть список пользователей (имя и ссылка на отдельную страницу), а также список записей, созданных пользователем.

Уведомления

Опять же обычный список. Плюс есть кнопка Refresh Notifications, которая подгружает новую порцию элементов. Их нужно будет добавить в существующий массив.

Свежие уведомления выделяются фоном.

Начало работы

Итак, первый взгляд бросили. Теперь создаем новый проект и начинаем работать.

Для этого либо используем CRA с шаблоном redux:

npx create-react-app my-app --template redux

Либо подключаем по отдельности react-redux и @reduxjs/toolkit.

#redux #управлениесостоянием #документация
👍2🔥1
Redux Essentials 1. Лента статей

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

https://codesandbox.io/s/redux-essentials-posts-react-junior-6cyh18?file=/src/App.js

Как и предполагалось, создаем отдельную папку features/posts, а в ней файл postsSlice.js. Используем функцию createSlice - пока без всяких редьюсеров, только с начальным состоянием.

Сразу здесь же создаем селектор getPosts для получения списка статей.

В файле app/store.js создаем хранилище (функция createStore), подключаем созданный слайс.

В файле App.js подготавливаем структуру приложения. У нас будет много страниц, поэтому подключаем react-router-dom. Пока роут только один - индексный. Тут будет выводиться лента постов, а затем еще и форма создания поста.

Компонент ленты постов - features/posts/PostsList.js. Чтобы получить список, используем хук useSelector.

#redux #управлениесостоянием #документация #примерыкода
👍4
Redux Essentials 2. Добавление статей

Важно: Появились проблемы с обновленным React 18, поэтому пока переключаюсь на версию 17.0.2.

https://codesandbox.io/s/redux-essentials-add-post-react-junior-5wmcg3?file=/src/App.js

Здесь все тоже знакомо.

Для внесения изменений в состояние нам потребуется экшен - postAdded. Добавляем его обработчик в postsSlice, в секцию reducers. При возникновении экшена просто пушим его данные в state.

Важно: Внутри функции createSlice допустимо мутировать состояние, так как оно специально обрабатывается.

Redux Toolkit автоматически генерирует нам action creator с таким же названием postAdded, который можно забрать из postSlice.actions.

Добавляем форму создания статьи - компонент features/posts/AddPostForm.js. Выводим ее в компоненте App.

Все состояние формы хранится внутри компонента. При отправке вызываем метод dispatch, чтобы отправить экшен в хранилище (берем из хука useDispatch). Сам экшен генерируем с помощью креатора из предыдущего абзаца.

#redux #управлениесостоянием #документация #примерыкода
👍4