React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
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
Кстати, в пакете @reduxjs/toolkit есть функция nanoid для генерации уникальных идентификаторов.

import { nanoid } from "@reduxjs/toolkit";


#redux #документация
👍5
Redux Essentials 3. Страница статьи

Добавляем возможность просмотреть отдельную статью.

https://codesandbox.io/s/redux-essentials-single-post-page-react-junior-3dziwq?file=/src/App.js

Для этого понадобится новый роут вида /posts/123 и компонент SinglePostPage (лежит в папке features/posts).

Роут будет динамический (с параметром - /posts/:id), в компонент нужно получить сначала значение этого параметра, чтобы узнать, с какой статьей мы имеем дело (хук useParams из react-router). А затем - получить данные статьи из хранилища с помощью селектора (getPostData).

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

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

Наконец, добавляем ссылки на посты в компоненте PostsList.

#redux #управлениесостоянием #документация #примерыкода
👍3
Redux Essentials 4. Редактирование статьи

https://codesandbox.io/s/redux-essentials-edit-post-react-junior-hbfrh5?file=/src/App.js

Чтобы иметь возможность редактировать статью, нам понадобится:

- новый роут /editPost/:id
- компонент формы EditPostForm
- новый экшен postUpdated, чтобы внести изменения в данные

1) В компоненте получаем id поста из урла с помощью useParams.
2) Затем получаем данные поста с помощью селектора getPostData и подставляем их в форму.
3) Все состояние формы хранится локально.
4) При отправке формы диспатчим экшен с помощью креатора postUpdated, передавая ему всю необходимую информацию.
5) Переходим на роут поста, чтобы увидеть изменения (хук useNavigate).

Так как формы создания и редактирования очень похожи (фактически, это одна и та же форма), появилось большое искушение вынести ее в отдельный компонент - /components/PostForm.js. Он немного избыточен, включает в себя помимо формы еще и текст заголовка и общие стили контейнера, но почему бы и нет.

Что касается экшенов postAdded, postUpdated. Тут как будто напрашивается нормализованное состояние и createEntityAdapter. Посмотрим, что будет дальше.

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

Следующий шаг - добавляем в приложение пользователей, ведь статьи не сами собой пишутся.

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

https://codesandbox.io/s/redux-essentials-users-slice-react-junior-52ltvw?file=/src/App.js

Что делаем?

1. Добавляем новую фичу users и новый слайс usersSlice. Здесь хранится простой список пользователей, даже никаких экшенов нет (пока по крайней мере). Подключаем слайд к хранилищу в файле store.js. Сразу создаем два селектора - для получения всего списка и для получения отдельного пользователя по идентификатору.

2. В форму создания поста добавлям селект для выбора автора (это сделано для простоты и наглядности, в реальном приложении обычно есть механизмы для определения активного пользователя). Список пользователей получаем с помощью useSelector.

3. Немного изменяем экшен posts/postAdded. Теперь он получает три отдельных параметра вместо одного объекта, поэтому оформляем его в виде массива с полями reducer и prepare.

4. Создаем отдельный компонент /posts/PostAuthor.js, который будет получать id пользователя и выводить его имя.

5. Наконец, подключаем этот компонент в каждый пост в ленте PostsList и в компонент SinglePostPage.

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

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

https://codesandbox.io/s/redux-essentials-dates-react-junior-y8f6x8?file=/src/App.js

1. Вносим изменения в слайс: добавляем постам новое поле date, устанавливаем текущую дату при создании поста. На прошлом шаге мы разделели редьюсер postAdded на reducer и prepare. Вот как раз в prepare и добавляется этот новый "подготовительный" функционал.

Важно: Так как в хранилище должны находиться только сериализуемые данные, вместо даты записываем туда строковый timestamp.

2. Создаем отдельный компонент TimeAgo для вывода даты (в формате 5 минут назад). Для форматирования используется библиотека date-fns.

3. Выводим дату в списке статей и на странице отдельного поста.

