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

Разобравшись более-менее с маршрутизацией (значимых альтернатив React Router вроде не нашлось), переходим к новой большой (очень большой) теме: управление глобальным состоянием приложения.

В ее рамках планирую:

- освежить в памяти хук useReducer и совместить его с хуком useContext для создания глобального состояния
- взглянуть на Redux (должно быть что-то очень похожее на предыдущую комбинацию)
- попробовать Redux Toolkit (набор инструментов для работы с Redux)
- разобрать альтернативные решения - Recoil, MobX и, может, что-то еще

#состояние #управлениесостоянием
👍3
Что такое стейт-менеджер

Заметка (рус.): https://gist.github.com/nodkz/41a5ee356581503033bd05104f5048bf

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

Главный вывод

Стейт-Менеджер = Переменная + Функция (для измнения переменной) + паттерн Observable (чтобы подписаться на изменение переменной)

#управлениесостоянием #ссылки
👍1
Почему React Context - это не инструмент для "управления состоянием" (и почему он не может заменить Redux)

Большая статья с подробным сравнением React Context и Redux (англ.): https://blog.isquaredsoftware.com/2021/01/context-redux-differences/

Основная идея заключается в том, что контекст сам по себе не имеет никакого отношения к управлению состоянием, а является только механизмом доставки данных без утомительного проброса пропсов. В некотором смысле это паттерн Внедрение зависимостей (Dependency Injection).

Контекст в комбинации с хуком useReducer - это уже что-то более похожее на state managment, но совсем на минималках. Тут уже можно работать с более-менее сложным состоянием. Это может быть хорошим решением для небольших проектов. Основная проблема - невозможность разделения данных. Если компонент подписывается на изменением состояния, то он будет перерендерен при любом его изменении, даже если изменилась часть, которая этот конкретный компонент не интересует. Конечно, можно создавать отдельные контексты для разных данных, но тут потенциал масштабирования очень маленький.

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

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

Статья написана очень подробно и понятно, рекомендую 🙂

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

Хук useState работает очень просто:


const [value, setValue] = useState()


У нас есть значение и функция, которой нужно передать новое значение. Мы сразу определяем, что должно лежать в value, и передаем это в setValue.

Но что если логика расчета нового value сложнее, чем прибавить единицу к предыдущему значению? Если есть несколько причин для изменения value и хотелось бы контролировать их все в одном месте, иначе отлаживать будет невозможно?

Для этого у нас есть хук useReducer, который по сути является более продвинутым useState. Говорили про него здесь. Он позволяет инкапсулировать всю логику изменения состояния в одной функции - редьюсере.


const [value, dispatch] = useReducer(reducer, initialValue)


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

Представим простейшую ситуацию: у нас есть некий счетчик. При нажатии на кнопку +1 мы хотим увеличить его значение на единицу.

С useState это выглядит так:


setValue(function(currentValue) {
return currentValue + 1;
})


С useReducer мы передаем не значение, а действие:


dispatch({ type: 'increment' })


Это действие передается в редьсер, который уже вычисляет новое значие value, исходя из типа экшена.

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

#хуки #управлениесостоянием #состояние
Очень простой пример использования useReducer

https://codesandbox.io/s/usereducer-react-junior-5x89x8?file=/src/App.js

Редьюсер и начальное состояние вынесены в отдельный файл state.js. Функция-редьюсер получает экшен, смотрит на его тип и производит все необходимые изменения, возвращает обновленное состояние.

Экшены отправляются с помощью функции dispatch, которую возвращает хук.

У нас есть четыре типа экшенов, которые понимает редьюсер: increment, decrement, add (с полезной нагрузкой, параметром, который определяет, сколько именно нужно прибавить к счетчику) и reset.

#примерыкода #хуки #состояние #управлениесостоянием
useReducer + useContext

Реализуем управление состоянием с помощью только хуков React (комбинация useReducer + useContext), без использования сторонних библиотек.

https://codesandbox.io/s/usereducer-usecontext-react-junior-um0bsr?file=/src/App.js

