React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
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
Принципы SOLID в React: так ли все с ними гладко? (Часть 2)

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

Принцип разделения интерфейсов

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

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

Принцип инверсии зависимостей

Реализация должна зависеть от абстракций и не должна зависеть от других реализаций.

React в принципе построен на этом принципе, мы используем его, даже не замечая.

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

В итоге наш компонент UserAvatar в исходном виде нарушает сразу три приципа SOLID:
- единой ответственности (компонент в курсе бизнес-логики, знает, как устроен user)
- разделения интерфейсов
- инверсии зависимостей (импортирует вместо того, чтобы принимать, зависит от реализации, а не от абстракции)

Автор отдельно отмечает, что принцип инверсии зависимостей это не только про обработчики событий, которые мы передаем дочерним компонентам. На самом деле композиция в React - это и есть инверсия зависимостей.
Проп children - это тоже инверсия зависимостей. Благодаря ей компонент-работник, который ничего не должен знать о бизнес-логике, может содержать в себе компоненты-менеджеры, не нарушая ни одного принципа, так как ничего об этом не знает. Более того, компонент даже может определить интерфейс для этого пропа (например, принимать только строку) - то есть зависит от абстракции.

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

#ссылки #паттерны
👍6
Зачем писать юнит-тесты на фронтенд?

Статья (рус.): https://habr.com/ru/companies/nordclan/articles/755302/

Статья для тех, кто (я 🙃) очень плохо понимает, как вообще тестировать фронтенд. Методички не будет, зато будет практический пример, очень понятный.

Самый важный посыл - нужно отделять логику от UI - и помещать ее, например, в хуки. Тогда мы сможем отдельно протестировать хук. Кроме того, мы сможем подкинуть компоненту поддельную логику и проверить, как он с ней взаимодействует.

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

Пункт 1 - сначала пишем тесты, потом реализацию.

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

Пункт 2 - отделяем логику от UI.

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

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

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

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

Пункт 3 - когда логика протестирована, мы тестируем взаимодействие UI с логикой.

#ссылки #тестирование
👍2🔥1
Testing Library. Вступление

Незапланированно и стремительно перехожу к рассмотрению Testing Library и ее субпакетов.

Вот тут сайтик с документацией и примерами: https://testing-library.com/

Что это вообще за штука такая?

По сути это набор утилит для тестирования DOM, то есть нашего с вами фронтенда. Они работают с любым DOM-подобным ресурсом и помогают находить элементы и эмулировать события, как это мог бы делать пользователь.

Философия Testing Library:
«Чем больше тесты похожи на реальное использование вашего продукта, тем больше уверенности они дают».

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

Testing Library может работать с реальным DOM из браузера, а также с его эмуляциями (JSDOM, Jest).

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

Итак, Testing Library это большой набор хелперов для работы с DOM в стиле реального пользователя. Ее удобно использовать в тестах, но это не тест-раннер и не полноценный фреймворк для тестирования - просто хелпер, не привязанный к другим технологиям (только к DOM).

Утверждается, что мы можем использовать Testing Library для любых видов тестов, от юнитов до e2e (есть даже интеграция с cypress).

Итак, основной принцип: мы работаем с DOM, а не с инстансами компонентов.

#тестирование #testinglibrary #документация
👍3
Testing Library. Запросы

Основная задача при работе с DOM - поиск нужных элементов. За это у нас отвечает пакет @testing-library/dom.

Что искать?

Testing Library придерживается философии, что на элементы нужно смотреть с точки зрения пользователя, а не программиста.

⁃ Пользователь ищет элемент не по классу или testid, а прежде всего по его роли: например, поле ввода или кнопка.
⁃ Элементы формы можно искать по тексту связанного с ними элементу label или по плейсхолдеру. В крайнем случае по значению, которое находится в поле в данный момент.
⁃ Неинтерактивные элементы (параграфы, дивы) можно искать по тексту.
⁃ Есть еще варианты искать по тексту атрибутов alt и title, но он уже менее желательный.
⁃ И наконец в конце списка идет поиск по testid.

Как искать?

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

⁃ getBy… и getAllBy… (например, `getByRole`)
⁃ queryBy… и queryAllBy…
⁃ findBy… и findAllBy…

