Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: TypeScript та інструменти

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

12 питань

Props компонента типізують звичайним типом чи інтерфейсом у параметрі функції:

type ButtonProps = {
  label: string;
  variant?: 'primary' | 'secondary';   // необов'язковий
  onClick: () => void;
};

function Button({ label, variant = 'primary', onClick }: ButtonProps) {
  return <button className={variant} onClick={onClick}>{label}</button>;
}

Значення за замовчуванням задають деструктуризацією - окремий defaultProps для функціональних компонентів у React 19 більше не підтримується.

children оголошують явно - як і будь-який інший prop:

import type { ReactNode } from 'react';

type CardProps = {
  title: string;
  children: ReactNode;
};

ReactNode чи ReactElement:

  • ReactNode - усе, що React уміє відрендерити: JSX, рядки, числа, bigint, null, undefined, boolean, масиви цього, а також проміси (для use). Правильний тип для children у більшості випадків;
  • ReactElement - лише результат JSX (<div />, <Card />). Рядок чи null туди не передати. Доречний, коли компонент справді приймає рівно один елемент (наприклад, щоб клонувати його чи додати props).
<Card title="Профіль">Текст</Card>          // ReactNode - ок
<Tooltip content="Підказка"><button /></Tooltip>   // children: ReactElement

React.JSX.Element - тип, який повертає JSX-вираз. У React 19 глобального простору імен JSX більше немає: пишуть React.JSX.Element або імпортують JSX з react. Тип повернення компонента зазвичай не вказують - TypeScript виведе його сам.

React.FC у сучасному коді вживають рідко: він не додає користі порівняно з типізованим параметром, а історично неявно додавав children. Звичайна функція з типізованими props - стандарт.

Пастка: children: JSX.Element забороняє передати текст чи кілька елементів - помилка компіляції там, де її не очікують.

Докладніше в документації: TypeScript у React: типізація children

React передає в обробники синтетичні події - обгортки над подіями браузера з однаковою поведінкою в усіх браузерах. Для кожного виду події є свій тип, параметризований елементом.

Інлайн-обробник типізувати не треба - TypeScript виведе тип сам:

<input onChange={(event) => setName(event.target.value)} />
// event: React.ChangeEvent<HTMLInputElement>

Окрема функція потребує явного типу:

import type { ChangeEvent, FormEvent, MouseEvent } from 'react';

function handleChange(event: ChangeEvent<HTMLInputElement>) {
  setEmail(event.target.value);
}

function handleSubmit(event: FormEvent<HTMLFormElement>) {
  event.preventDefault();
}

function handleClick(event: MouseEvent<HTMLButtonElement>) {
  console.log(event.clientX);
}

Найчастіші типи: ChangeEvent, FormEvent, MouseEvent, KeyboardEvent, FocusEvent, DragEvent, ClipboardEvent.

Тип самого обробника - зручно для props:

type SearchProps = {
  onSearch: (query: string) => void;                       // власний колбек
  onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>; // обробник DOM-події
};

KeyboardEventHandler<T> - це (event: KeyboardEvent<T>) => void.

Як дізнатися правильний тип: навести курсор на onChange в JSX - редактор покаже очікувану сигнатуру.

target чи currentTarget:

  • currentTarget - елемент, на якому висить обробник, і його тип відомий (HTMLButtonElement);
  • target - елемент, де виникла подія (може бути вкладений <span> усередині кнопки), тому для MouseEvent він має загальний тип EventTarget.

Для полів введення event.target.value працює, бо в ChangeEvent<HTMLInputElement> тип target уточнено.

Пастки:

  • власні колбеки не варто типізувати як події DOM: onSelect: (event) => ... з передачею всього об'єкта події змушує батька знати про DOM. Краще передавати значення: onSelect(item.id);
  • any для події вимикає перевірку доступу до полів - друкарська помилка event.target.vaule пройде непоміченою.

Докладніше в документації: TypeScript у React: події DOM

TypeScript виводить тип стану з початкового значення, і найчастіше явний тип не потрібен:

const [count, setCount] = useState(0);          // number
const [name, setName] = useState('');           // string
const [open, setOpen] = useState(false);        // boolean