4. В списке дополнительно сортируем статьи по дате, выводя более свежие первыми. Теперь можно добавить новую статью в форме создания, и она окажется первой в списке.

#redux #управлениесостоянием #документация #примерыкода
👍1
Redux Essentials 7. Реакции

Добавляем реакции к статьям.

https://codesandbox.io/s/redux-essentials-reactions-react-junior-1p1k32?file=/src/App.js

1. У каждой статьи появляется поле reactions с набором доступных реакций и счетчиком для каждой реакции.
2. Добавляется новый экшен reactionAdded, который увеличивает счетчик выбранного смайлика.
3. Добавляется компонент ReactionButtons - набор кнопок с разными реакциями.

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

#redux #управлениесостоянием #документация #примерыкода
👍2
Forwarded from Cat in Web
Легковесные альтернативы Redux

Статья (англ.): https://blog.openreplay.com/lightweight-alternatives-to-redux

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

1. Хуки React

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

2. Zustand

Zustand использует те же flux-принципы, что и Redux. Мы создаем хранилище с помощью функции create и получаем из нее хук useStore, который можно использовать в компонентах.

3. Jotai

Разработан той же командой, что и Zustand, но использует другой подход - атомарный (atomic). Каждая часть данных оформляется в виде атома с помощью функции atom. Для работы с атомами есть хук useAtom, который работает так же, как useState.

4. Valtio

Подход Valtio базируется на использовании прокси. Мы передаем объект в функцию proxy и дальше можем его мутировать - прокси отслеживает все изменения.

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

#redux #react #statemanagement #library
👍2
Forwarded from Cat in Web
React World

Развлекалочка на React: https://sfatihk.github.io/react-world/
Скроль страницу и совершай небольшое путешествие с героями любимых фильмов.

А потом можно залезть в исходники и посмотреть, как все это работает: https://github.com/sfatihk/react-world

#react
👍2
Redux Essentials 8. Запрос данных

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

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

В руководстве используется специальный фейковый REST API с сервером и клиентом, но я не буду так заморачиваться, сделаю простую имитацию с setTimeout.

Thunks

Хранилище работает синхронно, в редьюсерах не должно быть асинхронной логики. Поэтому мы выносим ее в thunks (санки).

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

Мы работаем с санками как с экшенами, передаем их в метод dispatch, а чтобы все правильно обрабатывалось, нужно использовать миддлвар redux-thunk.

createAsyncThunk

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

Итак, thunk creator у нас есть и экшены, связанные с ним, тоже. Но хранилище пока ничего не знает об этих экшенах и не умеет их обрабатывать. Добавляем редьюсеры для них с помощью метода extraReducers.

extraReducers это функция, которая в качестве аргумента получает builder. Вот в этот билдер и нужно добавить обработчики для новых экшенов (метод builder.addCase).

Состояние и отображение

Немного меняем состояние, теперь в нем есть поле items для хранения списка постов, а также поля status и error для хранения состояния запроса. Вносим изменения в селекторы, так как путь к данным изменился. Добавляем также новый селектор getStatus.

Список items изначально пустой, чтобы его заполнить, нужно запросить данные "с сервера". Делаем это в компоненте PostsList - внутри хука useEffect.

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

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

На базе предыдущего шага:

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

2. Состояние запроса хранится в виде строки из перечисления (enum).

#redux #управлениесостоянием #паттерны
👍1
Еще раз про createAsyncThunk

Это функция для создания thunk creators. Используем, если нужны экшены начала и конца запроса.

Без createAsyncThunk:

// action creators
const getFetchPostsPending = function() {
type: 'posts/fetchPosts/pending'
};
const getFetchPostsFulfilled = function(posts) {
type: 'posts/fetchPosts/fulfilled',
payload: posts
};
const getFetchPostsRejected = function(error) {
type: 'posts/fetchPosts/rejected',
error
};