Очевидно, что методы с префиксами getBy, queryBy и findBy ищут и возвращают один элемент. При этом если найдется несколько, то все они выбросят ошибку. Методы getAllBy, queryAllBy и findAllBy ищут все подходящие элементы и возвращают массив.

Первое отличие состоит в том, как этим методы реагируют на неудачу - если не нашлось ничего. Методы группы query не очень драматизируют по этому поводу и возвращают либо null, либо пустой массив. Остальные сразу кидают ошибку.

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

Пробуем

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

Рабочий пример: https://codesandbox.io/p/devbox/compassionate-mestorf-68kddy

#тестирование #testinglibrary #документация #примерыкода
👍4
Testing Library. Screen

Итак, в Testing Library есть куча методов для поиска элементов в DOM, но они все требуют передавать им первым аргументом HTML-контейнер.

Есть другой подход, более удобный. Библиотека экспортирует объект screen, в котором тоже есть все эти методы, но уже с предустановленным контейнером. Как нетрудно догадаться, в его роли выступает document.body.

Однако как будто нет способа установить свой собственный body для screen, он берется из глобального скоупа. То есть у нас должен быть глобальный document, но так как мы не работаем с браузером, его нет.
Можно попробовать установить его искусственно:


const dom = new JSDOM(‘’)

global.window = dom.window;
global.document = dom.window.document;


Но у меня что-то не получилось, все равно кидает ошибку, что document.body недоступно.

Поэтому мы возьмем вместо jsdom другой пакет, который сделает это за нас (возьмет jsdom и закинет его на глобальный уровень, как будто мы в браузере). Дальше можно будет работать как будто внутри браузера.

Пакет называется global-jsdom, а чтобы все завелось нужно подключить модуль ‘global-jsdom/register’.


require('global-jsdom/register')

// теперь можно взаимодействовать с DOM глобально
document.body.innerHTML = ''


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

Работающий пример тут: https://codesandbox.io/p/devbox/testing-library-global-jsdom-screen-react-junior-forked-8hrw8y

#тестирование #testinglibrary #документация #примерыкода
👍3
Testing Library. Расширение и песочница

Есть прикольное расширение, которое помогает выбрать подходящий метод для поиска элемента на странице: https://chromewebstore.google.com/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano
Устанавливаем его, в DevTools появляется новая вкладка Testing Playground.
Открываем ее, наводим курсор на нужный элемент, и расширение пытается подобрать самый подходящий метод с учетом всех best practices.

А еще есть песочница, где можно ввести любой кусок HTML и поиграться с запросами - https://testing-playground.com/

#тестирование #testinglibrary #документация
👍3
Forwarded from Cat in Web
React Reconciliation: как работает и зачем нам это знать

Статья (англ.): https://www.developerway.com/posts/reconciliation-in-react
Перевод: https://gist.github.com/zagazat/db926ec7ab69061934246a55b64913c3

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

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

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

В качестве решения проблемы рассматриваются два подхода: с позицией элемента в массиве и с атрибутом key.

#react
👍3
byRole

Итак, начнем с подробного разбора самого главного запроса в Testing Library: byRole. Это методы getByRole, getAllByRole, queryByRole, queryAllByRole, findByRole, findAllByRole.

Документация: https://testing-library.com/docs/queries/byrole

Собственно роль

Первым параметром метод принимает собственно роль нужного элемента в виде строки. Это может быть button, heading, switch и так далее.

Речь идет не об атрибуте role, а прежде всего о дефолтной (встроенной) роли элементов. Например, элемент button имеет роль button, даже без явного указания на это. Вот тут можно почитать про роли элементов.

Опции

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

Тут будет много опций для поиска по aria- атрибутам.

🔸hidden: boolean

По умолчанию значение равно false - из-за этого в поиск не включаются "недоступные" элементы (display: none, aria-hidden, role=none).

🔸name: TextMatch

Для элемента формы - это текст лейбла, для кнопки - собственно текст кнопки. Также может использоваться значение атрибута aria-label.

Что за тип такой TextMatch? Это составной тип, который может быть обычной строкой, регулярным выражением или даже функцией.

Функция принимает два аргумента: content: string (собственно текстовый контент, по которому производится поиск) и element (DOMElement) и должна вернуть true или false.


