Post

Срез 4

Изучение теории. Неделя 4

Срез 4

1. React. Lifecycle methods (Тема для пересдачи)

1.1. Уровень знаний 2

1.1.1. Основные методы жизненного цикла компонента в React

  1. Монтирование — вызываются, когда экземпляр компонента создается и вставляется в DOM.
    1. constructor() — инициализация состояния и привязка обработчиков событий.
    2. static getDerivedStateFromProps() — используется редко, когда состояние зависит от изменений в пропсах.
    3. componentWillMount (устаревший метод) — вызывается непосредственно перед тем, как компонент отрисуется в DOM.
    4. render() — единственный обязательный метод в классовом компоненте. Описывает, как выглядит UI.
    5. componentDidMount() — вызывается сразу после вставки компонента в DOM. Идеальное место для сетевых запросов или подписок.
  2. Обновление — вызываются при изменении пропсов или состояния.
    1. static getDerivedStateFromProps()
    2. shouldComponentUpdate() — позволяет пропустить рендеринг для оптимизации (возвращает true или false).
    3. render() — перерисовывает компонент.
    4. getSnapshotBeforeUpdate() — позволяет захватить информацию из DOM (например, позицию прокрутки) перед тем, как она будет изменена.
    5. componentDidUpdate() — вызывается сразу после обновления. Здесь можно работать с DOM или делать сетевые запросы на основе сравнения старых и новых пропсов.
  3. Размонтирование — вызывается, когда компонент удаляется из DOM.
    1. componentWillUnmount() — используется для «очистки»: отмены сетевых запросов, удаления таймеров или отписки от событий.

1.1.2. Использование componentDidMount для инициализации компонента

Метод componentDidMount() используется для выполнения действий, которые требуют наличия компонента в DOM. Это «точка входа» для любой настройки, которую нельзя сделать до отрисовки.

Основные задачи метода:

  • Сетевые запросы — загрузка данных с API сразу после появления компонента.
  • Подписки — установка слушателей событий (window.addEventListener).
  • Работа с DOM — измерение размеров элементов, установка фокуса в поле ввода или инициализация сторонних библиотек.
  • Таймеры — запуск setInterval или setTimeout.

1.1.3. Отличия между componentWillMount и componentDidMount

  • componentWillMount
    • Вызывается ДО рендеринга. Компонента еще нет в DOM.
    • Нельзя обращаться к элементам DOM (их еще нет), попытка вызовет ошибку.
    • Плохое место для запросов API. Запрос может начаться, но компонент может не успеть отрендериться, что приведет к утечкам памяти или пустым обновлениям.
    • Устарел. В современном коде его использовать нельзя, так как он мешает асинхронному рендерингу.
  • componentDidMount
    • Вызывается ПОСЛЕ рендеринга. Компонент уже вставлен в дерево DOM.
    • Можно обращаться к элементам DOM.
    • Идеальное место для запросов API. Гарантирует, что данные придут, когда компонент готов их отобразить.
    • Актуальный и основной метод для инициализации.

1.1.4. Влияние componentDidUpdate на компоненты

Метод componentDidUpdate() — это основной инструмент для реакции на изменения в уже отрисованном компоненте. Его главное влияние заключается в возможности синхронизировать внутреннее состояние или DOM с новыми данными.

Ключевые аспекты влияния:

  • Реакция на изменения — позволяет сравнивать старые пропсы и состояние с новыми (prevProps и prevState) и выполнять действия только при совпадении условий.
  • Работа с DOM — идеальное место для обновления сторонних библиотек, которым нужно знать о новых размерах или данных после перерисовки.
  • Риск бесконечного цикла — если вызвать this.setState() внутри этого метода без условия, компонент будет обновляться вечно, так как каждое обновление снова вызывает componentDidUpdate.

1.1.5. Оптимальный момент для выполнения сетевых запросов

  1. Первичная загрузка данныхcomponentDidMount
    • Гарантия наличия DOM — компонент уже готов отобразить состояние загрузки или полученные данные.
    • Безопасность — в отличие от методов, работающих до рендеринга (как constructor или устаревший componentWillMount), выполнение запроса здесь не блокирует отрисовку интерфейса и не вызывает ошибок при обновлении состояния.
    • Единственный вызов — срабатывает ровно один раз при создании компонента, что предотвращает дублирование тяжелых запросов.
  2. Обновление данныхcomponentDidUpdate
    • Используется для запросов, которые зависят от пропсов.
    • Нужно обязательно использовать проверку, иначе возникнет бесконечный цикл запросов.

1.2. Уровень знаний 3

1.2.1. Порядок выполнения методов жизненного цикла

Совпадает с 1.1.1. Основные методы жизненного цикла компонента в React

1.2.2. Роль и работа метода shouldComponentUpdate

Это метод-оптимизатор. Он позволяет вручную управлять процессом обновления компонента, отвечая на вопрос: «Нужно ли повторно рендерить компонент при изменении пропсов или стейта?»

Работа:

  1. Вызывается в фазе обновления прямо перед методом render()
  2. Принимает два аргумента: nextProps и nextState
  3. Возвращает boolean:
    • true (по умолчанию) — React запускает стандартный процесс рендеринга и сверки (diffing).
    • false — React полностью пропускает render(), а также методы componentDidUpdate() и жизненный цикл всех дочерних компонентов для этого обновления.

Роль:

  • Оптимизация — если вы точно знаете, что изменение конкретных данных не должно менять визуальную часть.
  • Сравнение данных — внутри метода обычно делают поверхностное сравнение текущих и новых данных.
  • Не для логики — в нем нельзя вызывать this.setState() — это приведет к бесконечному циклу.

1.2.3. Аргументы метода componentDidUpdate и их использование

Метод componentDidUpdate вызывается сразу после того, как обновление было зафиксировано в DOM. Он принимает три аргумента, которые позволяют сравнить «что было» с тем, «что стало».

Список аргументов:

  1. prevProps — предыдущие пропсы.
  2. prevState — предыдущее состояние.
  3. snapshot — данные, переданные из метода getSnapshotBeforeUpdate (если он реализован). Если нет — будет undefined.

Как используется:

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

1.2.4. Различие между getDerivedStateFromProps и componentWillReceiveProps

  • getDerivedStateFromProps()
    • Актуален.
    • Статический метод (this не доступен).
    • Вызывается и при монтировании, и при обновлении.
    • Чистая функция. Возвращает объект для обновления стейта или null.
    • Детерминирован и предсказуем.
  • componentWillReceiveProps()
    • Устарел.
    • Метод экземпляра (this доступен).
    • Вызывается только при обновлении (новые пропсы).
    • Разрешает this.setState и запросы.
    • Может вызываться несколько раз до рендера (небезопасно).

1.2.5. Применение getSnapshotBeforeUpdate для управления состоянием

getSnapshotBeforeUpdate нельзя использовать для управления состоянием.

Его единственная роль — захватить (прочитать) информацию из DOM непосредственно перед тем, как React применит изменения и обновит экран.

Как он работает:

  • Вызывается после метода render, но до того, как изменения зафиксированы в реальном DOM.
  • Он имеет доступ к старому DOM.
  • То, что метод возвращает (число, объект, строку или null), передается как третий аргумент (snapshot) в componentDidUpdate.
  • Этот метод должен быть чистой функцией. В нем запрещено вызывать this.setState().

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

1.3. Уровень знаний 4

Оптимизация рендеринга через методы жизненного цикла Отписка и очистка ресурсов в componentWillUnmount Влияние методов жизненного цикла на производительность Сценарии использования componentWillUnmount и их влияние на производительность

1.3.1. Значение порядка методов жизненного цикла для оптимизации

Значение порядка — чем выше по цепочке (в начале порядка) вы отсекаете изменения, тем меньше работы делает процессор.

  • shouldComponentUpdate(nextProps, nextState) — самый мощный инструмент. Позволяет пропустить render(), а также методы componentDidUpdate() и жизненный цикл всех дочерних компонентов для текущего обновления, если пропсы или стейт не изменились. Это экономит ресурсы на сравнении Virtual DOM.

1.3.2. Оптимизация рендеринга через методы жизненного цикла

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

Основные точки оптимизации:

  • shouldComponentUpdate(nextProps, nextState) — самый мощный инструмент. Позволяет пропустить render(), а также методы componentDidUpdate() и жизненный цикл всех дочерних компонентов для текущего обновления, если пропсы или стейт не изменились. Это экономит ресурсы на сравнении Virtual DOM.
  • getSnapshotBeforeUpdate — позволяет захватить данные из DOM прямо перед фиксацией изменений. Оптимизация здесь исключает «прыжки» контента и лишние перерисовки, которые могли бы возникнуть, если бы вы делали это позже в componentDidUpdate.
  • componentDidMount — место для запуска асинхронных операций (API-запросы). Оптимизация заключается в том, чтобы не блокировать начальный рендеринг, давая пользователю увидеть интерфейс как можно быстрее.

1.3.3. Отписка и очистка ресурсов в componentWillUnmount

Метод componentWillUnmount() вызывается перед удалением компонента из DOM и предназначен для очистки ресурсов:

  • отмена сетевых запросов,
  • удаление таймеров,
  • отписка от событий.

Невыполнение очистки может привести к утечкам памяти. Это критически важный метод для стабильности приложения.

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

Влияние:

  1. Предотвращение лишних рендеров
    • shouldComponentUpdate — позволяет вручную запретить рендер, если пропсы или состояние не изменились.
  2. Управление побочными эффектами — неправильное использование методов может привести к зависанию интерфейса.
    • componentDidMount — идеален для запросов к API. Выполнение тяжелых операций здесь гарантирует, что они произойдут один раз после появления компонента на экране.
  3. Утечки памяти
    • componentWillUnmount — обязателен для удаления таймеров, обработчиков событий или отмены сетевых запросов.

1.3.5. Сценарии использования componentWillUnmount и их влияние на производительность

Метод componentWillUnmount позволяет предотвратить утечки памяти.

Основные сценарии использования:

  • Очистка таймеров — удаление setTimeout или setInterval, чтобы код не продолжал выполняться после исчезновения компонента.
  • Отписка от событий — удаление слушателей событий.
  • Отмена сетевых запросов — остановка активных API-вызовов.
  • Разрыв соединений — закрытие WebSocket-соединений или Firebase-подписок.
  • Удаление сторонних библиотек — вызов методов уничтожения (destroy/cleanup) для графиков (D3, Chart.js) или карт (Leaflet, Google Maps), инициализированных в компоненте.

1.4. Вопросы

1.4.1. Какие основные этапы жизненного цикла у классового компонента React (монтирование, обновление, размонтирование) и какие методы обычно к ним относятся?

1.1.1. Основные методы жизненного цикла компонента в React

1.4.2. В каких случаях срабатывает componentDidUpdate, какие аргументы он получает и как их использовать, чтобы корректно реагировать на изменения props или state и не попасть в бесконечный цикл обновлений?

1.2.3. Аргументы метода componentDidUpdate и их использование

1.4.3. Объясните назначение getSnapshotBeforeUpdate: когда он вызывается, что должен возвращать, как его результат попадает в componentDidUpdate и в каких практических сценариях это помогает (например, сохранение позиции прокрутки) без ухудшения производительности?

1.2.5. Применение getSnapshotBeforeUpdate для управления состоянием

1.4.4. Зачем нужен shouldComponentUpdate, какие данные он получает и как его решение может повлиять на производительность и поведение компонента?

1.2.2. Роль и работа метода shouldComponentUpdate

2. Classes

2.1. Уровень знаний 2

2.1.1. Определение и объявление классов в TypeScript

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

2.1.2. Создание и использование статических членов класса

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

2.1.3. Модификаторы доступа: private, protected и public

