React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Использование useSelector

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

#управлениесостоянием #redux #началоработы #документация #важно
Глобальное и локальное состояние

Должно ли все состояние приложения быть глобальным и содержаться в хранилище? Конечно, нет.

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

- Знают ли другие части приложения об этих данных?
- Нужно ли вам на основе этих данных создавать какие-то другие производные данные (например, отфильтрованный список)?
- Используются ли эти данные в нескольких компонентах?
- Есть ли потребность в том, чтобы иметь возможность восстановить состояние этих данных для заданного момента времени (например, история действий пользователя)?
- Хотите ли вы кешировать эти данные (например, чтобы избежать нескольких запросов к серверу)?
- Хотите ли вы сохранить консистентность (согласованность) этих данных во время hot reloading (внутреннее состояние компонентов при этом может быть сброшено)?

Если на какие-то вопросы из списка вы ответили да, вероятно, вы имеете дело с глобальным состоянием.

#управлениесостоянием #redux #началоработы #документация
Почему редьюсер называется редьюсером

В JavaScript есть метод Array.reduce(), который дает возможность превратить входной массив в одно единственное значение согласно какой-то логике - буквально уменьшить (reduce) массив до значения.

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

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

Точно так же работает и редьюсер в Redux.

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

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

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

 
const actions = [
{ type: 'counter/incremented' },
{ type: 'counter/incremented' },
{ type: 'counter/incremented' }
]

const initialState = { value: 0 }

const finalResult = actions.reduce(counterReducer, initialState)

// {value: 3}


#управлениесостоянием #redux #документация
Redux DevTools

Redux DevTools - это расширение для браузера (доступно в Chrome и Firefox ). Оно дает возможность просматривать все изменения хранилища.

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

Подключаем пакет redux-devtools-extension и импортируем из него функцию composeWithDevTools. Это аналог функции compose, но тут автоматически еще добавляется интеграция с браузерным расширением.

import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(rootReducer, composeWithDevTools());

Посмотреть расширение в действии можно здесь: https://furrycat.ru/redux-test/

- Установите расширение
- Откройте панель разработчика - вкладка Redux
- Кликайте на кнопки

#управлениесостоянием #redux #инструменты
Проблемы селекторов

Селекторы - удобные штуки, которые инкапсулируют в себе все общение с хранилищем, однако, у них есть ряд недостатков.

‼️ Проблема #1. Вызов на каждый чих

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

https://codesandbox.io/s/selectors-problems-1-react-junior-5f3kf7?file=/src/store.js

В хранилище есть два поля: items и counter. Они никак не связаны друг с другом. Есть также два селектора getItems и getCounter.

При изменении счетчика (события INCREMENT, DECREMENT) поле items никак не меняется, даже ссылка на него остается прежней. Однако селектор getItems все равно запускается. Он выполняется (возможно с какими-то сложными вычислениями), но возвращает то же самое значение, поэтому ререндера компонента List не происходит.

‼️ Проблема #2. Преобразование ссылочных типов данных

Если у нас есть, например, массив каких-то значений (например, пользователей), а мы в селекторе хотим получить только идентификаторы этих пользователей, то мы используем метод массива .map(). При этом каждый раз создается новый массив, даже если состоит он из тех же самых идентификаторов.

https://codesandbox.io/s/selectors-problems-2-react-junior-8mzn1u?file=/src/store.js

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

Как решить эти проблемы - в следующем посте.

#управлениесостоянием #redux #оптимизация #важно
👍2
Reselect - оптимизированные селекторы

В прошлом посте отметили две проблемы селекторов в Redux (возможно, есть еще). Теперь поговорим о решении.

Документация Redux рекомендует использовать библиотеку reselect для создания оптимизированных селекторов.

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

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

Как это работает

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

Решение проблемы #1 (тяжелые вычисления)

https://codesandbox.io/s/selectors-problems-1-reselect-react-junior-0lttp3