screen.getByText((content, element) => content.startsWith('Hello'))


🔸description: TextMatch

Это для атрибута aria-describedby.

🔸selected: boolean

Отбор по значению атрибута aria-selected.

🔸busy: boolean

Отбор по значению атрибута aria-busy.

🔸checked: boolean

Отбор по значению атрибута aria-checked.

🔸pressed: boolean

Отбор по значению атрибута aria-pressed.

🔸suggest: boolean

Эта настройка для того, чтобы Testing Library предлагала вам запросы получше, чем тот, что написали вы.

🔸current: boolean | string

Отбор по значению атрибута aria-current.

🔸expanded: boolean

Отбор по значению атрибута aria-expanded.

🔸queryFallbacks: boolean

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

🔸level: number

Уровень заголовка для роли heading. Учитывает как семантику тега, так и атрибут aria-level.

🔸value

Это для группы атрибутов aria-value, например, aria-valuemin, aria-valuetext. Указывается в виде объекта:


screen.getByRole('spinbutton', { value: { min: 5, max: 10 }})


#тестирование #testinglibrary #документация
👍2
ByLabelText, ByPlaceholderText

Два запроса, предназначенных в основном для интерактивных элементов форм: поиск по тексту лейбла и по плейсхолдеру.

ByLabelText

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

- через атрибуты for и id
- через атрибут aria-labelledby
- если поле находится внутри label
- лейбл можно указать в атрибуте aria-label

Сигнатура:


screen.getByLabelText(
text: TextMatch,
options: {
selector?: string = '*',
exact?: boolean = true,
normalizer?: NormalizeFn
}
)


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

Вторым параметром идет объект настроект:
- selector - можно дополнительно указать селектор нужного элемента
- exact - если текст задан в виде строки, то этот параметр определяет должен ли поиск быть точным (по полной строке, с учетом регистра символов) или нет
- normalizer - по умолчанию Testing Library нормализует текст (убирает лишние пробелы). Можно передать собственный нормализатор

ByPlaceholderText

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

Сигнатура:


screen.getByPlaceholderText(
text: TextMatch,
options: {
exact?: boolean = true,
normalizer?: NormalizerFn
}
)


#тестирование #testinglibrary #документация #примерыкода
👍2
ByDisplayValue

Еще один запрос для интерактивных элементов, у которых может быть value (input, textarea, select). Соответственно поиск происходит по текущему значению value - это текст, который отображается в инпуте. Настроек у запроса немного:


screen.getByDisplayValue(
value: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
}
)


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

Есть возможность искать элементы по текстовому контенту. Это сработает для всех элементов, у которых есть textContent, а также для инпутов с типом submit или button.

Настройки у запроса стандартные:


getByText(
text: TextMatch,
options?: {
selector?: string = '*',
exact?: boolean = true,
ignore?: string|boolean = 'script, style',
normalizer?: NormalizerFn,
})

Новая для нас настройка - ignore, она указывает, какие селекторы игнорировать при поиске.

#тестирование #testinglibrary #документация #примерыкода
👍1
ByAltText, ByTitle

Еще два запроса для поиска по значению атрибутов:

- alt - в основном для изображений
- title


screen.getByAltText(
text: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
})

screen.getByTitle(
title: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
})


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

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


screen.getByTestId(
text: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
})


#тестирование #testinglibrary #документация #примерыкода
👍1
Родители (parents) и владельцы (owners) в React

Статьи (англ.):
Поток данных https://julesblom.com/writing/parents-owners-data-flow
Производительность рендеринга https://julesblom.com/writing/parents-owners-performance

Статьи очень подробно и с интерактивными примерами рассказывают о двух способах построения дерева рендеринга в React (Parent Hierarchy и Owner Hierarchy). И о том, как понимание разницы между ними позволяет нам избежать prop drilling, а также улучшить производительность рендеринга без использования memo.

Пример


const App = () => {
const [user, setUser] = useState({ name: ‘John’ })

return <div>
<Dashboard user={user} />
</div>
}

const Dashboard = ({ user }) => {
return <div>
<WelcomeMessage user={user} />
</div>
}

const WelcomeMessage = ({ user }) => {
return <div>Hello, {user.name}</div>
}


