React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Селекторы и ререндеринг при изменении данных

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

В компоненте 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 #примерыкода #документация
Создаем свой React с рендером и useState за 30 минут

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

Статья (рус.): https://habr.com/ru/post/652487/

Спойлер: React будет урезанный, только рендер и хук useState, без виртуального DOM и согласования изменений

1. Настройка проекта с использованием parcel и typescript для компиляции JSX.
2. Реализация React.createElement и ReactDOM.render для создания HTML-разметки из JSX.
3. Реализация метода для ререндера.
4. Реализация useState с сохранением состояния между рендерами.

В итоге получается нечто, что примерно соотствествует формуле React: интерфейс - это функция от состояния. Но без всяких оптимизаций.

Почитать интересно, чтобы еще раз это в голове уложить.

#ссылки #подкапотом
Road Map ReactJS-разработчика в 2022 году

Статья (англ.): https://medium.com/javarevisited/the-2019-react-js-developer-roadmap-9a8e290b8a56

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

Плюс, тут есть несколько ссылок на статьи с подборками источников знаний:

🔸 6 Best Websites to Learn React.js Coding for FREE in 2022
🔸 6 Best React.js Books for Beginners and Experienced Web Developers
🔸 Top 10 Online Courses to Learn React.js in Depth — Best of Lot


Итак, что должен изучить хороший React-разработчик?

✔️ 1. Основы (HTML, CSS, JS)

Без базовых знаний, понятно, никуда.

✔️ 2. Базовые скиллы разработчика

Тут и инструменты: Git, HTTP(S)-протокол со всеми основными методами (GET, POST, ...), работа с терминалом.
и теория: алгоритмы, структуры данных, шаблоны проектирования.

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

✔️ 3. React JS

Ну, конечно, нельзя стать React-разработчиком, не изучив React JS.

✔️ 4. Инструменты сборки

React - современный инструмент, который использует под капотом много технологий, поэтому необходимо особым образом организовывать свои проекты, чтобы они работали в реальных условиях, например, в браузере. Для этого нужно знать, что такое менеджеры пакетов (npm, yarn, ...) и сборщики (webpack, parcel, rollup, gulp...).

✔️ 5. Стилизация

React поддерживает множество подходов к стилизации компонентов (это мы уже проходили https://yangx.top/react_junior/142), и нужно иметь о них представление. Но автор статьи советует начать с Bootstrap.

✔️ 6. Управление состоянием

Состояние - важная концепция в React, поэтому его изучаем отдельно (да, мы так и делаем). В списке технологий: контекст, redux с кучей доп. пакетов), mobx, ...

✔️ 7. Проверка типов

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

✔️ 8. Обработка форм

Грамотная работа с формами - очень важная часть разработки, к счастью, у нас есть множество инструментов для этого (redux form, formik, formsy, final form).

✔️ 9. Маршрутизация

Необходима для создания API. Обеспечивается React Router https://yangx.top/react_junior/230.

✔️ 10. Клиенты API

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

✔️ 11. Вспомогательные библиотеки

Инструменты, которые берут на себя отдельные задачи и облегчают жизнь разработчика. В списке: Lodash, Moment, Classnames, ...

✔️ 12. Тестирование

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

✔️ 13. Интернационализация

Очень комплексная задача, для решения которой тоже есть готовые инструменты (react intl, react i18next)

✔️ 14. SSR

Первым в списке идет Next.js, и автор считает, что этого достаточно (по крайней мере, для начала)

✔️ 15. Генераторы статических сайтов

Тут только Gatsby.js

✔️ 16. Интеграция с бэкенд-фреймворками

Например, React on Rails для интеграции с Rails.

✔️ 17. Мобильная разработка

React может работать не только в вебе. Для мобильной разработки предлагается React Native.

✔️ 18. Разработка для Desktop

Для десктопной разработки тоже есть решения: Proton Native, Electron, React Native Windows.

✔️ 19. Виртуальная реальность

Тут React 360.

#ссылки #инструменты
👍1
Расширение функциональности

Итак, мы имеем примерное представление об устройстве и работе хранилища в Redux https://yangx.top/react_junior/268.

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

Проект: https://codesandbox.io/s/react-redux-enhancers-react-junior-j20kqm?file=/src/App.js

Энхансер оборачивает функцию createStore (как декоратор) и может изменить возвращаемый объект хранилища, например, создать собственные методы dispatch, subscribe или getState.