В качестве "входного" селектора используем функцию getItemsList, которая отслеживает только изменение store.items.
"Выходной" селектор с тяжелым вычислением будет вызван и выполнен только если изменится результат работы "входного" селектора.

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

Решение проблемы #2 (маппинг массива)

https://codesandbox.io/s/selectors-problems-2-reselect-react-junior-33czrp?file=/src/components/List.js

Тут решение такое же, как и в прошлом примере. Результат преобразования зависит только от исходного значения. Поэтому мы выносим в отдельный "входной" селектор проверку store.items. Если этот массив не меняется, то и селектор не будет вызван.

#управлениесостоянием #redux #оптимизация #важно
IT-KAMASUTRA о Reselect

У меня была какая-то необъяснимая сложность в понимании reselect, возможно, документация не очень правильно о нем говорит, ну либо причина гораздо более простая 🤓.

В общем, мне немного помогли разобраться три видео из курса по React с канала IT-KAMASUTRA.

81 - React JS - селекторы (reselect part 1)
82 - React JS - mapStateToProps (reselect часть 2)
83 - React JS - подключаем reselect (reselect часть 3)

Есть одна сложность - в курсе речь идет о старой версии redux, в которой не было хуков useDispatch и useSelect. Там все было посложнее, использовался компонент высшего порядка (connect), а глобальное состояние преобразовывалось в пропсы с помощью функций mapStateToProps и mapDispatchToProps. Это может вызвать трудности.

#ссылки #redux #управлениесостоянием
Reselect

Библиотека для создания мемоизированных селекторов от создателей redux: https://github.com/reduxjs/reselect

Позволяет вычислять "производное" состояние (в хранилище остаются только минимально необходимые данные, все остальное вычисляется).

Селекторы пересчитываются, только если изменяются их зависимости.

Кроме того, созданные селекторы можно использовать как "входные" для других селекторов.

createSelector (еще раз для полного осознания)

Для создания селектора используется функция createSelector. Она принимает один или несколько "входных" селекторов (в виде массива или просто отдельными аргументами) и один "выходной". Еще может принимать объект с настройками.

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

Для сравнения результатов используется алгоритм строгого сравнения (===, сравнение по ссылке).

Размер кэша

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

Подробнее - в документации. Там есть утилитарные функции для мемоизации и ряд опций для настройки, а также FAQ с распространенными проблемами.

#redux #управлениесостоянием
👏1
Валидация форм в React-приложениях с помощью хука useForm

Статья (англ.): https://blog.openreplay.com/react-form-validation-with-the-useform-hook

Валидация форм в вебе - очень важная задача, которая так до сих пор и не стандартизирована полноценно. Существуют десятки разных решений, и в статье разбирается одно из них - библиотека React Hook Form https://react-hook-form.com/get-started.

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

Предлагаемый библиотекой api интуитивно понятный, хоть и немного громоздкий (впрочем, где вы видели негромоздкую валидацию, да?)

https://codesandbox.io/s/react-hook-form-react-junior-qyv1if?file=/src/App.js

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

Использование