Компонент App рендерит Dashboard, а тот в свою очередь рендерит WelcomeMessage. В компоненте App определяется стейт user, который используется в WelcomeMessage. Нам приходится пробрасывать его сквозь Dashboard, хотя самому дашборду он совсем не нужен.

Компоненты со слотами

Ту же функциональность можно реализовать по-другому:


const App = () => {
const [user, setUser] = useState({ name: ‘John’ })

return <div>
<Dashboard>
<WelcomeMessage user={user} />
</Dashboard>
</div>
}

const Dashboard = ({ children }) => {
return <div>{children}</div>
}


Тут Dashboard реализован как компонент «со слотом». Он рендерит кусок приложения, не имея представления о том, что в нем происходит. Это инверсия контроля, когда ответственность за этот фрагмент переходит к родителю Dashboard.

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

На этом примере легко понять разницу между родителем (непосредственная обертка) и владельцем (компонент, который контролирует рендеринг). Для WelcomeMessage родителем будет Dashboard, но владеет им App.

Производительность

Важно понимать, что ответственность за перерендеринг компонента извне несет его владелец, а не родитель.

Добавим в компонент Dashboard собственное состояние:


const Dashboard = ({ children }) => {
const [data, setData] = useState(‘hello’)

return <div>
{data}
{children}
</div>
}


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

Живой пример можно посмотреть здесь: https://codesandbox.io/p/sandbox/parents-vs-owners-fjtr35. В консоли видно, какие компоненты обновляются при изменении состояния.

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

Вот плохой пример:


const App = () => {
const [counter, setCounter] = useState(1)
const increase = () => {
setCounter(prev => prev + 1)
}

return <div>
{counter}
<button onClick={increase)>Increase</button>
<VeryExpensiveComponent />
</div>
}


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

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


const App = () => {
return <Counter>
<VeryExpensiveComponent />
</Counter>
}


Не злоупотребляем

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

#паттерны #примерыкода #ссылки
👍5🔥2
Testing Library. Вызов событий. fireEvent

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

Testing Library предоставляет несколько способов эмулировать такие события. Самый простой - метод fireEvent из пакета @testing-library/dom.

Документация нам говорит, что в большинстве случаев мы будем использовать другой пакет - @testing-library/user-event, но пока посмотрим на этот.

Метод fireEvent принимает первым аргументом элемент, на котором необходимо вызвать событие. Вторым - объект события.


fireEvent(input, new MouseEvent('click'))


Также есть несколько готовых методов для конкретных событий, которым можно передать объект с настройками:


fireEvent.change(input, { target: { value: 'hello' } })
fireEvent.keyDown(domNode, {key: 'Enter', code: 'Enter', charCode: 13})


Дока тут: https://testing-library.com/docs/dom-testing-library/api-events

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

Пример: https://codesandbox.io/p/devbox/testing-library-global-jsdom-screen-react-junior-forked-7t9cmt

#тестирование #testinglibrary #документация #примерыкода
👍2
Testing Library. Асинхронщина. waitFor

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

waitFor

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

Мы можем даже использовать expect для проверки условий, так как при несоответствии он как раз выбрасывает ошибку:


await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))


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

Вторым аргументом можно передать объект с настройками:

- container: HTMLElement - по умолчанию document
Если мы ждем появления элемента внутри конкретного контейра, то можно передать его
- timeout - время ожидания
- interval - как часто вызывать коллбэк
- onTimeout: (error: Error) => Error - по умолчанию добавляет к ошибке текущее состояние элемента container
- mutationObsereverOptions - для настройки вызова коллбэка при изменениях контейнера

#тестирование #testinglibrary #документация #примерыкода
👍2👏1
Testing Library. Асинхронщина. findBy

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

Можно использовать в комбинации с await:


await screen.findByText('Clicked once')


#тестирование #testinglibrary #документация #примерыкода
👍1
Testing Library. Асинхронщина. waitForElementToBeRemoved

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

Первым параметром она принимает элемент или массив элементов. Кроме того, можно передать функцию-коллбэк, которая вернет элемент или массив элементов. Если элемент null или массив пустой, будет ошибка.


const el = document.querySelector('div.getOuttaHere')
await waitForElementToBeRemoved(el)


Вторым параметром можно передать объект с настройками, такой же как у функции waitFor.

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