Коли виведення не спрацьовує:

1. Початкове null - значення з'явиться пізніше:

type User = { id: number; name: string };

const [user, setUser] = useState<User | null>(null);

if (user) {
  user.name;   // після перевірки - User
}

Без явного типу useState(null) дасть тип null, і setUser(data) не скомпілюється.

2. Порожній масив:

const [items, setItems] = useState<Product[]>([]);

Без параметра useState([]) дає never[] - у такий масив нічого не додати.

3. Обмежений набір значень (union):

type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');

Без явного типу стан стане string, і setStatus('sucess') з помилкою пройде.

Стани, що залежать один від одного, краще поєднати в один з дискримінованим union - тоді неможливі комбінації не скомпілюються:

type RequestState =
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: string };

const [state, setState] = useState<RequestState>({ status: 'loading' });

if (state.status === 'success') {
  state.data;   // доступно лише тут
}

Окремі isLoading, error, data дозволяють стан «завантажується, але з помилкою й даними», а union - ні.

Функція оновлення типізується автоматично: setCount((c) => c + 1) знає, що c - число.

Лінива ініціалізація теж виводить тип з результату: useState(() => readFromStorage()).

Пастка: useState<User>({} as User) - «обіцянка» компілятору, що порожній об'єкт - повноцінний User. Помилки звернення до відсутніх полів з'являться під час виконання, а не компіляції. Чесніше User | null.

Докладніше в документації: TypeScript у React: useState

<StrictMode> - обгортка, яка в режимі розробки вмикає додаткові перевірки. У продакшен-збірці вона нічого не робить і нічого не коштує.

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

Шаблони Vite і більшості фреймворків вмикають її за замовчуванням.

Що вона робить:

1. Рендерить компоненти двічі. Функція компонента, ініціалізатори useState, функції useMemo та оновлювачі стану викликаються по два рази. Якщо результат різний - компонент нечистий: змінює щось зовнішнє під час рендеру.

function List({ items }) {
  items.push({ id: 'extra' });   // мутація props - у StrictMode побачите два 'extra'
  return items.map(...);
}

2. Запускає ефекти двічі при монтуванні: setup → cleanup → setup. Так React перевіряє, що ефект правильно прибирає за собою:

useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();
  return () => connection.disconnect();   // без цього - два з'єднання
}, [roomId]);

Якщо ефект ламається від повторного запуску, він зламається й у реальному житті - при поверненні на сторінку, швидкому оновленні (Fast Refresh) чи в майбутніх можливостях React, що зберігають стан при прихованні компонентів.

3. Перевіряє колбеки ref так само: прив'язка - відв'язка - прив'язка.

4. Попереджає про застарілі API.

Типові «симптоми» в розробці:

  • console.log у компоненті друкує двічі (React DevTools можуть приглушувати друге виведення);
  • запит у useEffect йде двічі - це нормально в розробці. Якщо він не скасовується й дає дублікати даних (два однакові записи через POST в ефекті), проблема в коді, а не в StrictMode;
  • лічильник, збільшений в ефекті без очищення, показує 2.

Чого НЕ робити: вимикати StrictMode, щоб «прибрати подвійний запит». Правильне рішення - очищення в ефекті, скасування запиту (AbortController) або завантаження даних засобами фреймворку чи бібліотекою на кшталт TanStack Query.

Додатково: React Compiler і правила ESLint eslint-plugin-react-hooks знаходять ті самі порушення чистоти ще до запуску.

Докладніше в документації: StrictMode

useRef має два різні призначення, і з TypeScript це видно в типах.

1. Посилання на DOM-елемент:

const inputRef = useRef<HTMLInputElement>(null);
// тип: RefObject<HTMLInputElement | null>

useEffect(() => {
  inputRef.current?.focus();   // до монтування - null, тому ?.
}, []);

return <input ref={inputRef} />;

Тип елемента - відповідний клас DOM: HTMLInputElement, HTMLDivElement, HTMLCanvasElement. Якщо вказати не той (HTMLDivElement для <input>), TypeScript повідомить про помилку в атрибуті ref.

2. Змінне значення, що не викликає рендер:

const timerId = useRef<number | null>(null);
const renders = useRef(0);   // RefObject<number>, тип виведено

function start() {
  timerId.current = window.setInterval(tick, 1000);
}

Що змінилося в типах React 19:

  • useRef вимагає аргумент. useRef<number>() без початкового значення - помилка компіляції; треба явно useRef<number | undefined>(undefined);
  • current завжди можна змінювати. Раніше useRef<T>(null) повертав RefObject з current лише для читання, а useRef<T>(initial) - MutableRefObject, і плутанина між ними давала незрозумілі помилки. Тепер є один RefObject<T> зі змінним current;
  • MutableRefObject оголошено застарілим.

window.setInterval, а не setInterval: у проєктах, де є і типи DOM, і типи Node.js, глобальний setInterval повертає NodeJS.Timeout, а не число. Явний window. прибирає конфлікт, або тип ref - ReturnType<typeof setInterval>.

Колбек-ref з очищенням (React 19):

<div ref={(node) => {
  if (!node) return;
  const observer = new ResizeObserver(onResize);
  observer.observe(node);
  return () => observer.disconnect();   // функція очищення
}} />

У TypeScript колбек-ref тепер не може неявно повертати значення: ref={(node) => (instance = node)} - помилка, бо присвоєння повертає значення, яке React вважав би функцією очищення. Потрібні фігурні дужки.

Правило з документації: не читати й не змінювати ref.current під час рендеру (крім лінивої ініціалізації) - лише в обробниках подій та ефектах.

Докладніше в документації: useRef

До React 19 функціональний компонент не отримував ref як prop - для цього обгортали компонент у forwardRef:

const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});

У React 19 ref - звичайний prop:

import type { ComponentProps } from 'react';

type InputProps = ComponentProps<'input'> & {
  label: string;
};

function Input({ label, ref, ...props }: InputProps) {
  return (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  );
}
const emailRef = useRef<HTMLInputElement>(null);
<Input label="Email" type="email" ref={emailRef} required />;

forwardRef ще працює, але документація позначає його як непотрібний для нового коду, і в майбутніх версіях його оголосять застарілим.

ComponentProps<'input'> - усі атрибути нативного <input>, включно з ref, обробниками подій, aria-* і data-*. Обгортка автоматично приймає все, що приймає нативний елемент, без ручного перелічення.

Варіанти утилітних типів:

  • ComponentProps<'button'> - атрибути разом з ref (у React 19 для нативних елементів);
  • ComponentPropsWithoutRef<'button'> - те саме без ref, коли обгортка свідомо не передає ref далі;
  • ComponentProps<typeof Button> - props іншого компонента. Корисно, щоб розширити чужий компонент чи переиспользувати його типи без експорту.

Перевизначення атрибута: якщо власний prop збігається з нативним, але має інший тип, нативний треба прибрати:

type SelectProps = Omit<ComponentProps<'select'>, 'onChange'> & {
  onChange: (value: string) => void;
};

Без Omit типи перетнуться, і TypeScript вимагатиме обробник, сумісний з обома сигнатурами одночасно.

Пастки:

  • розгорнути ...props до свого className - і переданий ззовні клас перезапише внутрішній. Порядок і злиття класів (clsx, tailwind-merge) треба продумати;
  • бібліотеки, що підтримують React 18, досі використовують forwardRef, і це нормально - для них потрібна сумісність з обома версіями.

Докладніше в документації: forwardRef

useReducer найкраще поєднується з дискримінованим union дій: поле type визначає, які ще поля має дія.

type CartItem = { id: number; title: string; price: number; qty: number };

type CartState = {
  items: CartItem[];
  coupon: string | null;
};

type CartAction =
  | { type: 'added'; item: CartItem }
  | { type: 'removed'; id: number }
  | { type: 'quantityChanged'; id: number; qty: number }
  | { type: 'couponApplied'; code: string }
  | { type: 'cleared' };

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'added':
      return { ...state, items: [...state.items, action.item] };
    case 'removed':
      return { ...state, items: state.items.filter((i) => i.id !== action.id) };
    case 'quantityChanged':
      return {
        ...state,
        items: state.items.map((i) => (i.id === action.id ? { ...i, qty: action.qty } : i)),
      };
    case 'couponApplied':
      return { ...state, coupon: action.code };
    case 'cleared':
      return { items: [], coupon: null };
    default: {
      const unreachable: never = action;
      return state;
    }
  }
}