*Пример условный, упрощенный и неоптимизированный, только для демонстрации работы с состоянием

Мы имеем тут какой-то личный финансовый кабинет. У пользователя есть баланс средств, баланс бонусов (10 бонусов начисляется, если пользователь вносит больше 1000), а также возможность пополнить баланс или снять деньги.

Состояние

Все состояние хранится в одном месте (файл state.js). Там же определен редьюсер, который понимает экшены двух типов put (пополнение) и withdraw (снятие). Редьюсер меняет баланс, следит за начислением бонусов, сохраняет историю операций. Кроме того, в состояние добавлено поле error - какая-либо ошибка при выполнении операций.

Контекст

Контекст используется, чтобы передавать состояние приложения в любой заинтересованный компонент без проброса пропсов. Все кейсы использования контекста (компонент-провайдер и хук для доступа к контексту) собраны в файле depositeContext.js.

Схема работы

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

#хуки #управлениесостоянием #состояние #примерыкода
Задача: при внесении большой суммы на счет (больше тысячи) нужно добавлять клиенту бонусы. Где должна находиться эта логика (проверка суммы)?
Anonymous Poll
65%
В редьюсере
20%
В компоненте
15%
Где-то еще (где?)
useReducer + useContext + асинхронное обновление состояния

Еще один небольшой проект с глобальным состоянием на хуках: https://codesandbox.io/s/usereducer-usecontext-async-react-junior-i13xix?file=/src/App.js

Это приложение для поиска аниме с фильтрами. Для поиска аниме используется бесплатное апи jikan.moe. Для отправки запросов - библиотека axios.

В состоянии помимо списка подходящих аниме хранится также флаг loading, обозначающий процесс загрузки данных.

При нажатии на кнопку Search диспатчится событие START_LOADING, а при получении данных событие UPDATE.

⁉️ Сама логика запроса списка находится в компоненте App, показалось неправильным размещать ее ниже по дереву, но это не точно, конечно. Вместо dispatch вниз по контексту уходит готовый метод обновления списка updateList. Вообще пока не очень понятно, как правильно работать с асинхронным обновлением состояния, как технически реализовывать и где ему место. Ведь в редьюсер его не положишь.

#управлениесостоянием #хуки #примерыкода
Глубокое погружение в код React. Часть 2. Самые популярные пакеты

Глубокое погружение в код React. Часть 1. Вводная

Продолжаем разбор кодовой базы React (англ.): https://dev.to/fromaline/deep-dive-into-react-codebase-ep2-what-package-from-the-repo-is-the-most-popular-on-npm-2328

Вторая часть серии - это краткий обзор пакетов, входящих в состав монорепозитория React. Тут есть утилиты вроде react-is и scheduler, инструменты для разработчиков, включая плагин для ESLint, инструменты для тестирования, для создания подписок, а также несколько экспериментальных нестабильных пакетов.

Во второй части статьи автор приводит статистику использования по всем пакетам React. Оказывается, сам React (ядро проекта) находится только на третьем месте по количеству скачиваний. Его уверенно обходят утилиты (react-is и scheduler). На четвертом месте предсказуемо react-dom, на пятом - плагин для ESLint.

#ссылки #подкапотом
Разделение контекстов

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

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

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


component Providers
return StateProvider
DispatchProvider
props.children

component App
return Providers
Components


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


component Providers
return StateProvider
DispatchProvider
Components


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

Пример: https://codesandbox.io/s/separate-contexts-react-junior-p06kbr?file=/src/App.js

UPD: Ответ в следующем посте ⬇️

#управлениесостоянием #контекст #хуки #паттерны #вопросы
👏1
Компоненты, вставленные через props.children, не обновляются при рендере родительского компонента

Разумеется, этим вопросом уже задавались люди до меня 🙂

Развернутый ответ: https://stackoverflow.com/questions/47567429/this-props-children-not-re-rendered-on-parent-state-change/57332409#57332409?newreg=c8b54a9215714a22b3d190c398e497f6