TypeScript предоставляет модификаторы доступа для контроля видимости членов класса.

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

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

2.1.4. Наследование и расширение классов

  • Наследование позволяет одному классу (потомку) перенимать свойства и методы другого класса (родителя). Это достигается с помощью ключевого слова extends.
  • Класс-потомок может добавлять новые члены или переопределять унаследованные методы для изменения их поведения.
  • Конструктор класса-потомка должен вызывать конструктор родителя с помощью super(), чтобы правильно инициализировать унаследованные свойства.
  • Наследование способствует повторному использованию кода и устанавливает иерархические отношения между классами.

2.2. Уровень знаний 3

2.2.1. Обобщенные классы в TypeScript

  • Обобщенные классы (Generics) позволяют создавать компоненты, способные работать с различными типами данных, а не с каким-то одним. Это достигается путем указания параметра типа в угловых скобках <T> при объявлении класса.
  • Внутри класса этот параметр типа можно использовать для аннотации полей, аргументов методов и возвращаемых значений.
  • Обобщенные классы повышают гибкость и возможность повторного использования кода, позволяя создавать универсальные и типобезопасные структуры данных.

2.2.2. Ограничения типов в обобщенных классах

  • Ограничения типов позволяют сужать набор типов, которые можно использовать в качестве аргумента для параметра обобщения. Это делается с помощью ключевого слова extends после параметра типа, например, <T extends SomeType>.
  • Таким образом, мы гарантируем, что переданный тип будет иметь как минимум те свойства или методы, которые определены в SomeType. Это позволяет использовать внутри обобщенного класса специфическую функциональность ограничивающего типа, сохраняя при этом полиморфизм и безопасность типов.

2.3. Уровень знаний 4

2.3.1. Классы, объявленные через выражения

  • Выражения класса — это еще один способ определения класса, где класс становится значением, которое можно передавать в функции, возвращать из них или присваивать переменным (по аналогии с function expression).
  • Синтаксис похож на объявление класса, но имя класса может быть опущено, создавая анонимный класс. Если имя указано, оно видно только внутри тела самого класса, что полезно для рекурсивных вызовов.
  • Класс, объявленный через выражение, используется так же, как и класс, объявленный через объявление, но его область видимости ограничена блоком кода, в котором находится выражение.

2.3.2. Отличия между классами, объявленными через выражения и через объявления

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

2.3.3. Взаимодействие и отношения между классами в TypeScript

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

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

2.4. Вопросы

3. Module

3.1. Уровень знаний 2

3.1.1. Основы модулей в TypeScript

  • Модули в TypeScript позволяют организовывать код в отдельные файлы, что облегчает его поддержку и повторное использование.
  • Каждый файл, содержащий импорт или экспорт на верхнем уровне, считается модулем. Ключевые слова import и export используются для определения зависимостей между модулями.
  • Эта система помогает инкапсулировать код и избегать загрязнения глобальной области видимости.
  • TypeScript поддерживает различные форматы модулей, такие как ES6, CommonJS, AMD и другие.

3.1.2. Синтаксис CommonJS для импорта и экспорта

  • Синтаксис CommonJS широко используется в среде Node.js для работы с модулями.
  • Для экспорта значений применяется module.exports или exports (module.exports = myFunction).
  • Для импорта применяется функция require() (const myModule = require('./myModule')).
  • Этот синтаксис является синхронным и идеально подходит для серверной разработки.
  • TypeScript может компилировать модули в формат CommonJS для совместимости с Node.js.

3.1.3. Цели и преимущества использования модулей

  • Основная цель модулей — организация кода в логические блоки, что улучшает его структуру и читаемость.
  • Модули способствуют инкапсуляции, позволяя скрывать внутренние детали реализации и показывать только необходимый интерфейс.
  • Они облегчают повторное использование кода в разных частях приложения или в различных проектах.
  • Использование модулей упрощает тестирование и отладку, так как каждая часть функциональности изолирована.
  • Кроме того, модули помогают управлять зависимостями и избегать конфликтов имен.

3.1.4. Примеры экспорта функций в TypeScript

В TypeScript можно экспортировать функции несколькими способами.

  • Используя ключевое слово export перед объявлением функции: export function greet() { }.
  • Также можно экспортировать функцию по умолчанию с помощью export default function() { }.
  • Кроме того, функции можно объявлять, а затем экспортировать отдельно через export { greet }.

3.1.5. Определение и использование модулей

  • Модуль определяется как файл с кодом, который использует экспорт или импорт.
  • Для использования модуля необходимо импортировать его в другой файл с помощью import (import { MyClass } from './MyModule').
  • Модули могут экспортировать переменные, функции, классы или интерфейсы.
  • Модули выполняются в своей собственной области видимости, а не в глобальной. Это предотвращает конфликты имен.

3.2. Уровень знаний 3

3.2.1. Опции разрешения модулей в TypeScript

TypeScript предлагает две основные стратегии разрешения модулей: 'classic' и 'node'.

  • Стратегия 'classic' является устаревшей и в основном используется для обратной совместимости.
  • Стратегия 'node' имитирует механизм разрешения модулей Node.js, что является современным стандартом.

Выбор стратегии влияет на то, как компилятор ищет модули на основе их путей импорта. Эта настройка определяется в файле tsconfig.json в опции moduleResolution.

3.2.2. Настройка TypeScript для генерации различных форматов модулей

  • Формат генерируемых JavaScript модулей настраивается в tsconfig.json с помощью опции module.
  • Доступные значения включают 'CommonJS', 'AMD', 'System', 'UMD', 'ES6', 'ES2015', 'ESNext'.
  • Выбор формата зависит от среды выполнения, например, 'CommonJS' для Node.js и 'ES6’ для современных браузеров.

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

3.2.3. Сравнение опций ‘node’ и ‘classic’ в разрешении модулей

  • Опция 'node' разрешает модули точно так же, как это делает Node.js, проверяя node_modules и используя информацию из package.json. Она также учитывает расширения файлов, такие как .ts, .tsx, .d.ts.
  • Опция 'classic' является устаревшей и использует более простой алгоритм, который не учитывает node_modules так же глубоко.

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

3.2.4. Настройка выходных опций модулей

  • Выходные опции модулей настраиваются в первую очередь через опцию модуля в tsconfig.json. Эта опция контролирует, в какой формат модулей будет компилироваться TypeScript код, например, 'ES2015' для нативных ES модулей или 'CommonJS' для Node.js.
  • Дополнительные настройки, такие как outDir, определяют целевую директорию для скомпилированных файлов.
  • Для сборки проектов, предназначенных для разных сред, можно использовать инструменты вроде Webpack или Rollup вместе с TypeScript для дальнейшей оптимизации выходного формата модулей.

3.2.5. Разница между опциями ‘module’ и ‘target’

  • Опция module определяет формат модулей в сгенерированном JavaScript коде (CommonJS, ES6).
  • Опция target определяет версию JavaScript, в которую компилируется TypeScript код (ES5, ES2016).
  • Они работают независимо: можно компилировать в ES5 (target: "es5") с использованием модулей ES6 (module: "es2015"), но для этого потребуется bundler для преобразования модулей в формат, понятный браузерам.
  • target влияет на синтаксис функций, стрелочных функций и других конструкций, в то время как module влияет только на импорт/экспорт.

3.3. Уровень знаний 4

3.3.1. Процесс разрешения модулей при использовании опции ‘node’

При стратегии ‘node’ компилятор TypeScript имитирует процесс разрешения модулей Node.js. Для импорта вида import * as foo from “foo” компилятор сначала ищет файл foo в ближайшей node_modules directory. Он проверяет наличие файла package.json и его поля main для определения точки входа. Если модуль не найден, компилятор ищет файлы с расширениями .ts, .tsx, .d.ts в соответствии с настройками. Этот процесс обеспечивает полную совместимость с npm пакетами и экосистемой Node.js.

3.3.2. Обработка путей импорта с опцией ‘baseUrl’

Опция baseUrl в tsconfig.json позволяет задать базовый каталог для разрешения не-относительных путей импорта. Когда baseUrl установлена, например, в ./src, импорт вида import { } from “modules/myModule” будет искать файл в ./src/modules/myModule. Это избавляет от необходимости использовать длинные относительные пути вроде ../../../myModule. Данная опция особенно полезна в больших проектах со сложной структурой директорий, так как упрощает навигацию и делает импорты более читаемыми.

3.3.3. Настройка псевдонимов модулей в TypeScript

Псевдонимы модулей настраиваются с помощью опции paths в tsconfig.json в сочетании с baseUrl. Эта конфигурация позволяет сопоставлять шаблоны путей с фактическими путями к файлам. Например, “@/”: [“src/”] позволяет использовать импорт import … from “@/components/MyComponent”, который будет разрешен в src/components/MyComponent. Псевдонимы значительно улучшают читаемость кода и удобство рефакторинга, избавляя от запутанных относительных путей. Для работы в runtime часто требуется дополнительная настройка bundler’а.

3.3.4. Использование и настройка опции ‘paths’

Опция paths предоставляет возможность создания пользовательских маппингов для импортов модулей. Она определяется в tsconfig.json как объект, где ключи — это шаблоны путей (например, “@app/”), а значения — массивы путей для поиска (например, [”./src/app/”]). Это позволяет создавать понятные и короткие алиасы для длинных или сложных путей. Важно отметить, что paths работает только на уровне TypeScript и для его работы в скомпилированном JavaScript может потребоваться настройка сборщика (Webpack, Rollup) или загрузчика модулей.

3.3.5. Оптимизация размера и производительности модулей

Для оптимизации размера модулей используется tree shaking — процесс удаления неиспользуемого кода, который поддерживают современные сборщики как Webpack и Rollup. Важно использовать синтаксис ES модулей и названные exports для возможности эффективного анализа кода. Разделение кода (code splitting) позволяет разбивать приложение на chunks, которые загружаются по требованию. Выбор современного формата модулей (ES) и target среды помогает избежать ненужных полифиллов. Минимизация и сжатие кода на этапе сборки также важны для производительности.

3.4. Вопросы

4. Strict Mode

4.1. Уровень знаний 2

4.1.1. Основы строгого режима в TypeScript

Строгий режим в TypeScript — это набор правил и проверок, которые усиливают статический анализ кода. Он активируется группой флагов компиляции, которые делают систему типов более строгой и надежной. Основная цель этого режима — выявление потенциальных ошибок на этапе компиляции, а не во время выполнения. Это включает в себя более тщательную проверку типов, запрет на неявные преобразования и контроль за возможными значениями null и undefined. Использование строгого режима значительно повышает качество и надежность кода.

4.1.2. Включение строгого режима в проекте

Чтобы включить строгий режим для всего проекта, необходимо в файле tsconfig.json установить опцию “strict”: true. Это автоматически активирует все основные флаги строгости. Альтернативно можно точечно включать отдельные строгие настройки, такие как strictNullChecks или noImplicitAny, установив их в true. Включение строгого режима рекомендуется для всех новых проектов, так как он помогает выявлять ошибки на ранних стадиях разработки. Эта настройка применяется ко всем файлам проекта.

4.1.3. Цели использования строгого режима

Главная цель строгого режима — повышение надежности и предсказуемости кода за счет раннего обнаружения ошибок. Он предотвращает распространенные pitfalls, такие как обращение к свойствам неопределенных объектов или использование переменных с неожиданным значением null. Режим способствует написанию более явного и самодокументирующегося кода, так как требует четкого указания типов. Это особенно важно в больших проектах и командах, где строгая дисциплина типов уменьшает количество runtime-ошибок и упрощает рефакторинг.

4.2. Уровень знаний 3

4.2.1. Преимущества строгого режима в разработке

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