// thunk creator
const fetchPosts = function(params) {
return async function (dispatch) {
dispatch(getFetchPostsPending());
try {
const posts = await fakeApi.getPosts(params);
dispatch(getFetchPostsFulfilled(posts))
} catch (err) {
dispatch(getFetchPostsRejected(err.toString()))
}
}
}

// обработка
const postsReducer = function(state, action) {
switch(action.type) {
case 'posts/fetchPosts/pending':
// ...
case 'posts/fetchPosts/fulfilled':
// ...
case 'posts/fetchPosts/rejected':
// ...
}
}

С createAsyncThunk:

// thunk creator
export const fetchPosts = createAsyncThunk("posts/fetchPosts", async function() {
const response = await fakeApi.getPosts();
return response.data;
});

// обработка
const postsSlice = createSlice({
// ...
extraReducers(builder) {
builder
.addCase(fetchPosts.pending, function(state, action) { })
.addCase(fetchPosts.fulfilled, function(state, action) { })
.addCase(fetchPosts.rejected, function(state, action) { });
}
})

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

builder позволяет добавлять в хранилище дополнительные редьюсеры - для дополнительных экшенов, которые генерируются где-то вне createSlice.

У него есть три метода:

- builder.addCase - для конкретного типа экшена. Первым аргументом нужно передать либо action creator, либо строку с типом экшена.
- bulder.addMatcher - первым аргументом принимает функцию-матчер. В эту функцию передаются все экшены, редьюсер срабатывает, если матчер вернет true.
- builder.addDefaultCase - принимает только редьюсер, который срабатывает, если не сработал ни один другой редьюсер.

#redux #управлениесостоянием #документация
👍2
Redux Essentials 9. Запрос пользователей

По аналогии с запросом статей делаем запрос пользователей.

https://codesandbox.io/s/redux-essentials-fetch-users-react-junior-ipw0r8?file=/src/App.js

1. Пишем thunk creator с помощью createAsyncThunk.
2. Добавляем редьюсер для экшена fetchUsers.fulfilled.
3. Диспатчим новый санк в файле index.js, там где импортируем объект хранилища.

#redux #управлениесостоянием #документация #примерыкода
👍1
Redux Essentials 10. Сохранение постов

Добавляем еще один thunk creator addNewPost (с помощью createAsyncThunk), который будет выполнять запрос к фейковому серверу, отправлять данные из формы и получать обратно объект нового поста (с уникальным id). Создание id происходит на сервере, как это обычно и бывает в реальных приложениях.

https://codesandbox.io/s/redux-essentials-create-post-react-junior-cbfh4u?file=/src/App.js

Убираем редьюсер postAdded и вместо него добавляем новый кейс в extraReducers - для экшена addNewPost.fulfilled.

Для загрузки постов мы добавили в слайс поле status и отслеживали статус запроса, чтобы отображать лоадер. Здесь пойдем другим путем - будем отслеживать статус прямо в компоненте (подробнее - возвращение промиса из thunk). Пока идет запрос, заблокируем отправку формы.

await dispatch(addNewPost({ title, content, user: userId })).unwrap()

Обратите внимание на метод unwrap() у результата выполнения функции dispatch. Его добавляет Redux Toolkit. Если промис завершился неудачно, этот метод выбросит ошибку, которую мы сможем поймать в try...catch

#redux #управлениесостоянием #документация #примерыкода
👍1
Redux Essentials 11. Список авторов и страница автора

На следующем этапе добавляем страницу со списком пользователей и страницу отдельного пользователя со списком статей.

https://codesandbox.io/s/redux-essentials-users-list-user-page-react-junior-xmtqds?file=/src/App.js

Ничего нового:

1) Новый селектор getUserPosts в postsSlice
2) Новый компонент UsersList (использует селектор getUsers)
3) Новый компонент UserPage (использует хук useParams и селектор getUserPosts)
4) Новые роуты /users и /users/:userId в App.js
5) Ссылка на страницу авторов в шапке сайта + ссылки на авторов в превью статей и на странице статьи

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