Исходная проблема

https://yangx.top/react_junior/257

Если перерендеривается родительский компонент Providers, я ожидаю, что все дочерние компоненты тоже перерендерятся. Но те компоненты, которые пришли в props.children не перерендериваются. Но если их прямо перенести в Providers, то они перерендериваются, как и ожидается.

Решение

Компонент App на JSX выглядит так (схематично):


Providers
Child


А на обычном JavaScript это выглядит так:


React.createElement(
Providers,
{},
React.createElement(
Child,
{},
{}
)
)


Именно этот код запускается при каждом рендере компонента App.

Код для рендера компонента Providers же выглядит так:


React.createElement(
'div',
{ },
this.props.children
)


Объект this.props.children здесь всегда представлен одной и той же ссылкой, он не меняется, а значит у React нет причин перерендеривать его.

Если же поместить Child напрямую в Providers, мы код рендера Providers изменится:


React.createElement(
'div',
{ },
React.createElement(
Child,
{},
{}
)
)


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

Выводы

1. JSX очень расслабляет и заставляет забыть о реальном JavaScript, который работает под капотом.
2. Четко сформулировать проблему - большой шаг к ее решению.
3. Не может ли возникнуть других ошибок в этом месте, связанных с необновлением дочерних компонентов?

Можно ли без JSX: https://yangx.top/react_junior/30

#вопросы #подкапотом #ошибки #важно
👏1
Оптимизация useReducer + useContext

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

https://codesandbox.io/s/usereducer-usecontext-optimization-react-junior-u5z6q7?file=/src/App.js

1. Используем разные контексты для передачи состояния и метода dispatch (и то, и то возвращается из хука useReducer).
2. Выносим провайдеры обоих контекстов в отдельный компонент-обертку и туда же помещаем вызов useReducer. Таким образом, при изменении состояния произойдет перерендер этого компонента, но вложенные компоненты не будут обновляться. (почему?)

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

#управлениесостоянием #хуки #оптимизация
👏1
Redux: начало

Начнем как всегда с начала, то есть с документации: https://redux.js.org/introduction/getting-started. Она, кстати, очень большая, с кучей разных туториалов и объяснений.

Итак, основные части Redux:

Хранилище (store)

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

Действия (actions)

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

Редьюсер

Функция, которая принимает на вход объект действия, а возвращает обновленное состояние. Редьюсер инкапсулирует всю логику изменения. Таким образом, все действия отправляются в редьюсер.

Все это нам уже известно на примере встроенного хука useReducer.

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

1. Определяемся со структурой состояния
2. Определяем, какие экшены доступны для его измнения
3. Создаем редьюсер, обрабатывающий эти экшены
4. Создаем объект хранилища с помощью метода createStore(reducer)

Взаимодействие с хранилищем

Чтение данных: store.getState()
Подписка на изменения: store.subscribe(cb)
Отправка действия: store.dispatch(actionObject)

#управлениесостоянием #redux #документация #началоработы
👏1
Redux не связан с React

Самый базовый пример работы Redux, без React - только ванильный JS + HTML (чтобы не было никакого волшебства):
https://codesandbox.io/s/redux-basic-forked-react-junior-ud3hq0?file=/index.js

Здесь используются все основные концепции:

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

Вообще Redux самостоятельная библиотека, не привязанная к какому-либо фреймворку. Она маленькая сама по себе (2kB), но кроме ядра (redux) нам понадобятся дополнительные функции, чтобы связать redux и react (пакет react-redux).

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

Вот пример из документации, где react работает вместе с redux без дополнительных биндингов: https://codesandbox.io/s/github/reduxjs/redux/tree/master/examples/counter?from-embed=&file=/src/index.js

Здесь просто на каждое изменение хранилища вручную вызывается ReactDOM.render.

#управлениесостоянием #примерыкода #redux
👏3
Связка React-Redux

Мы уже констатировали, что Redux - это абсолютно самостоятельная библиотека. У нее есть API, с помощью которого ее можно подключить к любому приложению, на любом фреймворк или без него.

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