4.2.2. Флаги строгого режима и их настройка

Опция “strict” является мета-флагом, который включает сразу группу наиболее важных настроек: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict. Каждый из этих флагов можно настраивать отдельно, отключая или включая в tsconfig.json для тонкого контроля над уровнем строгости. Это полезно для постепенного внедрения строгого режима в существующий проект или для случаев, когда требования к определенным проверкам отличаются.

4.3. Уровень знаний 4

4.3.1. Особенности работы со строгим режимом для типов null и undefined

При включенном strictNullChecks типы null и undefined не присваиваются автоматически другим типам. Это означает, что переменная типа string не может быть null или undefined, если это явно не разрешено с помощью union-типа (например, stringnull). Это заставляет разработчика явно обрабатывать возможные случаи отсутствия значений, что предотвращает классические ошибки вроде Cannot read property ‘X’ of null. Такой подход делает работу с потенциально отсутствующими данными более осознанной и безопасной.

4.3.2. Отличия между строгим и обычным режимами при работе с классами и интерфейсами

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

4.3.3. Настройка строгого режима для отдельного файла

Глобальные настройки из tsconfig.json применяются ко всем файлам проекта. Однако для отдельного файла можно частично ослабить строгость, используя комментарии // @ts-ignore для подавления конкретных ошибок или // @ts-nocheck для отключения проверок во всем файле. Важно отметить, что невозможно включить строгий режим для одного файла, если он выключен в конфигурации проекта. Для точечного повышения строгости рекомендуется использовать глобальную настройку, а затем при необходимости ослаблять ее в конкретных местах с помощью комментариев.

4.4. Вопросы

5. Union Types & Intersection

5.1. Уровень знаний 2

5.1.1. Определение и использование Union Types в TypeScript

Union Types (объединенные типы) позволяют переменной, параметру или свойству объекта принадлежать к одному из нескольких типов. Синтаксис объявления использует вертикальную черту: type MyType = stringnumber. Это особенно полезно для работы со значениями, которые могут принимать разные формы в ходе выполнения программы. Компилятор TypeScript будет разрешать только те операции, которые являются общими для всех типов в объединении, обеспечивая типобезопасность. Для работы с конкретным типом требуется сужение типа (type narrowing).

5.1.2. Объявление переменных с Union Types

Переменные объявляются с union type путем указания типов через оператор. Например: let id: stringnumber;. Такая переменная может хранить значение любого из указанных типов. При присвоении значения компилятор проверяет соответствие одному из типов объединения. Это позволяет гибко работать с данными, источник которых может быть разнородным. Для выполнения операций, специфичных для конкретного типа, необходимо использовать проверки типов или другие методы сужения.

5.1.3. Примеры использования Union Types с литеральными типами

Union Types часто сочетают с литеральными типами для создания перечислений допустимых значений. Пример: type Direction = ‘up’‘down’‘left’‘right’;. Переменная этого типа может принимать только одно из четырех строковых значений. Это эффективная замена традиционным enum для простых случаев, обеспечивающая лучшую типобезопасность и читаемость кода. Такой подход часто используется для описания статусов, флагов или предопределенных вариантов выбора.

5.2. Уровень знаний 3

5.2.1. Оператор распределения и Union Types

TypeScript применяет распределение (distribution) для условных типов при работе с union types. Например, в конструкции T extends U ? X : Y где T - union type, условие применяется к каждому члену объединения по отдельности. Это мощный механизм для преобразования типов, позволяющий создавать сложные type utilities. Распределение работает только с типами-параметрами в условных типах и является ключевой особенностью продвинутой системы типов TypeScript.

5.2.2. Условные операторы и определение типов в контексте Union Types

Условные типы (conditional types) часто используются вместе с union types для создания сложных преобразований. Синтаксис T extends U ? X : Y позволяет проверять принадлежность типа и возвращать различные типы в зависимости от условия. В сочетании с infer это позволяет извлекать информацию о типах из объединений. Это особенно полезно при создании универсальных утилитных типов и шаблонов для работы с разнородными данными.

5.2.3. Использование Union Types в функциях для определения параметров

В функциях union types позволяют принимать параметры разных типов: function processInput(input: stringnumber). Внутри функции необходимо сужать тип параметра с помощью проверок typeof, instanceof или пользовательских type guards. Это обеспечивает безопасное выполнение операций, специфичных для каждого типа. Такой подход часто используется в API, где функция должна обрабатывать различные форматы входных данных, сохраняя типобезопасность.

5.3. Уровень знаний 4

5.3.1. Основы и примеры использования Intersection Types

Intersection Types (пересеченные типы) объединяют несколько типов в один с помощью оператора &: type Person = Name & Address & Age. Результирующий тип имеет все свойства каждого из составляющих типов. Это полезно для комбинации объектов или создания типов на основе существующих. Intersection types часто используются с интерфейсами для создания сложных составных типов, которые наследуют все характеристики своих компонентов.

5.3.2. Создание сложных типов с помощью Intersection Types

Intersection types позволяют комбинировать несколько типов в один сложный тип. Например: type Employee = Person & Job & Department. Такой тип будет содержать все свойства из всех указанных типов. Это эффективный способ создания комплексных типов без необходимости наследования или дублирования свойств. Intersection особенно полезен для миксинов (mixins) и композиции объектов, где нужно объединить функциональность из разных источников.

5.3.3. Различие между Union и Intersection Types в функциональности объектов

Ключевое различие: union type (AB) означает, что значение принадлежит либо типу A, либо типу B, тогда как intersection type (A & B) означает, что значение должно удовлетворять требованиям обоих типов одновременно. Для объектов union type разрешает доступ только к общим свойствам, а intersection type требует наличия всех свойств из всех типов. Union представляет выбор между типами, а intersection - комбинацию типов, что фундаментально влияет на способ работы с значениями этих типов.

6. Generics

6.1. Уровень знаний 2

6.1.1. Основы дженериков в TypeScript

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

6.1.2. Объявление обобщённых типов и функций

Обобщённые функции объявляются с параметром типа в угловых скобках: function identity(arg: T): T { return arg; }. Параметр T выступает в роли заполнителя для конкретного типа, который будет указан при вызове функции. Можно явно задавать тип при вызове: identity("hello"), или позволить TypeScript вывести тип автоматически: identity("hello"). Это обеспечивает гибкость при сохранении строгой проверки типов.

6.1.3. Работа с переменными обобщённого типа

Переменные обобщённого типа позволяют использовать один и тот же код с различными типами данных. Внутри обобщённой функции или класса можно работать с параметром типа как с обычным типом: объявлять переменные, указывать типы параметров и возвращаемых значений. TypeScript будет следить за соблюдением типовой безопасности на протяжении всей операции. Это исключает необходимость дублирования кода для разных типов или использования опасного типа any.

6.2. Уровень знаний 3

6.2.1. Создание и использование обобщённых классов

Обобщённые классы объявляются с параметрами типа после имени класса: class Container { value: T; }. При создании экземпляра указывается конкретный тип: let numberContainer = new Container();. Это позволяет создавать универсальные структуры данных, такие как очереди, стеки или списки, которые остаются типобезопасными для любого типа данных. Свойства и методы класса могут использовать параметр типа T в своих аннотациях.

6.2.2. Применение параметров типа в обобщённых ограничениях

Ограничения дженериков позволяют сужать набор допустимых типов с помощью ключевого слова extends. Например: function logLength<T extends { length: number }>(arg: T). Это гарантирует, что переданный аргумент будет иметь свойство length типа number. Ограничения обеспечивают возможность использования специфических членов типа внутри обобщённого кода, сохраняя при этом гибкость и безопасность типов.

6.3. Уровень знаний 4

6.3.1. Использование классов в обобщениях

В качестве параметров типа можно использовать не только примитивы, но и классы. Это позволяет создавать обобщённые компоненты, которые работают с экземплярами конкретных классов: function createInstance(ctor: new() => T): T { return new ctor(); }. Такой подход часто применяется в фабричных функциях и паттернах проектирования, где требуется создавать объекты различных типов с одинаковой сигнатурой конструктора.

6.3.2. Работа с обобщёнными типами в классах и интерфейсах

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

6.3.3. Различия между обобщёнными типами и типом any

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

7. Utility types

7.1. Вопросы

  • Как бы вы устно описали, как работает условный тип (conditional type) и почему он важен для создания продвинутых utility types вроде Exclude/Extract? Приведите пример сценария из реального проекта (без кода), где это помогает избежать ошибок
    • Условный тип: Это «тернарный оператор» для типов (если T соответствует U, то тип X, иначе Y). Он критичен для Exclude/Extract, так как позволяет фильтровать объединения. Пример из жизни: разделение успешного и ошибочного ответа API, чтобы типизация не позволила обратиться к полям ошибки в успешном сценарии.
  • Объясните разницу между Pick и Omit: в каких ситуациях вы выберете один или другой, и какие риски для поддержки типов могут возникнуть при изменении исходного типа?
    • Pick vs Omit: Pick оставляет указанные ключи, Omit удаляет их. Pick лучше для явного разрешения полей (whitelist), Omit — для исключения лишнего (blacklist). Риск поддержки: при удалении ключа из источника Pick выдаст ошибку, а Omit может silently перестать исключать поле (если оно исчезло, исключать нечего).
  • Что такое utility types в TypeScript и зачем они нужны? Назовите 2–3 примера (например, Partial, Pick, Omit) и кратко объясните, что каждый делает.
    • Utility types (общее): Встроенные дженерики для стандартных трансформаций типов. Нужны для избегания дублирования кода. Примеры: Partial (делает все поля опциональными), Pick (оставляет только выбранные ключи), Omit (убирает выбранные ключи).
  • Что такое utility types в TypeScript и зачем они нужны в реальном проекте (например, в React), если можно описывать типы вручную?
    • Utility types (практика): Они гарантируют синхронизацию с исходным типом. Ручное описание ведет к рассинхронизации и дублированию при рефакторинге. В React: удобно адаптировать пропсы (например, сделать ID опциональным для создания записи), не копируя интерфейс компонента вручную.
  • Как бы вы устно описали, чем отличаются Pick/Omit от Exclude/Extract, и в каких ситуациях вы выберете каждый из них (например, для типов пропсов, union-типов и публичных контрактов библиотек)?
    • Pick/Omit vs Exclude/Extract: Pick/Omit манипулируют ключами объектов, Exclude/Extract — элементами union-типов. Pick/Omit используют для пропсов и публичных контрактов библиотек, Exclude/Extract — для фильтрации наборов значений (статусы, типы экшенов).
  • Объясните разницу между Partial, Required и Readonly: что именно они меняют в типе и какие риски/побочные эффекты могут появиться при их использовании в API компонентов?
    • Partial, Required, Readonly: Partial делает поля опциональными, Required — обязательными, Readonly запрещает перезапись. Риски: Partial может скрыть ошибку отсутствия данных, Required заставит передавать лишнее, Readonly сломает логику, требующую мутаций объектов.

7.1.1. Utility types

  • Required противоположен Partial и делает все свойства обязательными, удаляя модификаторы optional (?). Этот тип полезен, когда необходимо гарантировать наличие всех свойств объекта, даже если в исходном типе они были необязательными. Часто используется для валидации данных или преобразования частичных объектов в полные.
  • Readonly создает тип, все свойства которого становятся доступными только для чтения. Это обеспечивает иммутабельность объектов на уровне типизации, предотвращая случайные изменения свойств после создания объекта. Особенно полезен для работы с константными данными или защитой от непреднамеренных мутаций.
  • Record создает тип объекта с заданными ключами и единым типом значений. Это мощный инструмент для создания словарей или карт с однородными значениями. Позволяет строго типизировать объекты с динамическими ключами, но предсказуемым типом значений.
  • Pick позволяет выбрать из существующего типа только указанные свойства. Это полезно для создания более специфичных типов на основе существующих, уменьшая избыточность кода. Часто используется для создания типов представлений или DTO, содержащих подмножество свойств.
  • Omit противоположен Pick и создает тип, исключая указанные свойства. Полезен для создания типов, которые должны исключать определенные поля, например, при создании объектов для создания новых сущностей без идентификаторов. Упрощает работу с типами, убирая ненужные свойства.

