React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Redux-saga. Multicast-каналы

И наконец в дополнение ко всем уже разобранным каналам у нас есть multicast-каналы. Они работают так же, как и обычные каналы (channel), только уведомление о поступившем событии получают ВСЕ подписчики.

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


import { multicastChannel } from 'redux-saga'

function* watchRequests() {
const channel = yield call(multicastChannel)

// подписываем разные обработчики
yield fork(handler1, channel)
yield fork(handler2, channel)

while (true) {
const { payload } = yield take('REQUEST')
yield put(channel, payload)
}
}


#redux #управлениесостоянием #примерыкода #saga
👍2
Redux-saga. Кастомный I/O

Обычно мы подключаем саги к стору приложения с помощью миддлвара. Эффекты take и put связываются со стором и слушают его/диспатчат в него новые экшены.

Но можно запустить сагу отдельно от стора, если это необходимо.

Для этого нам нужно создать кастомный "стор" с полями
- channel,
- dispatch
- и getState.

То есть у этого стора должен быть канал, в который будут поступать события - его создаем с помощью функции stdChannel. Этот канал нужно соединить с внешним I/O.

Для запуска вызываем функцию runSaga и передаем ей новый стор и собственно сагу.


import { runSaga, stdChannel } from 'redux-saga'

const emitter = new EventEmitter()
const channel = stdChannel()
emitter.on("action", channel.put)

const myIO = {
channel,
dispatch(output) {
emitter.emit("action", output)
},
getState() {
return state
}
}

runSaga(
myIO,
function* saga() { ... },
)


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

На страничке Recipes https://redux-saga.js.org/docs/recipes в документации redux-saga есть несколько полезных сниппетов:

- throttling
С использованием встроенного эффекта throttle.

- debouncing
Сохраняем активный воркер и отменяем его (`cancel`), если пришло новое событие, либо используем встроенный эффект takeLatest.

- повторное выполнение неудачных XHR-запросов
С использованием встроенного эффекта retry или комбинацией базовых эффектов call, take, delay и блока try-catch.

- функционал Отменить последнее действие
С помощью spawn и race.

- batching нескольких экшенов
С использованием библиотеки redux-batched-actions.

#redux #управлениесостоянием #документация #saga
👍3
Forwarded from TypeScript Challenge
Type Challenges #11. Tuple to object

Ссылка на задачу

Условие

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

Решение


type TupleToObject<T extends ReadonlyArray<string | number | symbol>> = {
[P in T[number]]: P
}


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


type TupleToObject<T extends readonly PropertyKey[]> = {
[P in T[number]]: P
}


Для получения набора элементов массива используем конструкцию T[number].

#easy
👍6
Forwarded from TypeScript Challenge
Type Challenges #189. Awaited

Ссылка на задачу

Условие

Предположим, что у нас есть тип, завернутый в другой тип, например, Promise<string>. Нужно написать утилиту MyAwaited для получения внутреннего типа.

Решение

Чтобы решить задачку, нужно понять, какая именно обертка может быть. Посмотрим на тестовые кейсы:


type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }

type cases = [
Expect<Equal<MyAwaited<X>, string>>,
Expect<Equal<MyAwaited<Y>, { field: number }>>,
Expect<Equal<MyAwaited<Z>, string | number>>,
Expect<Equal<MyAwaited<Z1>, string | boolean>>,
Expect<Equal<MyAwaited<T>, number>>,
]


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

Для описания обертки нам подойдет встроенная утилита PromiseLike<T>, осталось только вывести вложенный тип с помощью infer:


type MyAwaited<T> = T extends PromiseLike<infer A> ? A : never;


Но не забываем, что внутри обертки может быть еще одна обертка, поэтому нужно добавить немножко рекурсии:


type MyAwaited<T> = T extends PromiseLike<infer A> ? (A extends PromiseLike<infer B> ? MyAwaited<A> : A) : never;

#easy
👍2
Redux ToolKit: краткий конспект

Собираюсь повторить RTK Query (а затем разобраться с React Query). Поэтому краткая выжимка по Redux Toolkit:

1. Мыслим слайсами

Функция createSlice создает сразу и action creators, и редьюсер, и даже набор селекторов, если мы работаем с EntityAdapter.

2. Асинхронщина

Вся асинхронщина через createAsyncThunk + extraReducers.

3. Селекторы

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

4. Связь с компонентами

В компонентах используем классические хуки useSelector и useDispatch.

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

Стор создается с помощью configureStore.

***