Структура энхансера

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


export const logDispatch = function(createStore) {
return function(rootReducer, preloadedState, enhancers) {
const store = createStore(rootReducer, preloadedState, enhancers)

function newDispatch(action) {
const result = store.dispatch(action)
console.log('Dispach:', { action, result })
return result
}

return { ...store, dispatch: newDispatch }
}
}


То же самое можно сделать с методами subscribe и getState, но dispatch, наверно, чаще изменяют таким способом.

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

Чтобы применить энхансер, его нужно передать в метод createStore третьим аргументом (после редьюсера и preloadedState`). Если `preloadedState не указывается, то его можно опустить, тогда энхансер станет вторым аргументом.

Если энхансеров несколько, то их нужно объединить в один с помощью функции compose.


import { compose, createStore } from "redux";
const composedEnhancer = compose(logDispatch, logGetState, logSubscribe);
const store = createStore(rootReducer, composedEnhancer);


#управлениесостоянием #redux #началоработы #примерыкода
👍2👏1
Redux предоставляет нам один готовый энхансер - applyMiddleware. Он работает с методом dispatch хранилища, то есть на участке между получением экшена и его реальной отправкой в редьюсер.

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

Структура миддлвара

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


function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {

return next(action)
}
}
}


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

Функция applyMiddleware принимает неограниченное количество аргументов - миддлваров - функций для обработки экшена и выстраивает из них пайплайн. То есть экшен отправится сначала в первый миддлвар, потом во второй, и так далее по цепочке, пока не дойдет до самого метода store.dispatch.

При этом каждый миддлвар может прервать цепочку, если не вызовет метод next.

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

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

https://codesandbox.io/s/react-redux-middleware-react-junior-obvtcs?file=/src/store/index.js

#управлениесостоянием #redux #началоработы #примерыкода #документация
👍2👏1
Зачем нужны миддлвары?

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

- логирование
- запросы к серверу и другой асинхронный API
- маршрутизацию приложения
- изменение экшенов
- задержка выполнения (или прекращение)

https://codesandbox.io/s/react-redux-async-actions-react-junior-t2npxd?file=/src/store/checking/index.js

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

Для этого:

- добавляем новый редьюсер (checkingReducer) и набор экшенов (START_CHECKING, FINISH_CHECKING, SET_CHECKING_STATUS;
- добавляем компонент CheckingStatus, который отслеживает статус проверки и выводит сообщение;
- создаем миддлвар checkText, который реагирует только на события добавления нового элемента в список. Он останавливает цепочку обработки экшена и отправляет запрос на проверку текста. Только после окончания проверки он запускает цепочку обработки дальше (если нет ошибки), и экшен отправляется в редьюсер.

#управлениесостоянием #redux #началоработы #примерыкода #документация
👍1
Экшены-функции

Еще один вариант - переместить код, связанный с экшеном в функцию-создатель экшена. Но тут есть проблема - у функций-создателей нет доступа к методу store.dispatch.

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

https://codesandbox.io/s/react-redux-functional-actions-react-junior-zyleg0?file=/src/store/items/actions.js


const handleFunction = function(storeAPI) {
return function(next) {
return function(action) {
if (typeof action === 'function') {
return action(storeAPI.dispatch, storeAPI.getState)
}

return next(action)
}
}
}


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

И наконец, у Redux есть официальный миддлвар для обработки функциональных экшенов - пакет redux-thunk. Можно заменить нашу самописную handleFunction на него:

https://codesandbox.io/s/react-redux-thunk-react-junior-d71jls?file=/src/store/index.js

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

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

* Redux Essentials - изучение сверху вниз, демонстрация лучших практик и распространенных кейсов.

* Redux Fundamentals - изучение снизу вверх, от основ, для лучшего понимания.

Мы, разумеется, начали снизу вверх, как я люблю)) Сначала основы, потом лучшие практики.

#redux #документация
Поток данных в Redux

Визуализация однонаправленного потока данных: https://redux.js.org/assets/images/ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif

- UI вызывает экшен
- он отправляется в редьюсер
- редьюсер изменяет состояние
- из-за этого изменяется UI

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

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

- Состояние доступно только для чтения. Любые изменения - только через экшены и редьюсер. Таким образом, интерфейс не сможет случайно переписать состояние.

- Иммутабельность состояния. Редьюсер не мутирует состояние, а создает его копию.

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

#важно #управлениесостоянием #redux #документация
Использование 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