7.1.2. Работа с подмножествами типов и комбинирование их (Exclude, Extract)

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

8. Type Guards

  • Как работают пользовательские type guards с предикатом вида “param is Type” и как они влияют на сужение типов в разных ветках (if/else, switch, тернарный оператор)? Объясните, какие ошибки проектирования могут привести к небезопасному сужению и как это распознать на уровне типов.
    • Предикаты и сужение: Сигнатура arg is Type вручную указывает компилятору сужать тип в ветке if. Ошибка проектирования: если runtime-проверка внутри функции слабая, TS всё равно поверит сигнатуре, что приведет к unsafe-доступу в коде.
  • Объясните разницу между проверкой типа через оператор “in”, через “typeof” и через “instanceof” как type guards: в каких случаях каждый подход корректен и какие у них ограничения?
    • in vs typeof vs instanceof: typeof для примитивов (string, number), instanceof для классов (не работает с литералами), in для проверки наличия ключа в объекте. Ограничения: typeof не различит объекты, instanceof может не сработать across realms (iframe).
  • Что такое type guard в TypeScript и какую задачу он решает при работе с union-типами? Приведите устный пример ситуации, когда без type guard нельзя безопасно обратиться к свойству.
    • Суть type guard: Убирает неоднозначность union-типа. Пример: в union CatDog нельзя вызвать .meow(), пока гард не подтвердит, что объект именно Cat.
  • Объясните, как type guards взаимодействуют с conditional types и утилитными типами вроде Extract/Exclude при проектировании API: как вы мысленно проверяете, что после проверки в рантайме компилятор корректно сузит тип и не появятся “дыры” в типобезопасности? Приведите устный пример сценария и возможной ошибки дизайна.
    • Гарды и условные типы: Гарды работают в runtime, условные типы — в compile-time. Они должны зеркалить друг друга. Ошибка дизайна: рассинхронизация (гард проверяет одно, тип сужается по другому), создающая «дыры» в типобезопасности.
  • Чем отличаются встроенные проверки (например, typeof/instanceof/оператор in) от пользовательского type predicate (функции с возвращаемым типом вида “x is T”)? В каких ситуациях вы выберете одно вместо другого?
    • Встроенные vs Кастомные: Встроенные проще и надежнее для базовых проверок. Кастомные предикаты (x is T) выбираю для сложной валидации (структура API, глубокая проверка), где операторов JS недостаточно.
  • Что такое type guard в TypeScript и зачем он нужен при работе с union-типами? Объясните, как он помогает сузить тип во время выполнения без написания кода.
    • Зачем нужен гард: Позволяет TS автоматически определить конкретный тип внутри блока кода без опасных приведений (as). Проверка происходит в runtime, что гарантирует безопасность доступа к свойствам.

9. Configuration

9.1. Уровень знаний 2

9.1.1. Принципы сужения типов в TypeScript

Сужение типов (type narrowing) — это процесс, при котором TypeScript определяет более конкретный тип значения на основе структуры кода. Это происходит через проверки условий, которые позволяют компилятору делать выводы о типах внутри определенных блоков кода. Основные методы включают проверки с помощью typeof, instanceof, операторов сравнения и пользовательских type guards. Сужение типов повышает безопасность кода, позволяя использовать операции, специфичные для конкретного типа, только после соответствующих проверок.

9.1.2. Использование оператора ‘instanceof’ для сужения типов

Оператор instanceof проверяет, является ли объект экземпляром определенного класса. Это эффективно для сужения типов при работе с иерархиями наследования. Например, if (error instanceof NetworkError) позволяет компилятору знать, что внутри блока error имеет тип NetworkError. Это дает доступ к специфическим свойствам и методам данного класса. Такой подход особенно полезен для обработки ошибок и работы с полиморфными объектами.

9.1.3. Техники сужения типов с помощью оператора ‘in’

Оператор in проверяет наличие свойства в объекте. Это полезно для различения типов в объединении (union types), особенно когда типы являются объектами с разными наборами свойств. Например, if (“radius” in shape) позволяет определить, что shape является кругом, а не квадратом. TypeScript автоматически сужает тип на основе результата проверки, предоставляя доступ к соответствующим свойствам. Этот метод часто используется с дискриминантными объединениями.

9.1.4. Сужение типов с помощью оператора равенства

Операторы равенства (===, !==, ==, !=) могут использоваться для сужения типов на основе конкретных значений. Это особенно эффективно с литеральными типами. Например, проверка if (status === “success”) позволяет TypeScript понять, что внутри блока status имеет именно это значение. Это также работает с проверкой на null или undefined: if (value !== null) гарантирует, что value не является null. Такие проверки исключают нежелательные значения из типа.

9.1.5. Использование управления потоком для сужения типов

TypeScript анализирует поток управления (control flow) для автоматического сужения типов. Это включает условия, циклы, возвраты из функций и другие конструкции, которые влияют на выполнение кода. Например, после return в функции TypeScript понимает, что последующий код не выполняется. Анализ потока позволяет точно определять типы переменных в разных частях кода, основываясь на логике программы. Это делает процесс сужения типов естественным и интегрированным в разработку.

9.2. Уровень знаний 3

9.2.1. Как сужение типов влияет на анализ потока управления

Анализ потока управления использует сужение типов для отслеживания возможных значений переменных в каждой точке программы. При встрече условий (как if, switch) TypeScript сужает тип внутри соответствующих блоков. После выхода из блока исходный тип восстанавливается, если не было присваиваний. Это позволяет точно моделировать поведение программы и обнаруживать потенциальные ошибки, например, обращение к свойству, которое может быть undefined, без предварительной проверки.

9.2.2. Примеры сужения типов при анализе потока управления

Рассмотрим функцию:

1
2
3
4
5
6
7
function example(x: string | number) { 
  if (typeof x === "string")  { 
    console.log(x.toUpperCase()); 
  } else { 
    console.log(x.toFixed(2)); 
  } 
}

Здесь анализ потока определяет, что в первом блоке x — это string, а во втором — number. После проверки typeof TypeScript позволяет использовать методы, специфичные для каждого типа. Это исключает необходимость приведения типов и делает код безопасным.

9.2.3. Применение сужения типов для управления потоком выполнения функций

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

9.2.4. Техники уточнения типа переменной при помощи сужения типов

Помимо встроенных проверок, можно использовать пользовательские type guards — функции, возвращающие предикат типа. Например: function isString(value: any): value is string { return typeof value === “string”; }. При использовании if (isString(value)) TypeScript сужает тип value до string. Это позволяет создавать сложную логику проверок, сохраняя безопасность типов. Такие guards особенно полезны для проверки сложных объектов или пользовательских классов.

9.2.5. Объяснение и примеры использования сужения типов в функциях

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

1
2
3
4
5
6
7
8
function processInput(input: string | string[]) { 
  if (Array.isArray(input)) { 
    input.forEach(process); 
  } 
  else {
     process(input); 
  } 
}

Здесь Array.isArray служит type guard, позволяя обработать как одиночные строки, так и массивы строк. Это обеспечивает правильную работу кода для всех вариантов входных данных.

9.3. Уровень знаний 4

9.3.1. Примеры использования type guards для определения типа переменных

Type guards — это функции, которые возвращают булево значение и имеют предикат типа. Например:

1
2
3
function isFish(pet: Fish | Bird): pet is Fish { 
  return (pet as Fish).swim !== undefined; 
  }

При вызове if (isFish(pet)) TypeScript сужает тип pet до Fish внутри блока. Это позволяет безопасно обращаться к свойству swim. Такие guards незаменимы для работы с сложными union types и дискриминантными объединениями.

9.3.2. Особенности и преимущества использования типа never в сужении типов

Тип never представляет значения, которых никогда не должно существовать. Он часто возникает в exhaustive checks — проверках, которые гарантируют, что все возможные случаи обработаны. Например, в switch или цепочке if-else, если TypeScript обнаруживает недостижимый код, он может использовать never для выявления ошибок на этапе компиляции. Это помогает писать более надежный код, так как компилятор предупредит, если какое-то значение не было обработано.

9.3.3. Роль typeof в сужении типов и повышение безопасности кода

Оператор typeof проверяет тип значения на этапе выполнения и возвращает строку: “string”, “number”, “boolean”, “object”, “undefined”, “function”, “symbol”. TypeScript использует эти проверки для сужения типов внутри условий. Например, if (typeof value === “string”) позволяет безопасно использовать строковые методы. Это один из самых распространенных и простых способов обеспечения типобезопасности при работе с динамическими данными.

9.3.4. Конкретные примеры использования typeof для сужения типов

Пример: функция, которая логирует значение в зависимости от его типа.

1
2
3
4
5
6
7
function logValue(value: string | number) { 
  if (typeof value === "string") { 
    console.log(value.toUpperCase()); 
  } else { 
    console.log(value.toFixed(2)); 
  } 
}

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

9.3.5. Применение type guards в реальных проектах

В реальных проектах type guards используются для валидации данных из внешних источников (API, пользовательский ввод). Например, функция isUser(data: any): data is User проверяет, что объект имеет все обязательные свойства пользователя. Это позволяет безопасно работать с данными, полученными извне, и избегать ошибок доступа к несуществующим свойствам. Guards интегрируются с системами валидации, такими как Zod или Yup, для обеспечения end-to-end типобезопасности.

10. Enum

10.1. Уровень знаний 2

10.1.1. Что такое числовые перечисления (Numeric enums) и как создать?

Числовые перечисления — это структура, где каждому элементу автоматически присваивается числовое значение, начиная с 0. Создаются с помощью ключевого слова enum. Например: enum Direction { Up, Down, Left, Right }. Здесь Up будет равно 0, Down — 1, и так далее. Можно задать начальное значение: enum Status { Success = 200, NotFound = 404 }. Числовые перечисления поддерживают reverse mapping, позволяя получить имя по значению.

10.1.2. Как определить строковое перечисление (String enums)?

Строковые перечисления инициализируются строковыми значениями. Каждый член должен быть явно проинициализирован: enum Color { Red = “RED”, Green = “GREEN” }. Они не поддерживают reverse mapping, но обеспечивают лучшую читаемость при отладке. Строковые перечисления часто используются для представления фиксированных наборов строковых значений, которые важны для семантики приложения.

10.1.3. Различия между числовыми и строковыми перечислениями

Ключевое отличие: числовые перечисления автоинкрементальные и поддерживают reverse mapping, а строковые — нет. Числовые значения более компактны для сериализации, но строковые дают понятные значения при логировании. Строковые перечисления требуют явной инициализации каждого члена. Выбор между ними зависит от потребностей: числа для эффективности, строки для читаемости и семантической ясности.

10.1.4. Основы работы с перечислениями в TypeScript

Перечисления объединяют логически связанные константы в один тип. Доступ к значениям: Direction.Up. Могут использоваться в качестве типа для переменных и параметров функций. TypeScript обеспечивает проверку на соответствие допустимым значениям перечисления. Перечисления компилируются в JavaScript объекты, что позволяет использовать их как во время компиляции, так и во время выполнения.

10.1.5. Примеры использования перечислений в TypeScript