В целом это все, в основном удобный синтаксический сахар, ничего сверхъестественного поверх базового Redux нет.

#управлениесостоянием #redux
👍5
Forwarded from TypeScript Challenge
Type Challenges. Уровень easy. Резюме

Итак, мы прорешали 13 задачек уровня easy.

4 - Pick
7 - Readonly
11 - Tuple to Object
14 - First of Array
18 - Length of Tuple
43 - Exclude
189 - Awaited
268 - If
533 - Concat
898 - Includes
3057 - Push
3060 - Unshift
3312 - Parameters

Проведем небольшую ретроспективу и отметим основные моменты решений.

🟢 1. extends

Самое популярное ключевое слово в решениях.

Используется для уточнения типов в дженериках (9 задач):


type MyPick<T, K extends keyof T>


Используется для сравнения в условных типах (2 задачи):


type If<C extends boolean, T, F> = C extends true ? T : F


Используется в комбинации с ключевым словом infer для выведения одних типов из других (4 задачи):


type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never


🟢 2. Копирование ключей объекта (3 задачи)

Используется для создания Mapped Types, когда новый тип повторяет ключи исходного


type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}


🟢 3. keyof

Ключевое слово для получения набора из всех ключей объекта (2 задачи)


type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}


🟢 4. Превращение массива типов в перечисление типов(1 задача)

Используем конструкцию T[number].


type TupleToObject<T extends ReadonlyArray<string | number | symbol>> = {
[P in T[number]]: P
}


🟢 5. Использование readonly структур для работы с кортежами (3 задачи)


type Length<T extends readonly any[]> = T['length']


🟢 6. Деструктуризация массивов (5 задач)


type Push<T extends any[], U> = [...T, U]


#easy
👍4
RTK Query

Освежаю знания об RTK Query

RTK Query - это часть Redux Toolkit, которая предназначена для удобной работы с API. Она собирает все, что относится к API в одном месте и дает нам много приятного синтаксического сахара.

Заводим для API отдельный слайс, где будут лежать все данные, полученные с сервера. Но не обычный слайс, который createSlice, а прокачанный слайс, который createApi.
Подключить этот api-слайс к стору посложнее, чем обычный, тут нужно и редьюсер добавить, и специальный миддлвар.

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

На этом техническая подготовка заканчивается и начинается собственно логика обмена данными - поле endpoints. Тут мы получаем builder и с его помощью (`builder.query`) создаем массив наших эндпоинтов (есть много разных настроек) . Каждый эндпоинт на выходе из слайса создает себе личный хук , который можно дернуть в любом месте приложения. А в этом хуке уже предусмотрена куча удобных поле - data, isLoading, error и т.п. И дополнительно можно закинуть ряд настроек, вроде селектора . То есть вытаскивать данные из API со всеми удобствами мы уже можем. Можно даже сочетать все это дело с EnitityAdapter.

Примечание: можно выполнить запрос и «вручную» без использования хука.

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

Самое приятное, что запросы к серверу кешируются, однако кеш иногда приходится сбрасывать. Это можно сделать напрямую руками, с помощью метода refetch, который есть в хуках. Но лучше настроить зависимости с помощью системы тегов, чтобы RTK Query сбрасывала то, что нужно, самостоятельно.

Конечно, api-слайс можно расчленять (injectEndpoints).

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

Предыдущее резюме по RTK Query, с большим количество технических подробностей.

#управлениесостоянием #rtkquery #обменданными
🔥3👍2
Forwarded from TypeScript Challenge
Type Challenges #12. Chainable Options

Ссылка на задачу
Ссылка на песочницу с тестами

Условие

У нас есть некий объект/класс с двумя методами: option(key, value) и get(). В первый можно передать имя поля (строка) и любое значение для него. Таким образом мы "набираем" себе объект с нужными полями. Причем вызовы метода option можно объединять в цепочку (чейнить). Метод get возвращает этот "собранный" объект.


declare const config: Chainable

const result = config
.option('foo', 123)
.option('name', 'type-challenges')
.option('bar', { value: 'Hello World' })
.get()

interface Result {
foo: number
name: string
bar: { value: string }
}


Задача: типизировать класс Chainable.

Важный момент: нельзя два раза передать в option одно и то же свойство.

Решение

Тут нам нужно решить две задачи:
- сохранять переданные ключи и значения, чтобы набрать нужный тип и вернуть его в get - для этого сделаем Chainable дженериком и будем просто дописывать новые данные
- запретить передавать один ключ дважды - для этого будем проверять, если ли у нас уже такой ключ и возвращать never если нет