Что у нас тут есть?

🔸 1. Создание хранилища

Это сфера ответственности Redux, тут никаких специальных методов не требуется.

🔸 2. Получение данных из хранилища

Передача данных из хранилища в приложение в react-redux работает по модели контекста. Все приложение оборачивается в компонент Provider, а в заинтересованных компонентах нужно использовать хук useSelector, чтобы получить доступ к хранилищу. Этот хук принимает функцию-селектор. При каждом изменении хранилища она вызывается, получает текущее состояние в качестве аргумента и может надергать из него все, что нужно.

🔸 3. dispatch

Чтобы отправить экшен в хранилище, есть хук useDispatch, работает точно так же, как метод store.dispatch (это он и есть, в общем).

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

Приложение

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

Стандартная "тудушка", с массивом элементов и возможностью добавить/отредактировать/удалить их.
В глобальное состояние вынесен только массив элементов (items), состояние инпута для ввода текста хранится локально в компоненте.
Использованные пакеты: redux, react-redux.

#управлениесостоянием #redux #началоработы #примерыкода
Селекторы и ререндеринг при изменении данных

В нашем приложении есть одна проблема.

В компоненте List мы получаем из state список элементов с помощью функции-селектора. Из-за того что редьюсер является чистой функцией, при каждом изменении этот список копируется, то есть в селекторе получается каждый раз НОВЫЙ массив. Хук useSelector сравнивает его с предыдущим значением, считает, что произошли изменения, и перерендеривает весь компонент List.

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

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

Изменим код приложения. В компоненте List будем получать только список идентификаторов элементов, чтобы понять, не изменился ли состав списка. А получение собственно данных перенесем в компонент Item. То есть каждый элемент будет самостоятельно запрашивать данные для своего отображения (текст и флаг completed`), зная только свой `id. Таким образом, если если данные элемента изменятся, то перерендерится только он один.

А для списка добавим функцию сравнения, которая будет проверять, не отличается ли новый набор идентификаторов от старого.


const selector = function (state) {
return state.items.map(function(item) {
return item.id;
});
};
const equalityFn = function (newState, oldState) {
return JSON.stringify(newState) === JSON.stringify(oldState);
}

//...

const items = useSelector(selector, equalityFn);


Но можно не писать функцию сравнения самому, а взять уже готовую shallowEqual из пакета react-redux.

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

https://codesandbox.io/s/react-redux-equalityfn-react-junior-4ne0m5?file=/src/components/List/index.js

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

Один из паттернов, который рекомендует нам документация Redux, - это Создатели Экшенов.

То есть мы не передаем в dispatch объект действия напрямую, а вызываем функцию, которая нам его создает.

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

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

Еще один хороший прием - разделение одного редьюсера на несколько. Для каждой части приложения свой.

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

https://codesandbox.io/s/react-redux-combine-reducers-react-junior-ry72g1

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

У меня они находятся в папках store/filters и store/items. Документация предлагает делить немного по-другому, но это нужно будет отдельно разобрать.

Перед созданием хранилища нужно собрать два редьюсера в один:


const rootReducer = function(state = {}, action) {
return {
items: itemsReducer(state.items, action),
filters: filtersReducer(state.filters, action)
};
};


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

Вместо ручного объединения лучше использовать готовую функцию combineReducers из пакета react-redux.

#управлениесостоянием #redux #началоработы #примерыкода #документация
Префиксы в экшенах

При разделении приложения на несколько "фич" рекомендуется использовать префиксы в типах экшенов. Вместо addItem - items/addItem.

#redux #управлениесостоянием
Упрощенный код хранилища Redux


function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = []

function getState() {
return state
}

function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}

function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}

dispatch({ type: '@@redux/INIT' })

return { dispatch, subscribe, getState }
}


1. В функцию создания хранилища createStore можно передать исходное состояние - параметр preloadedState
2. После создания вызывается инициализирующий экшен

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