Пример управления состоянием: enum State { Loading, Success, Error }. Обработка действий: function handleState(state: State) { switch(state) { case State.Loading: // загрузка break; } }. Для API статусов: enum HttpStatus { OK = 200, BadRequest = 400 }. Это улучшает читаемость и предотвращает использование магических чисел или строк в коде.

10.2. Уровень знаний 3

10.2.1. Использование перечислений во время компиляции

Во время компиляции перечисления используются для strict type checking. TypeScript проверяет, что присваиваемые значения соответствуют объявленному перечислению. Это помогает выявлять ошибки на раннем этапе. Перечисления также позволяют получать автодополнение в IDE, что ускоряет разработку и снижает вероятность опечаток.

10.2.2. Возможности перечислений во время выполнения

В runtime перечисления становятся реальными JavaScript объектами. Для числовых перечислений доступен reverse mapping: Direction[0] вернет “Up”. Это полезно для преобразования чисел в читаемые строки. Перечисления можно использовать в условиях, циклах и любых других конструкциях JavaScript. Однако строковые перечисления не поддерживают обратное преобразование.

10.2.3. Перечисления как типы объединения (Union enums) и типы членов перечисления (Enum member types)

Когда перечисление объявлено как const, его члены становятся типами. Это позволяет использовать их в union types: type Result = State.Success | State.Error. Каждый член перечисления может быть использован как самостоятельный тип. Это обеспечивает дополнительный уровень типобезопасности, позволяя точно указывать, какие именно значения допустимы.

10.2.4. Продвинутое использование перечислений в TypeScript

Можно создавать гетерогенные перечисления (смесь чисел и строк), но это не рекомендуется. Вычисляемые значения поддерживаются только для числовых перечислений в не-const варианте. Перечисления можно использовать вместе с keyof и mapped types для создания динамических структур. Также их можно экспортировать/импортировать между модулями.

10.2.5. Примеры сложного использования перечислений

Пример с битовыми флагами: enum FileAccess { Read = 1, Write = 2, ReadWrite = Read | Write }. Для управления правами: if (access & FileAccess.Read) { ... }. Создание дискриминантных объединений: объединение типов с общим полем-перечислением для сопоставления с образцом. Это позволяет строить сложные type-safe системы.

10.3. Уровень знаний 4

10.3.1. Сравнение объектов и перечислений в TypeScript

Объекты с as const более легковесны: const Status = { Success: 200, Error: 500 } as const. Но они не создают отдельный тип и не поддерживают reverse mapping. Перечисления предоставляют отдельный тип и namespace, но добавляют код в bundle. Выбор зависит от необходимости reverse mapping и строгой типизации.

10.3.2. Окружающие перечисления (Ambient enums) и их использование

Окружающие перечисления описывают существующие перечисления из внешнего кода. Объявляются с помощью declare enum. Используются для интеграции с JavaScript библиотеками, которые уже предоставляют перечисления. Не компилируются в JavaScript код, а только используются для проверки типов. Это помогает описывать существующие контракты без генерации лишнего кода.

10.3.3. Использование перечислений для улучшения типизации и управления состоянием в React

В React перечисления идеальны для задания ограниченного набора состояний компонента: enum ViewState { List, Grid, Detail }. Для действий в reducers: enum ActionType { Add, Delete, Update }. Это делает код более предсказуемым и исключает невалидные состояния. Перечисления хорошо работают с дискриминантными объединениями для типобезопасных редюсеров.

10.3.4. Продвинутые техники типизации с использованием перечислений

Использование template literal types с перечислениями: type EventType = ${ActionType}Event. Создание mapped types на основе перечислений: type Handlers = { [K in ActionType]: () => void }. Комбинация с conditional types для сложных преобразований. Это позволяет создавать highly expressive type system.

10.3.5. Специфические примеры использования перечислений в React

Для роутинга: enum Routes { Home = “/”, Users = “/users” }. Для тем оформления: enum Theme { Light = “light”, Dark = “dark” } с сохранением в Context. Для статуса загрузки компонента: enum LoadState { Idle, Loading, Loaded, Error }. Такие перечисления делают код более декларативным и простым для анализа.

11. Props validation

11.1. Уровень знаний 2

11.1.1. Установка значений props по умолчанию

Значения по умолчанию для props устанавливаются через свойство defaultProps у функционального компонента. Это гарантирует, что даже если проп не передан, компонент получит корректное значение. Например: Button.defaultProps = { size: ‘medium’ }. В функциональных компонентах также можно использовать параметры по умолчанию прямо в сигнатуре функции: function Button({ size = ‘medium’ }). Это обеспечивает предсказуемость работы компонента при отсутствии явных значений.

11.1.2. Использование propTypes в функциональных компонентах

Для функциональных компонентов propTypes определяются как статическое свойство после объявления функции. Например: function MyComponent({ text }) { … } followed by MyComponent.propTypes = { text: PropTypes.string }. Это позволяет проводить валидацию полученных свойств в development-режиме. Важно отметить, что проверка срабатывает только в процессе разработки и не влияет на production-сборку.

11.1.3. Преимущества использования propTypes

Основное преимущество propTypes — это раннее обнаружение ошибок во время разработки. Они служат документацией к компоненту, явно указывая ожидаемые типы и обязательность props. propTypes легко интегрируются в существующие JavaScript-проекты без необходимости перехода на TypeScript. Они предоставляют понятные сообщения об ошибках в консоли, если переданные данные не соответствуют ожиданиям.

11.2. Уровень знаний 3

11.2.1. Проверка типов во время выполнения с использованием propTypes

propTypes выполняют проверку типов исключительно во время выполнения (runtime) в браузере. Эта проверка активируется только в development-сборке приложения. При несоответствии типа в консоль выводится предупреждение, но выполнение кода не прерывается. Этот механизм не предоставляет статического анализа до запуска приложения, в отличие от TypeScript или Flow.

11.2.2. Различие между isRequired и опциональными propTypes

Ключевое различие — обязательность пропа. PropTypes.string.isRequired указывает, что проп должен быть обязательно передан компоненту, иначе будет выведено предупреждение. Опциональные пропы, объявленные как PropTypes.string, могут быть не переданы, и их значение будет undefined. Использование isRequired позволяет явно обозначить критически важные для работы компонента данные.

11.3. Уровень знаний 4

11.3.1. Сравнение Flow и TypeScript с propTypes в контексте валидации props

propTypes — это runtime-валидация, в то время как Flow и TypeScript обеспечивают статическую проверку типов на этапе компиляции. Статический анализ обнаруживает ошибки до запуска кода, что более надежно и эффективно. propTypes остаются полезны в проектах на чистом JavaScript, но в TypeScript/Flow-проектах они становятся избыточными, так как типы проверяются статически.

11.3.2. Преимущества Flow и TypeScript по сравнению с propTypes для больших приложений

Для больших приложений ключевое преимущество — статический анализ, который выявляет ошибки до выполнения кода. Это значительно повышает надежность и упрощает рефакторинг. TypeScript и Flow предоставляют мощные возможности: generics, union types, интерфейсы, которых нет в propTypes. Они интегрируются в IDE, обеспечивая автодополнение и навигацию по коду, что критично для больших codebase.

11.3.3. Основные отличия между Flow и TypeScript в валидации props

Оба инструмента выполняют статическую проверку, но TypeScript — это полноценный язык со своей экосистемой, а Flow — анализатор типов для JavaScript. Синтаксис TypeScript более стандартизирован и распространен. TypeScript требует явного преобразования .js файлов в .ts, в то время Flow можно постепенно внедрять в существующие JS-файлы. Поддержка и сообщество TypeScript значительно шире, что делает его более предсказуемым выбором для новых проектов.

12. PureComponent/memo

12.1. Уровень знаний 2

12.1.1. Определение и назначение PureComponent

PureComponent — это базовый класс в React, который автоматически реализует метод shouldComponentUpdate с поверхностным сравнением пропсов и состояния. Его основное назначение — оптимизация производительности за счет предотвращения ненужных ререндеров компонента. Если пропсы и состояние не изменились, компонент не будет обновляться. Это особенно полезно для компонентов, которые часто перерисовываются с одинаковыми данными.

12.1.2. Случаи использования PureComponent

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

12.2. Уровень знаний 3

12.2.1. Сравнение PureComponent и Component

Основное отличие — PureComponent автоматически реализует поверхностное сравнение в shouldComponentUpdate, тогда как обычный Component всегда перерисовывается при обновлении родителя. PureComponent требует больше памяти для хранения предыдущих пропсов и состояния. Обычный Component проще в использовании но может привести к избыточным ререндерам. Выбор зависит от необходимости оптимизации производительности.

12.2.2. Ограничения и сравнение пропсов в PureComponent

Главное ограничение — поверхностное сравнение: вложенные объекты и массивы сравниваются по ссылке, а не по содержимому. Если пропсы содержат сложные структуры данных, которые мутируются, сравнение может дать ложный отрицательный результат. Для работы с изменяемыми данными требуется использование иммутабельных структур данных или глубокое сравнение через shouldComponentUpdate. Это важно учитывать при проектировании компонента.

12.3. Уровень знаний 4

12.3.1. Принципы и использование React.memo

React.memo — аналог PureComponent для функциональных компонентов. Это higher-order component, который мемоизирует результат рендера и переиспользует его при неизмененных пропсах. Принимает компонент и optional функцию сравнения пропсов. Используется для оптимизации функциональных компонентов также как PureComponent для классовых. Подходит для предотвращения ненужных ререндеров когда родитель обновляется часто.

12.3.2. Контроль сравнения пропсов в React.memo

Для кастомного сравнения пропсов React.memo принимает вторым аргументом функцию arePropsEqual(prevProps, nextProps). Эта функция должна возвращать true если пропсы равны и ререндер не нужен. Позволяет реализовать глубокое сравнение или игнорировать определенные пропсы. По умолчанию используется поверхностное сравнение аналогичное PureComponent. Кастомная функция сравнения требует осторожности чтобы не пропустить важные изменения данных.

13. Refs

13.1. Уровень знаний 2

13.1.1. Базовое понимание refs в React

Refs (ссылки) предоставляют способ доступа к DOM-узлам или React-элементам, созданным в методе render. В отличие от состояния, изменения refs не вызывают повторный рендер компонента. Они используются для императивного взаимодействия с дочерними компонентами или DOM-элементами. Refs создаются с помощью React.createRef() в классовых компонентах или useRef() в функциональных.

13.1.2. Основные применения refs в React

Основные применения включают: управление фокусом, выделение текста или воспроизведение медиа. Также refs используются для интеграции со сторонними DOM-библиотеками. Они позволяют вызывать методы дочерних компонентов напрямую. Еще одно применение — измерение размеров или положения DOM-элемента. Refs идеальны для сценариев, где не нужен реактивный рендеринг.

13.1.3. Концепция доступа к DOM-элементам с помощью refs

Доступ к DOM-элементам осуществляется через присвоение ref атрибуту элемента. В классовых компонентах ref создается в конструкторе и присваивается через this.myRef = React.createRef(). В функциональных используется хук useRef(): const myRef = useRef(null). После присвоения, доступ к элементу получаем через myRef.current.

13.1.4. Введение в управление фокусом элементов через refs

Управление фокусом — одно из основных применений refs. После получения ссылки на элемент, можно вызвать метод focus(): myRef.current.focus(). Это особенно полезно для форм и accessibility. Например, автоматический фокус на поле ввода после монтирования компонента. Также можно управлять фокусом при валидации или навигации по форме.

13.1.5. Различия между refs и состоянием (useState)

Ключевое отличие: изменения refs не вызывают ререндер, а изменения состояния — вызывают. Refs мутабельны и обновляются синхронно, состояние иммутабельно и обновляется асинхронно. Refs лучше подходят для значений, которые не влияют на отображение. Состояние используется для данных, которые должны trigger перерисовку компонента. Refs сохраняются между рендерами, как и состояние.

13.2. Уровень знаний 3

13.2.1. Продвинутое использование refs в React

Продвинутое использование включает: создание callback refs для динамического управления ссылками. Использование refs для хранения таймеров или интервалов. Интеграция с Web API, которые требуют прямого доступа к DOM. Реализация сложных анимаций без перерисовок. Сохранение предыдущих значений состояния через refs. Мемоизация тяжелых вычислений.

13.2.2. Использование ref для управления фокусом на элементе

Refs позволяют управлять фокусом на DOM-элементах императивным способом. После создания ref и присвоения его целевому элементу, можно программно вызывать метод focus() на этом элементе. Это особенно полезно для улучшения пользовательского опыта в формах, когда необходимо автоматически установить фокус на поле ввода после загрузки компонента или при переходе между шагами многостраничной формы. Управление фокусом через refs также важно для обеспечения доступности приложения, позволяя направлять внимание пользователя на ключевые элементы интерфейса.

13.2.3. Техники доступа к DOM элементам через refs

Доступ через ref.current дает полный доступ к DOM API элемента. Можно использовать: getBoundingClientRect() для размеров, addEventListener() для обработки событий. Важно проверять ref.current на null перед использованием. Для динамических элементов использовать callback refs. Избегать частых обновлений через refs чтобы не нарушать производительность.

13.2.4. Особенности работы с forwardRef

forwardRef позволяет передавать ref через компоненты-обертки. Используется когда нужно получить доступ к DOM-элементу внутри кастомного компонента. Синтаксис: React.forwardRef((props, ref) => {}). Ref передается как второй аргумент и может быть присвоен внутреннему элементу. Особенно полезно для библиотек компонентов.

13.2.5. Методы управления состоянием компонентов через refs

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

13.3. Уровень знаний 4

13.3.1. Глубокое понимание refs в React

Refs работают на принципе мутабельного контейнера, который сохраняется между рендерами. Они не уведомляют компонент об изменениях. Ref объект имеет свойство current, которое можно читать и изменять. При присвоении ref элементу, React автоматически установит current в соответствующий DOM-узел. При удалении элемента, current становится null.

13.3.2. Примеры использования refs для управления состоянием

Пример хранения предыдущего значения:

1
2
3
4
5
6
7
8
function Component() {
  const [count, setCount] = useState(0);
  const prevCount = useRef(count);
  
  useEffect(() => {
    prevCount.current = count;
  }, [count]);
}

13.3.3. Сравнительный анализ refs и состояния через useState с примерами

Ключевое отличие между refs и состоянием заключается в том, что обновление состояния через useState вызывает повторный рендер компонента, в то время как изменение значения ref не приводит к перерисовке. Состояние следует использовать для данных, которые直接影响 визуальное отображение компонента, например, значения полей формы, открытие/закрытие модальных окон или переключение тем оформления. Refs лучше подходят для хранения служебной информации, которая не влияет на UI, такой как хранение ссылок на DOM-элементы, управление таймерами или сохранение предыдущих значений состояния для сравнения.

13.3.4. Комплексные сценарии применения refs в разработке

Сложные сценарии включают: реализация drag-and-drop без лишних ререндеров. Интеграция с D3.js или другими графическими библиотеками. Управление воспроизведением видео/аудио. Реализация сложных анимаций через requestAnimationFrame. Создание кастомных хуков с сохранением состояния между рендерами. Оптимизация производительности тяжелых компонентов.

13.3.5. Подробное изучение жизненного цикла и взаимодействия refs с компонентами React

Refs присваиваются после монтирования компонента и обновляются при обновлении DOM. При unmount, refs автоматически очищаются (current = null). В классовых компонентах refs создаются в конструкторе и существуют весь жизненный цикл. В функциональных — сохраняются между рендерами. Изменения refs внутри useEffect не trigger эффекты, так как нет зависимости.

14. Context

14.1. Уровень знаний 2

14.1.1. Основы React.Context

React Context — это механизм для передачи данных через дерево компонентов без необходимости передавать пропсы на каждом уровне. Он предназначен для глобальных данных, которые могут потребоваться многим компонентам в приложении. Контекст позволяет избежать “пропс дриллинга” — ситуации, когда пропсы передаются через множество промежуточных компонентов, которые сами не используют эти данные. Этот механизм особенно полезен для тем оформления, данных аутентификации, языковых настроек и других общих состояний.

14.1.2. Создание и использование контекста

Создание контекста начинается с функции React.createContext(), которая возвращает объект с Provider и Consumer компонентами. Значение по умолчанию можно передать в createContext, оно используется когда компонент не имеет соответствующего Provider выше в дереве. Для использования контекста в функциональных компонентах применяется хук useContext(), который принимает объект контекста и возвращает его текущее значение. В классовых компонентах можно использовать Consumer или static contextType.

14.1.3. Цели и преимущества использования React.Context

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

14.2. Уровень знаний 3

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

Контекст позволяет передавать данные непосредственно в глубоко вложенные компоненты, минуя промежуточные уровни. Provider компонент оборачивает часть дерева компонентов и передает значение через свойство value. Любой компонент внутри этого Provider, независимо от глубины вложенности, может получить доступ к данным контекста с помощью useContext или Consumer. Это устраняет необходимость передачи пропсов через компоненты, которые сами не используют эти данные.

14.2.2. Использование Provider и Consumer

Provider компонент принимает пропс value и предоставляет это значение всем потребителям контекста в своем поддереве. Consumer компонент позволяет подписаться на изменения контекста без использования хуков. Consumer использует render prop подход — функция в качестве дочернего элемента, которая получает текущее значение контекста. В современных приложениях Consumer используется реже, так как хук useContext предоставляет более простой и elegant способ доступа к контексту.

14.2.3. Обновление данных в контексте с помощью React hooks

Для обновления данных в контексте часто используется комбинация useContext и useReducer или useState. Провайдер может передавать не только данные, но и функции для их обновления. Например, значение контекста может включать состояние и функцию dispatch для useReducer. Это позволяет компонентам-потребителям не только читать данные, но и инициировать их обновление. Важно мемоизировать значение контекста чтобы избежать ненужных ререндеров.

14.3. Уровень знаний 4

14.3.1. Темизация приложений с использованием React.Context

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

14.3.2. Влияние контекста на производительность и оптимизация

Контекст может влиять на производительность при частых обновлениях, так как все потребители перерисовываются при изменении значения. Для оптимизации следует разделять контексты на логические части чтобы избежать ненужных ререндеров. Мемоизация значения контекста с помощью useMemo предотвращает обновления при неизменных данных. Также можно использовать несколько специализированных контекстов вместо одного большого. Важно избегать передачи часто изменяющихся объектов в значение контекста.

14.3.3. Управление состоянием с использованием useContext и useReducer

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

15. Portals

15.1. Уровень знаний 2

15.1.1. Основы порталов в React

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

15.1.2. Создание порталов в React приложениях

Для создания портала используется функция ReactDOM.createPortal(). Первый аргумент — это React-компонент или элемент, который нужно отрендерить, а второй — DOM-узел, в который будет произведён рендеринг. Обычно целевой DOM-узел создается заранее и находится вне корневого элемента React-приложения. Порталы можно использовать в любом компоненте, и они интегрируются с жизненным циклом React, как и любой другой компонент.

15.1.3. Сценарии использования порталов в React

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

15.1.4. Преимущества использования порталов для модальных окон

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

15.1.5. Передача контекста через порталы в React

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

15.2. Уровень знаний 3

15.2.1. Работа с DOM-узлами вне иерархии при использовании порталов

При использовании порталов React-компонент рендерится в выбранный DOM-узел, который может находиться anywhere в документе, но логически он остаётся частью React-дерева. Это позволяет обходить ограничения CSS, такие как overflow: hidden или z-index, которые могут влиять на отображение компонента если бы он рендерился в своём обычном месте. При этом все события и обновления состояния обрабатываются React-ом как обычно.

15.2.2. Использование порталов для улучшения UX при работе с модальными окнами

Порталы улучшают пользовательский опыт, обеспечивая корректное отображение модальных окон поверх всего контента, включая элементы с высоким z-index. Они предотвращают проблемы с обрезанием контента из-за CSS-свойств overflow и position. Порталы также помогают в управлении фокусом и доступностью, автоматически обрабатывая переход фокуса в модальное окно и обратно, что особенно важно для пользователей клавиатуры и screen readers.

15.2.3. Передача данных и контекста через порталы

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

15.2.4. Интеграция порталов с существующими компонентами React

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

15.2.5. Управление событиями в порталах

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

15.3. Уровень знаний 4

15.3.1. Всплытие событий через порталы

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

15.3.2. Обработка событий в порталах

Обработка событий в порталах ничем не отличается от обработки событий в обычных компонентах. Можно использовать стандартные React-обработчики событий, такие как onClick, onChange и другие. События будут корректно обрабатываться, и их всплытие будет происходить через React-дерево, что позволяет использовать привычные паттерны обработки событий.

15.3.3. Особенности всплытия событий через порталы по сравнению с обычным всплытием

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

15.3.4. Продвинутые техники управления событиями в порталах

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

15.3.5. Глубокое понимание механизмов работы порталов в React

Порталы работают на уровне React Reconciler, который обеспечивает интеграцию порталов с виртуальным DOM и жизненным циклом React. Несмотря на рендеринг в другом месте DOM-дерева, порталы полностью participate в процессе согласования React. Это означает, что обновления состояния, пропсов и контекста корректно применяются к компонентам внутри порталов. React управляет созданием, обновлением и удалением порталов так же, как и обычными компонентами.

16. Controlled/uncontrolled components

16.1. Уровень знаний 2

16.1.1. Определение и различия между управляемыми и неуправляемыми компонентами

Управляемые компоненты — это компоненты, которые получают своё текущее значение через пропсы и уведомляют об изменениях через колбэки (например, onChange). Значение таких компонентов полностью контролируется React-состоянием. Неуправляемые компоненты хранят своё состояние в DOM и используют refs для получения текущего значения. Разница заключается в том, где хранится состояние: у управляемых — в состоянии React, у неуправляемых — в DOM.

16.1.2. Преимущества и недостатки управляемых компонентов

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

16.2. Уровень знаний 3

16.2.1. Суть управляемых и неуправляемых компонентов

Суть управляемых компонентов в том, что их состояние управляется React через пропсы, что обеспечивает предсказуемость и контроль. Неуправляемые компоненты ближе к традиционному HTML, где DOM является источником истины, а React получает доступ к данным через refs по мере необходимости. Выбор между ними зависит от конкретных требований: управляемые для сложной логики и валидации, неуправляемые для простых форм и интеграции с ненактивными библиотеками.

16.2.2. Работа с управляемыми компонентами

Работа с управляемыми компонентами включает: объявление состояния для хранения значения, передачу этого значения в компонент через пропс, обработку изменений через колбэк (например, onChange), обновление состояния при получении новых значений. Это позволяет immediately реагировать на изменения, проводить валидацию, управлять disabled-состоянием кнопок и динамически менять поведение формы, основанной на введённых данных.

16.3. Уровень знаний 4

16.3.1. Примеры работы с неуправляемыми компонентами в React

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

17. Patterns

17.1. Уровень знаний 2

17.1.1. Условный рендеринг в React

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

17.1.2. Использование тернарного оператора для условного рендера

Тернарный оператор — компактный способ условного рендеринга непосредственно в JSX. Синтаксис: {condition ? : }. Он идеален для простых условий с двумя вариантами отображения. Тернарный оператор сохраняет читаемость кода при простых условиях, но может ухудшить её при сложных вложенных условиях.

17.1.3. Рендеринг компонентов на основе состояния или пропсов

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

17.2. Уровень знаний 3

17.2.1. Концепция render props в React

Render props — паттерн, при котором компонент принимает функцию в качестве пропса и вызывает её для рендера содержимого. Эта функция получает данные или методы компонента и возвращает React-элементы. Паттерн позволяет повторно использовать логику компонента и инкапсулировать состояние, предоставляя гибкость в рендеринге.

17.2.2. Примеры использования render props

Компонент переключения:

1
2
3
4
5
6
7
8
9
const Toggle = ({ render }) => {
  const [on, setOn] = useState(false);
  return render({ on, toggle: () => setOn(!on) });
};

// Использование
<Toggle render={({ on, toggle }) => 
  <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
}/>

17.2.3. Принципы композиции компонентов с помощью render props

Render props позволяют композицию компонентов через передачу функций, что делает код декларативным и переиспользуемым. Компоненты с render props могут быть вложены друг в друга, создавая сложную логику с чистым разделением ответственности. Каждый компонент отвечает за свою логику, а рендеринг делегируется наружу через render prop.

17.3. Уровень знаний 4

17.3.1. Концепция HOC (Higher-Order Component)

HOC — функция, которая принимает компонент и возвращает новый компонент с дополнительной логикой или пропсами. Это паттерн для повторного использования кода и cross-cutting concerns. HOC не изменяет оригинальный компонент, а оборачивает его в новый, добавляя новую функциональность.

17.3.2. Преимущества использования HOC

Основные преимущества: переиспользование логики между компонентами, абстракция сложной функциональности, separation of concerns. HOC позволяют выносить общую логику (аутентификация, загрузка данных, подписки) в отдельные функции. Они интегрируются с экосистемой React и совместимы с другими паттернами.

17.3.3. Примеры создания и использования собственных HOC в React

Пример HOC для аутентификации:

1
2
3
4
5
6
const withAuth = (Component) => {
  return (props) => {
    const isAuthenticated = checkAuth();
    return isAuthenticated ? <Component {...props} /> : <LoginPage />;
  };
};

18. Basic react optimization

18.1. Уровень знаний 2

18.1.1. Использование ключей в React

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

18.1.2. Принципы правильного использования ключей в списках

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

18.2. Уровень знаний 3

18.2.1. Метод жизненного цикла shouldComponentUpdate

shouldComponentUpdate — это метод жизненного цикла классового компонента, который позволяет оптимизировать производительность. Он вызывается перед рендерингом и получает новые пропсы и состояние. Возвращая false, можно предотвратить ненужный ререндер компонента.

18.2.2. Оптимизация рендеринга компонентов с помощью shouldComponentUpdate

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

18.3. Уровень знаний 4

18.3.1. Отличия между Component и PureComponent

PureComponent автоматически реализует поверхностное сравнение пропсов и состояния в shouldComponentUpdate. Обычный Component всегда выполняет ререндер при обновлении родителя. PureComponent подходит для простых пропсов, но может пропустить обновления при глубоких изменениях объектов.

18.3.2. Влияние props на рендеринг компонентов

Изменение пропсов всегда вызывает ререндер компонента (если не предотвращено через shouldComponentUpdate или React.memo). React сравнивает ссылки на пропсы, а не их содержимое. Передача новых объектов или функций в пропсы всегда вызовет ререндер, даже если их содержимое не изменилось.

18.3.3. Профилирование производительности в React

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

19. Reselect&Recompose

19.1. Уровень знаний 2

19.1.1. Основы библиотеки Reselect

Reselect — это библиотека для создания мемоизированных селекторов в Redux. Селекторы представляют собой функции, которые извлекают и преобразовывают данные из хранилища. Reselect позволяет создавать селекторы, которые пересчитываются только при изменении входных данных, что оптимизирует производительность.

19.1.2. Основы библиотеки Recompose

Recompose — это библиотека утилит для функциональных компонентов React, которая предоставляет набор Higher-Order Components (HOC) и утилит для управления состоянием, жизненным циклом и другими аспектами компонентов. Она позволяет использовать паттерны функционального программирования в React.

19.1.3. Преимущества использования Reselect и Recompose

Reselect: улучшает производительность за счёт мемоизации, уменьшает количество перерасчетов, упрощает тестирование селекторов. Recompose: повышает переиспользуемость кода, позволяет разделять логику и представление, упрощает работу с функциональными компонентами, предоставляет готовые HOC для common задач.

19.1.4. Различия между Reselect и обычными селекторами в Redux

Обычные селекторы вычисляются при каждом вызове, что может lead к performance issues при complex вычислениях. Reselect селекторы мемоизируют результаты и пересчитываются только при изменении входных данных, что делает их более эффективными.

19.1.5. Проблемы, которые решают Reselect и Recompose

Reselect решает проблемы избыточных перерасчетов в селекторах и улучшает производительность при работе с большими объемами данных. Recompose решает проблемы сложности управления состоянием и логикой в функциональных компонентах, предоставляя инструменты для композиции и reuse.

19.2. Уровень знаний 3

19.2.1. Мемоизация в Reselect

Мемоизация в Reselect реализована через кэширование результатов вызовов селекторов. Селектор пересчитывается только если изменились входные данные (пропсы или часть состояния). Это предотвращает избыточные вычисления и улучшает производительность.

19.2.2. Типы компонентов, создаваемые с помощью Recompose

Recompose позволяет создавать компоненты, обогащенные дополнительной функциональностью: withState (управление состоянием), withHandlers (обработчики событий), lifecycle (методы жизненного цикла), pure (оптимизация рендеров), и многие другие.

19.2.3. Оптимизация сложных селекторов с Reselect

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

19.2.4. Использование HOC из Recompose

Recompose предоставляет множество HOC, таких as withState, withHandlers, compose, withProps. Они могут быть скомбинированы для добавления функциональности к компонентам. Например, compose(withState(‘counter’, ‘setCounter’, 0), withHandlers({ increment: ({ setCounter }) => () => setCounter(n => n + 1) })).

19.2.5. Ограничения и недостатки Recompose

Recompose добавляет layer абстракции, что может complicate debugging. С появлением Hooks многие возможности Recompose стали less актуальными, так как Hooks предоставляют similar functionality в более simple и integrated форме.

19.3. Уровень знаний 4

19.3.1. Создание кастомного селектора с Reselect

Кастомный селектор создается с помощью createSelector. Первые аргументы — input селекторы, последний — transform функция. Например: const getVisibleTodos = createSelector([getTodos, getVisibilityFilter], (todos, filter) => { return todos.filter(todo => todo.text.includes(filter)) }).

19.3.2. Управление состоянием в функциональных компонентах с помощью Recompose

Recompose предоставляет HOC withState для управления локальным состоянием в функциональных компонентах. Он принимает имя свойства, функцию для его обновления и начальное значение. Это позволяет использовать state в functional компонентах до появления Hooks.

19.3.3. Продвинутые техники мемоизации в Reselect

Продвинутые техники включают: кастомная функция сравнения для input селекторов, использование createSelector с multiple аргументами, создание селекторов которые depend от пропсов компонента, и комбинирование селекторов в complex цепочки.

19.3.4. Использование Reselect с React-Redux

Reselect seamlessly интегрируется с React-Redux. Селекторы могут быть использованы в mapStateToProps для предоставления данных компонентам. Это ensures что компоненты re-render только когда relevant данные изменяются, improving performance.

19.3.5. Паттерны повышения переиспользуемости логики с Recompose

Recompose позволяет выносить общую логику в HOC, которые затем могут быть reused across multiple компонентов. Паттерны include: extraction of state management, event handlers, lifecycle methods, и props transformations into separate HOC, which are then composed together.

20. Virtualization

20.1. Уровень знаний 2

20.1.1. Основы виртуализации

Виртуализация — это техника рендеринга только видимой части данных вместо отображения всего массива информации. Она работает по принципу “окна”, которое показывает ограниченное количество элементов, динамически подгружая и выгружая контент по мере прокрутки. Это значительно снижает нагрузку на DOM и улучшает производительность.

20.1.2. Цели и применение виртуализации

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

20.2. Уровень знаний 3

20.2.1. Библиотеки для виртуализации в React

Популярные библиотеки: react-window, react-virtualized, react-virtuoso и react-tiny-virtual-list. Они предоставляют готовые компоненты для виртуализации списков, таблиц и сеток. Выбор зависит от сложности задач и требований к функциональности.

20.2.2. Использование react-window для виртуализации

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

20.3. Уровень знаний 4

20.3.1. Влияние виртуализации на оптимизацию рендеринга

Виртуализация сокращает количество DOM-узлов, что ускоряет первоначальный рендеринг и обновления. Уменьшает время layout и paint в браузере, особенно заметное на мобильных устройствах и больших наборах данных.

20.3.2. Решаемые виртуализацией проблемы

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

20.3.3. Реализация виртуализации без сторонних библиотек

Базовая реализация involves расчет видимого area, рендеринг только visible элементов и обработка scroll событий. Требует ручного управления позиционированием через absolute или transform координаты. Подходит для простых случаев но lacks оптимизаций готовых библиотек.

20.3.4. Подходы к виртуализации за пределами списков

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

20.3.5. Оптимизация производительности через виртуализацию

Дополнительные техники: lazy loading контента, prefetching следующих элементов, memoization рендеринга элементов и оптимизация обработки scroll событий через throttling. Комбинация этих методов с виртуализацией дает максимальный прирост производительности.

21. useMemo/useCallback

21.1. Уровень знаний 2

21.1.1. Различие между useMemo и useCallback

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

21.1.2. Основные цели использования useMemo и useCallback

Основная цель — оптимизация производительности через предотвращение избыточных вычислений и рендеров. useMemo избегает повторных вычислений значений, useCallback сохраняет стабильность ссылок на функции, что предотвращает ненужные обновления дочерних компонентов.

21.1.3. Типы значений для мемоизации с помощью useMemo

useMemo подходит для любых вычисляемых значений: отфильтрованные или отсортированные массивы, сложные объекты, результаты математических вычислений, JSX-элементы. Главное условие — стоимость вычисления должна быть достаточно высокой для оправдания мемоизации.

21.1.4. Основные преимущества мемоизации в React

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

21.1.5. Как и когда определять необходимость использования мемоизации

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

21.2. Уровень знаний 3

Как избежать лишних рендеров компонентов с помощью useMemo и useCallback?

Answer: Для избежания лишних рендеров компонентов нужна мемоизация. Использование useMemo и useCallback позволяет контролировать обновление компонентов. Они особенно эффективны в сочетании с React.memo, так как предотвращают передачу новых ссылок на функции или значения при каждом рендере. Они помогают избежать лишних рендеров, сохраняя стабильные ссылки на функции и значения между рендерами, что критично для компонентов, которые зависят от поверхностного сравнения пропсов.

21.2.1. Оптимизация работы компонентов React с помощью useMemo и useCallback

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

21.2.2. Избежание лишних рендеров и перерисовок с помощью useMemo и useCallback

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

21.2.3. Лучшие практики использования useMemo и useCallback

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

21.2.4. Управление зависимостями в useMemo и useCallback

Зависимости должны включать все значения, используемые внутри хука и изменяемые между рендерами. Неполный список зависимостей может привести к использованию устаревших значений, а избыточный — к слишком частым пересчётам.

21.2.5. Критерии для выбора между использованием useMemo и useCallback

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

21.3. Уровень знаний 4

21.3.1. Продвинутые техники оптимизации с использованием useMemo и useCallback

Комбинирование с пользовательскими хуками, создание мемоизированных селекторов для Redux (Библиотека Reselect — это аналог useMemo для Redux. Она создает селекторы, которые мемоизируют результат своих вычислений. ), использование для мемоизации компонентов высшего порядка, интеграция с контекстом для предотвращения ненужных обновлений.

Конкретные техники для useMemo: Мемоизация тяжёлых вычислений: Преобразование данных, фильтрация больших массивов, агрегация. Стабилизация ссылок на объекты и массивы: Передача в качестве пропсов или в зависимости других хуков. Конкретные техники для useCallback: Передача колбэков в оптимизированные дочерние компоненты (обернутые в React.memo). Зависимости других хуков: Если колбэк является зависимостью useEffect или useMemo, его стабильность предотвращает повторные запуски этих хуков. Мемоизация обработчиков событий

21.3.2. Методы определения и устранения ненужной мемоизации

Профилирование производительности React DevTools, анализ количества ререндеров, измерение времени вычислений. Удалять мемоизацию для простых операций, где затраты на сравнение зависимостей превышают пользу от кэширования.

21.3.3. Оптимизация производительности в высоконагруженных приложениях

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

21.3.4. Измерение эффективности использования хуков в проекте

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

21.3.5. Интеграция useMemo и useCallback с другими методами оптимизации

Эффективно сочетать с: React.memo для компонентов, виртуализацией для списков, lazy loading для кода, ошибки границ для обработки сбоев, контекстом с мемоизированными значениями для глобального состояния.

22. Lazy imports and dynamic imports

22.1. Уровень знаний 2

22.1.1. Основы ленивой загрузки в React

Ленивая загрузка (lazy loading) — это метод, при котором компоненты или ресурсы загружаются только тогда, когда они действительно нужны, а не сразу при запуске приложения. В React это достигается с помощью функции React.lazy(), которая позволяет динамически импортировать компоненты. Это особенно полезно для больших приложений с множеством компонентов, чтобы уменьшить начальный размер bundle и ускорить загрузку.

22.1.2. Преимущества ленивой загрузки

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

22.1.3. Применение ленивой загрузки за пределами React компонентов

Ленивая загрузка может применяться не только к компонентам, но и к другим ресурсам, таким как изображения, видео, шрифты или даже данные. Например, изображения могут загружаться только когда они попадают в viewport, что снижает начальную нагрузку. Также можно лениво загружать модули JavaScript или CSS, которые не критичны для первоначального отображения.

22.2. Уровень знаний 3

22.2.1. Работа механизма React.lazy и React.Suspense

React.lazy() принимает функцию, которая возвращает динамический импорт компонента: const LazyComponent = React.lazy(() => import('./LazyComponent')). React.Suspense используется для отображения fallback-контента (например, спиннера) пока ленивый компонент загружается: <Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense>.

22.2.2. Ограничения ленивой загрузки в React

React.lazy работает только с default экспортами. Он не поддерживает серверный рендеринг (SSR) из коробки, что требует дополнительных решений для Next.js или других фреймворков. Также возможны задержки при первом обращении к компоненту, так как он должен быть загружен по сети.

22.3. Уровень знаний 4

22.3.1. Оптимизация компонентов с внешними данными через ленивую загрузку

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

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

Динамический импорт — это нативная возможность JavaScript, которую использует React.lazy. Синтаксис import() возвращает промис, который резолвится в модуль. Это позволяет загружать модули по требованию, а не во время initial parsing кода. Динамический импорт можно использовать и без React.lazy для любых модулей.

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

Ленивая загрузка значительно улучшает метрики как Time to Interactive (TTI), так и First Contentful Paint (FCP), уменьшая объем кода, который нужно обработать браузеру на старте. Для измерения эффективности можно использовать Lighthouse или Webpack Bundle Analyzer. Важно балансировать, чтобы не разбивать код на слишком мелкие chunks, что может увеличить количество запросов.

23. Concurrent Mode and Suspense

23.1. Уровень знаний 2

23.1.1. Основы Concurrent Mode в React

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

23.1.2. Включение Concurrent Mode в React приложении

Concurrent Mode включается использованием новых корневых API: createRoot вместо ReactDOM.render. Например: ReactDOM.createRoot(document.getElementById(‘root’)).render(). Это активирует concurrent возможности, такие как приоритизация обновлений и прерываемый рендеринг.

23.1.3. Преимущества использования Concurrent Mode

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

23.2. Уровень знаний 3

23.2.1. Влияние Concurrent Mode на рендеринг компонентов

Concurrent Mode делает рендеринг прерываемым — React может приостанавливать длительные рендеры для обработки более важных задач. Это позволяет избегать лагов и задержек при взаимодействии. Компоненты могут использовать новые хуки как useTransition и useDeferredValue для управления приоритетом обновлений.

23.2.2. Использование UI-паттернов в Concurrent Mode

Новые паттерны включают: постепенное раскрытие контента с помощью Suspense, индикаторы загрузки для фоновых процессов, приоритизацию ввода пользователя над фоновыми обновлениями. Это позволяет создавать более предсказуемые и плавные пользовательские experiences.

23.3. Уровень знаний 4

23.3.1. Управление приоритетами обновлений с помощью Concurrent API

Concurrent API предоставляет useTransition для низкоприоритетных обновлений и useDeferredValue для отложенного обновления значений. Это позволяет React обрабатывать срочные обновления (как пользовательский ввод) немедленно, откладывая менее важные.

23.3.2. Сложности и решения при миграции на Concurrent Mode

Основные сложности: потенциальные breaking changes в жизненных циклах компонентов, необходимость адаптации под прерываемый рендеринг, совместимость с существующими библиотеками. Решения: постепенное внедрение через createRoot, тестирование с StrictMode, использование concurrent функций только где необходимо.

23.3.3. Оптимизация производительности с помощью Concurrent API

Concurrent API позволяет оптимизировать: время реакции на пользовательский ввод через приоритизацию, память через отмену устаревших рендеров, плавность анимаций через разделение работы на chunks. useDeferredValue помогает избегать jank при частых обновлениях сложных компонентов.

24. Bundle optimization

24.1. Уровень знаний 2

24.1.1. Работа с lazy-loading в React

Lazy-loading позволяет загружать компоненты только когда они действительно нужны. Используется React.lazy() для динамического импорта компонентов и React.Suspense для отображения запасного контента во время загрузки. Это уменьшает начальный размер бандла и ускоряет загрузку приложения.

24.1.2. Tree shaking в React

Tree shaking — это процесс удаления неиспользуемого кода из финального бандла. Webpack и другие сборщики анализируют импорты и экспорты, исключая код, который нигде не используется. Для эффективного tree shaking важно использовать ES6 модули и избежать side effects в модулях.

24.2. Уровень знаний 3

24.2.1. Измерение import cost в React приложениях

Import cost показывает размер импортируемых модулей. Инструменты вроде import-cost extension для VSCode визуализируют размер каждого импорта прямо в редакторе. Это помогает выявлять тяжелые зависимости и оптимизировать импорты.

24.2.2. Оптимизация Webpack для улучшения производительности сборки

Оптимизация включает: настройку splitChunks для разделения кода, минификацию с TerserPlugin, сжатие gzip/brotli, кэширование сборок, использование DllPlugin для редко изменяемых зависимостей. Анализ бандла с Webpack Bundle Analyzer помогает найти точки для оптимизации.

24.2.3. Использование chunks для оптимизации загрузки в React

Chunks позволяют разделить код на отдельные файлы, которые загружаются по требованию. Webpack автоматически создает chunks для динамических импортов. Это уменьшает initial bundle size и ускоряет время загрузки. Настройка splitChunks позволяет контролировать группировку модулей.

24.3. Уровень знаний 4

24.3.1. Стратегии кэширования для React приложений

Эффективные стратегии: хэширование имен файлов для долгосрочного кэширования, использование Service Workers для кэширования ресурсов, настройка CDN для статических файлов. Кэширование сборок Webpack и зависимостей уменьшает время повторной загрузки.

24.3.2. Настройки Webpack для оптимизации проекта React

Ключевые настройки: mode: ‘production’, минификация, разделение кода, сжатие assets, tree shaking, устранение дубликатов зависимостей. Оптимизация разрешения модулей через alias и extensions. Использование cache для ускорения повторных сборок.

24.3.3. Внутренние инструменты React для оптимизации

React предоставляет: React.memo для мемоизации компонентов, useMemo и useCallback для мемоизации значений и функций, Profiler для измерения производительности. Concurrent Mode с useTransition и useDeferredValue для приоритизации обновлений.

25. React devtools and redux devtools

25.1. Уровень знаний 2

25.1.1. Что такое React DevTools и для чего они используются?

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

25.1.2. Как установить Redux DevTools в браузер?

Redux DevTools устанавливаются как расширение для браузера (Chrome, Firefox, Edge) через официальные магазины расширений. После установки необходимо подключить его к хранилищу Redux с помощью middleware (например, redux-devtools-extension). Для этого в store добавляется: const store = createStore(reducer, window.REDUX_DEVTOOLS_EXTENSION && window.REDUX_DEVTOOLS_EXTENSION()).

25.1.3. В чем основное предназначение Redux DevTools?

Основное предназначение — отладка состояния Redux. Инструменты позволяют отслеживать dispatched actions, inspect текущее и предыдущее состояние, откатывать действия (time travel debugging), и экспортировать/импортировать состояние для совместной отладки.

25.2. Уровень знаний 3

25.2.1. Как сохранить состояние приложения в Redux DevTools для последующего анализа?

В Redux DevTools есть кнопка “Export” (значок download), которая сохраняет текущее состояние в JSON-файл. Этот файл можно импортировать later через “Import” для восстановления точного состояния приложения. Это полезно для баг-репортов и анализа специфических сценариев.

25.2.2. Какие возможности предоставляют React DevTools для исследования и отладки компонентов React?

Возможности включают: inspection дерева компонентов с пропсами и состоянием, highlighting обновлений компонентов в реальном времени, profiling производительности рендеров, отладка хуков (состояние, эффекты, контекст), и поиск по компонентам. Также есть возможность изменять пропсы и состояние прямо в браузере для тестирования.

25.2.3. Как можно использовать Redux DevTools для отслеживания действий и изменений в состоянии приложения?

Redux DevTools показывает лог всех dispatched actions с их payload и временными метками. Для каждого действия можно увидеть разницу в состоянии (diff), откатить действие к предыдущему состоянию, или пропустить action. Это позволяет точно определить, какое действие привело к изменению состояния.

25.3. Уровень знаний 4

25.3.1. Как профилировать компоненты в React DevTools для определения узких мест в производительности?

Вкладка “Profiler” позволяет записывать сессии взаимодействия с приложением. После записи можно analyze время рендера каждого компонента, причины ререндеров (props, state, context), и сравнить commits. Компоненты с долгим временем рендера подсвечиваются, что помогает найти затор.

25.3.2. Какие подходы и техники можно использовать для оптимизации производительности компонентов React, используя React и Redux DevTools?

Техники включают: мемоизацию компонентов (React.memo), оптимизацию селекторов Redux (Reselect), избежание лишних ререндеров через useMemo/useCallback, lazy loading компонентов, и использование shouldComponentUpdate/PureComponent. DevTools помогают идентифицироввать проблемы через profiling и отслеживание обновлений.

25.3.3. Какие инструменты в React DevTools помогают анализировать рендер компонентов и выявлять неэффективные обновления?

Вкладка “Profiler” показывает flamegraph и ранжированные диаграммы рендеров, подсвечивает компоненты с медленным рендерингом. Настройка “Highlight updates” подсвечивает компоненты при обновлении, показывая ненужные ререндеры. Также можно включить “Record why each component rendered” для точного определения причин обновлений (props, state, hooks).

This post is licensed under CC BY 4.0 by the author.