type Chainable<T = {}> = {
option: <K extends string, V>(
key: K extends keyof T ? never : K,
value: V
) => Chainable<Omit<T, K> & Record<K, V>>;
get: () => T;
}


#medium
👍2
React Query за 10 минут

Видео (рус): https://www.youtube.com/watch?v=4-SOv7eTfoQ

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

Видео объясняет, как:

- получить данные через useQuery(key, fetchFn), в том числе как сделать запрос с параметрами (например, с номером текущей страницы)
- как получить состояние запроса (isLoading, error)
- как работает кеширование и как оно помогает улучшить пользовательский опыт
- зачем React Query делает фоновые запросы
- как обновить данные на сервере через useMutation(mutationFn, config)
- как принудительно сбросить кеш запроса (инвалидация кеша)

React Query умеет все то же самое, что и RTK Query, но работает по-другому: в форме хуков, без общего стора. С RTK Query мы выносили все, что связано с api в отдельный слайс, в отдельный файл и работали с этими данными отдельно. React Query же работает как хук, внутри компонента и предоставляет все необходимое на месте.

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

#управлениесостоянием
👍4
Forwarded from Cat in Web
Решаем задачки

Если интересно порешать задачки, то заходите 🤓:

- TypeScript Challenge
- Leetcode Challenge

#challenges #реклама
🔥2
React Query vs RTK Query: всестороннее сравнение

Статья (англ.): https://www.frontendmag.com/insights/react-query-vs-rtk-query/

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

Сходство

🔹Оба инструмента используют лучшие performance-практики работы с удаленными данными:

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

🔹Оба инструмента предлагают решения для самых распространненных задач получения данных:

- пагинация - обновление списка данных
- оптимистические обновления

Отличие

Отличие заключается в архитектуре инструментов.

🔸React Query - это независимая библиотека. Ее функциональность реализована в виде хуков, то есть привязана к жизненному циклу React-компонентов.

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

Кривая обучения

Автор считает, что и React Query, и RTK Query интуитивно понятны. Это довольно спорное утверждение, так как чтобы понять и то, и другое, требуется немного отформатировать свое понимание работы с данными.

Что касается RTK Query, тут вообще требуется сначала разобраться с Redux Toolkit - а это дело непростое.

Когда что использовать (привет, кэп)

RTK Query лучше использовать, если:

- В приложении уже используется Redux и Redux Toolkit. Тогда RTK Query доступна из коробки и уже интегрирована с прочими инструментами.
- Требуется сложная логика управления данными. RTK Query - это более мощный инструмент и на нем можно построить более сложную систему.

React Query лучше использовать, если:

- В приложении не используется глобальное состояние или запросы данных с ним не взаимодействуют. React Query не предоставляет никаких способов создания глобального стейта или взаимодействия с ним, все только внутри компонента.
- Требуется простая логика обработки данных.
- Не хочется долго разбираться.
- Не хочется тянуть зависимости в виде Redux Toolkit.

#управлениесостоянием #redux
👍1
Forwarded from Cat in Web
Переменная объявлена без указания типа:
const a = 42
Какой тип выведет для нее TypeScript?
Anonymous Quiz
72%
number
10%
42
13%
any
4%
unknown
1%
undefined
👍1👏1
Forwarded from TypeScript Challenge
Type Challenge #1042. IsNever

Ссылка на задачу
Ссылка на песочницу с тестами

Условие

Реализовать утилиту IsNever<T>, которая возвращает true, если в качестве аргумента получает never. В остальных случаях возвращает false.

Решение

Очень важная задачка, так как она затрагивает тему "распределенных" условных типов (описание в документации). Когда у нас есть дженерик с условным типом (extends) и он получает в качестве параметра union, он себя ведет "распределенно", то есть применяется к каждому элементу объединения.

И тут мы иногда имеем странные вещи:

// тут все нормально (без дженериков)
type A = never extends never ? 'yes' : 'no'
let a: A // a: 'yes'

// а тут что-то пошло не так
type B<T> = T extends never ? 'yes' : 'no'
let b: B<never> // b: never


Штука в том, что распределенный условный тип B ожидает union, а получает never, который интерпретирует как пустой union - и вообще не выполняется.

Есть лайфхак, чтобы отменить распределенное поведение: обернуть обе части условного типа в квадратные скобки:

type B<T> = [T] extends [never] ? 'yes' : 'no'
let b: B<never> // b: 'yes'


Зная это, решение составить очень просто:

type IsNever<T> = [T] extends [never] ? true : false