const [cart, dispatch] = useReducer(cartReducer, { items: [], coupon: null });

Що дає типізація:

  • у кожній гілці switch TypeScript звужує тип: у case 'removed' доступне action.id, а action.item - помилка;
  • dispatch перевіряє дії: dispatch({ type: 'removed' }) без id чи з неіснуючим type не скомпілюється;
  • перевірка повноти: присвоєння never у default дає помилку компіляції, якщо додати новий тип дії й забути обробити його в редьюсері.

Тип стану й дій виводиться з функції-редьюсера, тож параметри useReducer вказувати явно не треба. Явна анотація повернення CartState у редьюсері ловить випадки, коли гілка повертає щось не те.

Назви дій - у минулому часі («що сталося»: added, couponApplied), як радить документація React, а не команди (ADD_ITEM).

Лінива ініціалізація - третій аргумент: useReducer(cartReducer, userId, createInitialCart), де createInitialCart(userId) повертає початковий стан. Тип аргументу перевіряється.

Пастка: мутувати стан у редьюсері (state.items.push(...)) TypeScript не заборонить - для гарантій незмінності типи можна оголосити як readonly (readonly CartItem[]), або використати Immer (useImmerReducer).

Докладніше в документації: TypeScript у React: useReducer

createContext потребує значення за замовчуванням. Для контексту з реальними даними (поточний користувач, кошик) осмисленого значення за замовчуванням немає, тому часто пишуть null:

type AuthContextValue = {
  user: User;
  logout: () => void;
};

const AuthContext = createContext<AuthContextValue | null>(null);

Тепер кожен useContext(AuthContext) повертає AuthContextValue | null, і в кожному компоненті потрібна перевірка. Набридливо - і перевірка однаково нічого не робить, бо провайдер завжди є.

Рішення - власний хук з перевіркою в одному місці:

export function useAuth(): AuthContextValue {
  const context = useContext(AuthContext);
  if (context === null) {
    throw new Error('useAuth треба викликати всередині <AuthProvider>');
  }
  return context;
}
function Header() {
  const { user, logout } = useAuth();   // AuthContextValue без null
  return <button onClick={logout}>Вийти, {user.name}</button>;
}

Переваги:

  • тип без null у компонентах;
  • зрозуміла помилка одразу, якщо компонент випадково відрендерили поза провайдером (замість Cannot read properties of null десь далі);
  • контекст можна не експортувати - лише хук і провайдер. Так менше способів використати його неправильно.

Провайдер разом із логікою:

export function AuthProvider({ user, children }: { user: User; children: ReactNode }) {
  const logout = useCallback(() => router.post('/logout'), []);
  const value = useMemo(() => ({ user, logout }), [user, logout]);
  return <AuthContext value={value}>{children}</AuthContext>;
}

У React 19 контекст можна рендерити напряму як провайдер - <AuthContext value={...}> замість <AuthContext.Provider value={...}>.

Альтернатива, коли розумне значення за замовчуванням є (тема, мова): createContext<Theme>('light') - перевірки на null не потрібні взагалі.

Пастка: createContext<AuthContextValue>(null!) чи {} as AuthContextValue - «заглушка» для компілятора. TypeScript замовкне, але компонент поза провайдером отримає null чи порожній об'єкт і впаде з незрозумілою помилкою під час виконання.

Докладніше в документації: TypeScript у React: useContext

Компоненти на кшталт списку, таблиці чи випадного списку працюють з даними будь-якого типу. Без узагальнень доводиться писати any - і зв'язок між даними та колбеками губиться.

Узагальнений компонент:

type ListProps<T> = {
  items: T[];
  getKey: (item: T) => string | number;
  renderItem: (item: T) => ReactNode;
  onSelect?: (item: T) => void;
};

function List<T>({ items, getKey, renderItem, onSelect }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={getKey(item)} onClick={() => onSelect?.(item)}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}
<List
  items={users}                      // T = User - виведено з items
  getKey={(user) => user.id}
  renderItem={(user) => user.name}   // user: User
  onSelect={(user) => open(user.id)}
