Irbisa · Шпаргалка

Хуки подробно

Правила хуков, кастомные хуки, useReducer, useContext, useMemo и useCallback, useId, useSyncExternalStore, useTransition, use

Middle-разработчик · 19 вопросов

  1. 01

    Какие есть правила хуков и зачем они нужны React?

    Хуки вызывают только на верхнем уровне функционального компонента или кастомного хука, никогда внутри условий, циклов, вложенных функций или после раннего return.

  2. 02

    Что такое кастомный хук и когда его стоит выносить?

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

    function useDebouncedValue<T>(value: T, delayMs: number): T {  const [debounced, setDebounced] = useState(value);   useEffect(() => {    const id = setTimeout(() => setDebounced(value), delayMs);    return () => clearTimeout(id);…
  3. 03

    Какую проблему решает useContext и как предоставить и прочитать контекст?

    useContext позволяет любому компоненту прочитать значение ближайшего провайдера выше по дереву без передачи пропсов через каждый промежуточный компонент (prop drilling).

    type Theme = 'light' | 'dark';const ThemeContext = createContext<Theme>('light'); function App() {  const [theme, setTheme] = useState<Theme>('dark');  return (…
  4. 04

    Когда стоит использовать useReducer вместо useState?

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

    type State = { items: string[]; draft: string };type Action =  | { type: 'typed'; text: string }  | { type: 'added' }  | { type: 'removed'; index: number };…
  5. 05

    Чем отличаются useMemo и React.memo?

    React.memo оборачивает компонент и пропускает его ререндер, если пропсы равны предыдущим.

    // React.memo: wraps a component, skips its render when props are equalconst Row = memo(function Row({ item }: { item: Item }) {  return <li>{item.name}</li>;}); function ProductList({ items, query }: { items: Item[]; query: string }) {…
  6. 06

    Как React понимает, какой вызов useState возвращает какое состояние, если у хуков нет имён и ключей?

    По порядку вызовов.

  7. 07

    Чем отличаются useMemo и useCallback и когда их стоит использовать?

    useMemo(fn, deps) кэширует результат вызова fn; useCallback(fn, deps) кэширует саму функцию и равносилен useMemo(() => fn, deps).

    const Chart = memo(function Chart({ points, onSelect }: ChartProps) {  /* expensive SVG rendering */}); function Dashboard({ rows, filter }: { rows: Row[]; filter: string }) {  const [selected, setSelected] = useState<string | null>(null);…
  8. 08

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

    Когда провайдер получает новое value (сравнение через Object.is), React ререндерит каждый компонент, который читает этот контекст, даже если он использует одно поле, и memo на потребителе это не останавливает.

  9. 09

    Найдите баг: профиль работает для гостей, но после входа страница падает с ошибкой "Rendered more hooks than during the previous render".

    function Profile() {  const user = useContext(UserContext);  if (!user) return <SignInPrompt />;   const [orders, setOrders] = useState<Order[]>([]);  useEffect(() => {    fetchOrders(user.id).then(setOrders);  }, [user.id]);   return <OrderList user={user} orders={orders} />;}

    Ранний return стоит выше useState и useEffect, поэтому рендер для гостя не вызывает после него ни одного хука, а рендер для вошедшего пользователя вызывает ещё два.

  10. 10

    Что выведется в консоль, если пользователь трижды нажмёт «+1», а потом «Log»?

    function Counter() {  const [count, setCount] = useState(0);   const log = useCallback(() => {    console.log(count);  }, []);   return (    <>      <button onClick={() => setCount(count + 1)}>+1</button>      <button onClick={log}>Log</button>    </>  );}

    Выведется 0.

  11. 11

    Как объединить useReducer и контекст для управления состоянием фичи и где этот паттерн перестаёт масштабироваться?

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

    const CartContext = createContext<CartState | null>(null);const CartDispatchContext = createContext<Dispatch<CartAction> | null>(null); export function CartProvider({ children }: { children: React.ReactNode }) {  const [cart, dispatch] = useReducer(cartReducer, { lines: [] });  return (…
  12. 12

    Для чего нужен useId и почему не генерировать id через Math.random() или счётчик в модуле?

    useId() возвращает уникальную строку-идентификатор, которая стабильна всё время жизни компонента и совпадает на сервере и на клиенте, поэтому ею безопасно связывать <label> с <input> или aria-describedby с подсказкой в HTML, отрендеренном на сервере.

    function EmailField() {  const id = useId();  return (    <div>      <label htmlFor={`${id}-email`}>Email</label>      <input id={`${id}-email`} type="email" aria-describedby={`${id}-hint`} />…
  13. 13

    Что делают useTransition и useDeferredValue и когда выбирать каждый?

    Оба помечают обновление как несрочное, чтобы React отрендерил его в фоне и прервал ради срочного ввода вроде набора текста.

    function Search({ items }: { items: Item[] }) {  const [query, setQuery] = useState('');  const deferredQuery = useDeferredValue(query); // lags behind while rendering  const isStale = query !== deferredQuery;   return (…
  14. 14

    Что такое API use в React 19 и чем он отличается от других хуков?

    use(resource) читает промис или контекст во время рендера.

    // PostPage.tsx, a Server Component: starts the request, does not await itexport default function PostPage({ postId }: { postId: string }) {  const commentsPromise = fetchComments(postId);  return (    <Suspense fallback={<Spinner />}>      <Comments commentsPromise={commentsPromise} />…
  15. 15

    Как написать хук useDebounce для поля поиска?

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

    // debounced value: the usual hook for a search inputfunction useDebouncedValue<T>(value: T, delayMs: number): T {  const [debounced, setDebounced] = useState(value);  useEffect(() => {    const timer = setTimeout(() => setDebounced(value), delayMs);    return () => clearTimeout(timer); // the next keystroke cancels the pending update…
  16. 16

    Как написать хук useLocalStorage и как синхронизировать его между вкладками и сделать безопасным при серверном рендеринге?

    Считайте localStorage внешним хранилищем и читайте его через useSyncExternalStore: подпишитесь на событие storage, чтобы получать записи из других вкладок, отправляйте своё событие для записей в этой вкладке и возвращайте null из серверного снимка, чтобы серверный рендеринг никогда не обращался к window.

    function subscribe(callback: () => void) {  window.addEventListener('storage', callback);       // writes from other tabs  window.addEventListener('local-storage', callback); // writes from this tab  return () => {    window.removeEventListener('storage', callback);    window.removeEventListener('local-storage', callback);…
  17. 17

    Что такое useSyncExternalStore и почему внешние сторы должны использовать его, а не useState плюс useEffect?

    useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) подписывает компонент на стор, который живёт вне React (API браузера, стор на уровне модуля), и читает его так, что это безопасно при конкурентном рендеринге.

    function subscribe(callback: () => void) {  window.addEventListener('online', callback);  window.addEventListener('offline', callback);  return () => {    window.removeEventListener('online', callback);    window.removeEventListener('offline', callback);…
  18. 18

    Найдите баг: бейдж корзины подвешивает вкладку с ошибкой "The result of getSnapshot should be cached to avoid an infinite loop".

    function useCartSummary() {  return useSyncExternalStore(cartStore.subscribe, () => ({    count: cartStore.getState().items.length,    total: cartStore.getState().total,  }));} function CartBadge() {  const { count, total } = useCartSummary();  return <span>{count} items, ${total}</span>;}

    getSnapshot создаёт новый объект на каждый вызов, поэтому React при каждой проверке видит другой снимок, решает, что стор изменился, ререндерит и снова проверяет, и так бесконечно.

  19. 19

    Как спроектировать API переиспользуемого кастомного хука, чтобы им было удобно пользоваться и он не вызывал лишних ререндеров?

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

    interface UseDisclosureOptions {  defaultOpen?: boolean;  onOpenChange?: (open: boolean) => void;} export function useDisclosure({ defaultOpen = false, onOpenChange }: UseDisclosureOptions = {}) {…