#medium
👍2
Вариантность и совместимость типов в TypeScript

Попробуем разобраться, что такое ковариантность и контравариантность и на что они влияют в TypeScript.

Подтип и супертип

Сначала определим основные понятия - подтип и супертип.

Подтип расширяет супертип.

⁃ Dog - это подтип, Animal - это супертип.
⁃ String - это подтип, Object - это супертип.
⁃ number - это подтип, any - это супертип.

Мы пишем, что подтип меньше супертипа (SubType <: SuperType), однако по факту, в подтипе больше информации, чем в супертипе, потому что подтип буквально расширяет супертип. Подтип «избыточен» по сравнению с супертипом.

Любое значение с типом SubType всегда является одновременно значением типа SuperType, так как имеет все необходимые для SuperType поля.

Структурная типизация

Важно понимать, что в TypeScript подтип и супертип необязательно связаны отношениями наследования (class Dog extends Animal). TypeScript реализует структурную типизацию и ориентируется только на структуру типов.


type Animal = {
name: string
}

type Dog = {
name: string
breed: string
}


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

Ковариантность как основная стратегия

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

Есть конкретные правила для определения совместимости. В большинстве случаев верно утверждение:

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


let a: any = 42 // number <: any
let b: Animal = new Dog() // Dog <: Animal


Довольно просто понять, почему так сделано - значение с типом SubType также является значением типа SuperType, поэтому тут нет противоречий. Если программа готова справиться с Animal (супертип), значит, она справится и с Dog (подтип), ведь он предоставляет те же самые данные.

Такая стратегия называется Ковариантностью. Она разрешает «избыточность» типа и запрещает «недостаточность».

Контравариантность для параметров функций

Стратегия ковариантности действует везде, кроме одного места - определения совместимости функций.


type Foo1 = (param: number) => void
type Foo2 = (param1: number, param2: string) => void


Казалось бы, Foo2 является подтипом для Foo1 (Foo2 <: Foo1), а значит функцию типа Foo2 должно быть можно положить в переменную с типом Foo1:


let foo1: F1 = (param1: number, param2: string) => {} // Error


Но мы получаем ошибку. При этом наоборот все работает:


let foo2: F2 = (param1: number) => {} // Ok


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

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

Такая стратегия называется «контравариантность». Она разрешает «недостаточность» типа и запрещает «избыточность».

#typescript
👍3🌭1
Как устроена система типов в TypeScript

Статья (рус.): https://ru.hexlet.io/blog/posts/sistema-tipov-v-typescript

В статье разбирается стратегия ковариантности типов на основе принципе подстановки Барбары Лисков: поведение подтипа не должно противоречить поведению супертипа.

Объясняется, что подтип более строгий, чем супертип, так как ему соответствует меньше значений (собственно поэтому SubType < SuperType).

Немного говорится об иерархии типов в TypeScript, особенностях типов any, unknown, never и void.

Приводится простой способ проверить соответствие:


type A = string extends unknown ? true : false // true
type B = unknown extends string ? true : false // false


Статья также рассказывает о двух видах приведения типов:

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

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

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

#typescript
👍3
TypeScript: Защитники типов (Type Guards)

Когда мы работаем с объединениями (union) в TypeScript, часто возникает необходимость совершить разные действия в зависимости от того, какой конкретно тип пришел.


function move(obj: Dog | Fish | Bird) {
if (obj.type === ‘dog’) obj.run()
else if (obj.type === ‘fish’) obj.swim()
else obj.fly()
}


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


function move(obj: Dog | Fish | Bird) {
if (obj.type === ‘dog’) {
// Здесь TS уже уверен, что работает именно с Dog
obj.run()
}
// …
}


Такие проверочные конструкции, которые понятны TS, называются «защитниками типа» - Type Guards. Они позволяют «сузить тип», выбрать нужный из объединения.

1. На основе типа данных

TypeScript понимает операторы instanceof и typeof.