/>

TypeScript виводить T з переданих даних, і всі колбеки отримують правильний тип. Звернення до неіснуючого поля - помилка компіляції.

Обмеження типу параметра:

function Table<T extends { id: number }>({ rows, columns }: TableProps<T>) { /* ... */ }

Тепер можна використовувати row.id всередині й не передавати getKey.

Колонки, прив'язані до полів даних:

type Column<T> = {
  key: keyof T;
  header: string;
  render?: (value: T[keyof T], row: T) => ReactNode;
};

key: keyof T дозволяє лише існуючі поля - перейменування поля в моделі одразу підсвітить усі таблиці, де воно використовується.

Синтаксис у .tsx: стрілкова функція const List = <T,>(props: ListProps<T>) => ... потребує коми після T - інакше <T> прочитається як JSX-тег. Оголошення function List<T>(...) цієї проблеми не має.

Явний параметр можна передати в JSX, якщо виведення неможливе: <List<User> items={[]} ... />.

Пастки:

  • memo чи forwardRef губить узагальнення: memo(List) повертає компонент з T = unknown. Рішення - приведення типу (memo(List) as typeof List) або відмова від обгортки (з React Compiler потреба в ручному memo зменшується);
  • надто складні узагальнення (умовні типи, кілька параметрів) погіршують повідомлення про помилки - для команди простіший тип часто корисніший за максимально точний.

Докладніше в документації: Узагальнені типи (Generics)

Звичайні необов'язкові props дозволяють будь-які комбінації, навіть безглузді:

type ButtonProps = {
  href?: string;          // посилання
  onClick?: () => void;   // кнопка
  external?: boolean;     // має сенс лише з href
};

<Button onClick={save} external />   // компілюється, хоча безглуздо
<Button />                            // ні дії, ні посилання

Дискримінований union описує кожен допустимий варіант окремо:

type LinkProps = { as: 'link'; href: string; external?: boolean };
type ActionProps = { as: 'button'; onClick: () => void; disabled?: boolean };

type ButtonProps = (LinkProps | ActionProps) & { children: ReactNode };

function Button(props: ButtonProps) {
  if (props.as === 'link') {
    return (
      <a href={props.href} target={props.external ? '_blank' : undefined}>
        {props.children}
      </a>
    );
  }
  return (
    <button onClick={props.onClick} disabled={props.disabled}>
      {props.children}
    </button>
  );
}
<Button as="link" href="/jobs">Вакансії</Button>
<Button as="button" onClick={save}>Зберегти</Button>
<Button as="button" href="/x">...</Button>   // помилка: href не існує для button

Варіант без явного дискримінатора - через never, щоб props взаємно виключали один одного:

type Props =
  | { value: string; defaultValue?: never; onChange: (v: string) => void }   // керований
  | { defaultValue?: string; value?: never; onChange?: never };              // некерований

Передати одночасно value і defaultValue неможливо - типова помилка з полями форм.

Типові застосування:

  • керований / некерований компонент;
  • компонент стану запиту: { status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; data: T };
  • модальне вікно з різними наборами кнопок;
  • поле форми, де options обов'язкові лише для типу select.

Пастки:

  • деструктуризація в параметрах губить звуження: function Button({ as, href, onClick }) - href не існує у варіанті button, TypeScript не дасть деструктуризувати. Звужувати треба по props.as, а деструктуризувати вже всередині гілки;
  • ...rest з union дає перетин полів і може «загубити» варіант - передаючи props далі, краще явно перелічувати;
  • повідомлення про помилки для великих union стають довгими - варто тримати варіантів небагато й давати їм імена.

Докладніше в документації: Звуження типів: дискриміновані union

Створення проєкту:

npm create vite@latest my-app -- --template react-ts

Шаблон містить vite.config.ts з плагіном @vitejs/plugin-react, tsconfig для коду застосунку й окремо для конфігурації, ESLint з eslint-plugin-react-hooks.