Хук useForm может принимать базовые настройки валидации (можно ничего не передавать и использовать дефолтные) и возвращает несколько методов (`register`, handleSubmit`), а также объект `formState. По названиям очевидно, для чего они предназначены:

* register - регистрирует каждое валидируемое поле с конкретными настройками валидации.
* в handleSubmit нужно передать функцию, которая будет вызвана при удачной отправке формы. Обычно такие методы вызываются на событие form.onsubmit.
* formState содержит актуальную информацию, включая ошибки валидации.

Регистрация поля

Функция register принимает имя поля и объект с настройками валидации и возвращает набор атрибутов для контрола (для отслеживания фокуса и изменений значения). Есть ряд базовых опций, таких как required, minLength, maxLength, pattern. Для каждой настройки можно просто ставить true или конкретное значение (например, минимальная длина или паттерн), а можно передать объект с полями value и message.

Кроме того, можно установить собственную функцию для валидации.

formState

Содержит флаги isSubmitted, isValid, объект с ошибками errors и др.

#ссылки #валидацияформ
👍1👏1
Статус асинхронного запроса

В документации Redux описан хороший паттерн работы с асинхронными запросами. Он не относится непосредственно к Redux, применять его можно где угодно.

Очень часто для отслеживания состояния запроса используют флаги типа isLoading, isLoaded, но намного удобнее иметь дело со строковыми статусами: idle, loading, succeeded, failed. Так проще охватить все возможные состояния и проанализировать их для выведения нужного представления.

Подробнее в документации: https://redux.js.org/tutorials/fundamentals/part-7-standard-patterns#async-request-status

Пример: https://codesandbox.io/s/async-request-status-react-junior-itoqep?file=/src/store/index.js

#redux #документация #паттерны #примерыкода
👏2
Flux Standard Actions

Соглашение по структуре экшенов в Redux:

- экшен - это обычный JavaScript-объект
- у него обязательно есть поле type
- любые данные должны лежать в поле payload
- кроме того у экшена может быть поле meta для различной описательной информации
- при необходимости экшен может иметь поле error (булево значение). Если оно равно true, экшен представляет собой ошибку и является аналогом промиса в состоянии rejected. В этом случае в поле payload должен находиться объект ошибки.

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

Во всех предыдущих примерах элементы хранились в store в виде массива. Но в больших проектах со сложным состоянием разумно использовать другую структуру данных - словарь с доступом по идентификатору (обычный JavaScript-объект).

Это способ "нормализовать" состояние.

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

Допустим, у нас есть массив пользователей и массив постов, которые можно лайкать. Для каждого поста нужно сохранять список пользователей, которые его лайкали. Разумнее сохранять только идентификаторы этих пользователей, а не копировать их полностью. Ведь если придется вносить какие-то изменения, их придется вносить в нескольких местах, что увеличивает вероятность ошибки. При необходимости мы всегда можем получить полные данные пользователя, и удобнее это делать из объекта-словаря (время доступа О(1)), чем из массива (время доступа О(n)).

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

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

Раздел в документации (англ.): https://redux.js.org/tutorials/fundamentals/part-7-standard-patterns#normalized-state

#redux #документация #паттерны
👍3
Возращение промиса из thunk

И еще один полезный паттерн, который предлагает нам документация Redux.

Для выполнения асинхронных экшенов мы используем пакет thunk, который позволяет передавать в dispatch функции вместо объектов. Это удобно, например, делать для ajax-запросов к серверу.

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

Это состояние можно хранить в глобальном хранилище, но все-таки чаще ему место в компоненте.

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

Уместо поместить логику отображения лоадера в сам компонент, а не в глобальное хранилище. И это сделать очень просто.

Когда мы передаем в dispatch функцию, thunk возвращает результат ее выполнения. Этим результатом вполне может быть промис, ориентируясь на состояни которого мы можем отображать или скрывать лоадер.

Схема работы

https://codesandbox.io/s/thunks-and-promises-react-junior-5edjgl?file=/src/store.js

Функция addNewTodo(text) это создатель экшена, она возвращает thunk вместо обычного объекта.


function addNewTodo(text) {
return function (dispatch) {
return new Promise(function(resolve) {
// запрос на сервер
dispatch(...)
resolve()
})
}
}


При добавлении нового элемента вызываем dispatch(addNewTodo(text)).

В dispatch попадает функция, а не объект, поэтому она сразу же запускается, а результат этого запуска возвращается.


const [ loading, setLoading ] = useState(false)

const handleSubmit = function() {
setLoading(true)
dispatch(addNewTodo(text)).then(function() {
setLoading(false);
})
}


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

#redux #управлениесостоянием #примерыкода #паттерны
👍3
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