function foo(param: number | string) {
if (typeof param === ‘string’) {
param.toUpperCase()
} else {
param.toFixed(2)
}

function move(obj: Dog | Fish | Bird) {
if (obj instanceof Dog) obj.run()
else if (obj instanceof Fish) obj.swim()
else obj.fly()
}


Эти операторы отлично распознаются компилятором внутри условий if-else, а также внутри тернарного оператора. Но не распознаются внутри конструкции switch.

2. С использование Tagged Union

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


class Bird {
type: ‘bird’ = ‘bird’
}
class Fish {
type: ‘fish’ = ‘fish’
}


Не имеет значения, как называется дискриминант, главное, чтобы он однозначно определял тип - TypeScript это понимает:


function move(obj: Dog | Fish | Bird) {
if (obj.type === ‘dog’) obj.run()
else if (obj.type === ‘fish’) obj.swim()
else obj.fly()
}


Такая проверка будет работать в любом условии: и в if-else, и в тернарном операторе, и даже в switch.

3. По наличию публичного поля

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


class Author {
public book: string = ‘Odyssey’
}
class Artist {
public painting: string = ‘Mona Lisa’
}

function getMasterpiece(creator: Author | Artist) {
let masterpiece;
if (‘book’ in creator) masterpiece = creator.book;
else masterpiece = creator.painting;
}


4. Функция-предикат

И наконец последняя (самая мощная) конструкция, которая может сузить объединение типов и которую может распознать TypeScript - это функция, которая возвращает предикат. Предикат - это однозначное утверждение, что значение, переданное этой функции, принадлежит к конкретному типу.

Такая функция оформляется особым образом - с помощью ключевого слова is:


function isDog(obj: Dog | Fish | Bird): obj is Dog {
return obj.type === ‘dog’
}


Функция принимает параметр obj и после проведения проверки утверждает, принадлежит ли obj к типу Dog.

Проверка внутри функции может быть абсолютно любая - TypeScript это не волнует. Нужно только, чтобы функция вернула логическое значение - true или false. Таким образом, ответственность за определение типа полностью перекладывается на разработчика.

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


function move(obj: Dog | Fish) {
if (isDog(obj)) {
obj.run()
} else {
obj.swim()
}
}


#typescript
👍3🔥2
React useTransition: решает все проблемы с производительностью или нет?

Статья (англ.): https://www.developerway.com/posts/use-transition

В React 18 появился "конкурентный рендеринг", цель которого решить проблемы с производительностью веб-приложений.

‼️Суть проблемы

Обновление стейта в React - штука синхронная. Как только стейт начал обновляться, поток выполнения кода блокируется. Пока не будут произведены все необходимые вычисления, пока не перерендерятся все изменившиеся компоненты, поток будет занят. Если в это время пользователь захочет повзаимодействовать со страницей, у него ничего не получится.

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

🔆 Переходные изменения для решения проблемы

React 18 вводит понятие "переходных изменений" - transitions. Переходное изменение в данном случае - это НЕ критичное изменение стейта. Например, вот этот самый список: отрисовать его не так критично, как сохранить интерактивность. Он и так большой, ничего страшного, если придется подождать чуть больше до его появления на странице.

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

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

Выглядит это все вот так:


const [list, setList] = useState([])
const [isPending, startTransition] = useTransition()

const showList = () => {
startTransition(() => {
setList(hugeList)
})
}


‼️Проблема переходных изменений

Проблема скрывается вот тут:


const [isPending, startTransition] = useTransition()


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

В статье эта проблема очень хорошо разобрана на примере.

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

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

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

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

#ссылки #concurrent #важно
👍6🔥4
Принципы SOLID в React: так ли все с ними гладко? (Часть 1)

Статья (рус.):https://nuancesprog.ru/p/19540/

Автор разбирает тонкости и подводные камни SOLID применительно к React-приложениям.

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

Принцип единой ответственности

Должна быть только одна причина для существования компонента. Но у нас же есть "контейнерные" компоненты, которые могут управлять сразу несколькими дочерними подкомпонентами.

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

Принцип открытости/закрытости

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

В React у нас есть несколько способов расширять компонент, не модифицируя его. Посмотрим на примере компонента кнопки, у которого есть проп icon - иконка. Иконка размещается перед текстом кнопки. Поступила задача - теперь должна быть возможность располагать иконку после текста. Есть несколько способов решения:
- добавить проп iconPlacement
- добавить новые свойства iconStart и iconEnd (продолжая поддерживать icon)
- или вообще создать новый компонент IconButton, наследующий от Button

Что выбрать, во многом зависит от ситуации.

Принцип подстановки Лисков

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

Однако мы и не должны это делать. Важно понимать, для чего мы создали IconButton. Он не должен заменять Button, в целом это совершенно независимый компонент, который никак не полагается на наследование от Button. Наоборот, он использует композицию.

Однако принцип Лисков вполне работает в React, в тех ситуациях, когда такая замена изначально предусматривается. Например, компонент Button фактически наследует от HTMLButtonElement - он должен и может везде его заменить.

#ссылки #паттерны
👍42