Що робить @vitejs/plugin-react:

  • перетворює JSX (автоматичний runtime - імпорт React у файлах не потрібен);
  • Fast Refresh - зміна компонента оновлює лише його, зі збереженням стану. Працює, якщо файл експортує лише компоненти: експорт констант чи функцій поряд з компонентом змушує перезавантажувати модуль повністю;
  • у Vite 8 перетворення робить швидкий компілятор на Rust (Oxc), Babel за замовчуванням не потрібен.

React Compiler у поточній версії плагіна підключається двома способами:

// 1. через Babel - стабільний варіант
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';

export default defineConfig({
  plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
});

// 2. нативний порт на Rust - експериментальний
export default defineConfig({
  plugins: [react({ compiler: true })],
});

Компілятор 1.0 стабільний, працює з React 17+ і додає мемоізацію на етапі збирання. Для поступового впровадження - compilationMode: 'annotation' (компілюються лише функції з директивою "use memo").

TypeScript важливо розуміти правильно:

  • Vite не перевіряє типи - він лише видаляє їх під час перетворення. Збирання пройде навіть з помилками типів;
  • перевірка - окремим кроком: tsc -b у скрипті build (так у шаблоні) і в CI, а під час розробки - редактор чи vite-plugin-checker;
  • "jsx": "react-jsx", "moduleResolution": "bundler", "strict": true, "noUncheckedIndexedAccess" - корисні налаштування для нового проєкту;
  • "isolatedModules": true потрібен, бо кожен файл перетворюється окремо: export type для реекспорту типів.

Змінні оточення - лише з префіксом VITE_, через import.meta.env; типи - в vite-env.d.ts.

ESLint з eslint-plugin-react-hooks (конфігурація recommended) у версії 7 містить і правила хуків, і правила React Compiler - порушення, через які компілятор пропустив би компонент, видно ще в редакторі.

З Laravel - laravel-vite-plugin поряд з плагіном React, сторінки через Inertia; стартовий набір Laravel React вже містить це налаштування.

Докладніше в документації: Vite: початок роботи

eslint-plugin-react-hooks - офіційний плагін ESLint від команди React. З версії 6-7 він містить не лише два класичні правила хуків, а й правила, побудовані на аналізі React Compiler.

// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  reactHooks.configs.flat.recommended,
]);

Класичні правила:

  • rules-of-hooks - хуки лише на верхньому рівні компонентів і власних хуків, не в умовах і циклах;
  • exhaustive-deps - усі значення, використані в useEffect/useMemo/useCallback, мають бути в залежностях.

Правила компілятора (у конфігурації recommended) ловлять порушення правил React, через які компонент працюватиме неправильно або компілятор його пропустить:

  • purity - нечисті виклики під час рендеру (Math.random(), Date.now() у тілі компонента);
  • immutability - мутація props, стану чи значень з хуків;
  • refs - читання чи запис ref.current під час рендеру;
  • set-state-in-render і set-state-in-effect - виклик setState прямо в рендері (нескінченний цикл) чи синхронно в ефекті (зайвий рендер; часто стан можна обчислити під час рендеру);
  • static-components - оголошення компонента всередині іншого компонента (він перестворюється щоразу й губить стан);
  • globals - зміна глобальних змінних під час рендеру;
  • preserve-manual-memoization - ручний useMemo/useCallback, який компілятор не може зберегти;
  • incompatible-library - бібліотеки з API, несумісним з мемоізацією компілятора;
  • error-boundaries, use-memo, unsupported-syntax, config, gating.

Конфігурація recommended-latest додає експериментальні правила.

Чому це важливо навіть без компілятора: кожне з цих порушень - справжній баг або джерело зайвих рендерів незалежно від того, чи увімкнено компілятор. А з компілятором ESLint показує, які компоненти він пропустить і чому, - ще в редакторі.

Як упроваджувати в старий проєкт:

  • увімкнути правила й подивитися кількість порушень - це оцінка, наскільки код готовий до компілятора;
  • виправляти поступово, починаючи з purity, immutability, refs - вони вказують на реальні помилки;
  • не вимикати правило коментарем без пояснення: eslint-disable для exhaustive-deps - найчастіше маскування справжньої проблеми із застарілими значеннями (stale closure).

Докладніше в документації: eslint-plugin-react-hooks