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

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

Вот упрощенный пример того, что я строил, с некоторыми котятами кода состояния HTTP, попробуйте сами в этой песочнице кода.

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

import "./styles.css";
import { useState } from "react";
import { Sidebar } from "./Sidebar";
export default function App() {
  const statusCodes = [500, 404, 403, 401, 418, 420, 301, 302, 200, 201, 204];
  const [selectedCode, setSelectedCode] = useState(null);
  const Codes = (
    <ul>
      {statusCodes.map((code) => (
        <li key={code}>
          <button onClick={() => setSelectedCode(code)}>{code}</button>
        </li>
      ))}
    </ul>
  );
  return (
    <div className="App">
      <h1>HTTP Status Cats</h1>
      {Codes}
      <Sidebar
        onClose={() => setSelectedCode(null)}
        ariaLabel={`${selectedCode} status code info`}
        open={Boolean(selectedCode)}
      >
        <h2>{selectedCode}</h2>
        <img
          alt={`Cat demonstrating HTTP status code: ${selectedCode}`}
          src={`https://http.cat/${selectedCode}.jpg`}
        />
      </Sidebar>
    </div>
  );
}

И боковая панель, где происходит «краже/восстановление фокуса»:

import { useEffect, useRef, useState } from "react";
export const Sidebar = ({ ariaLabel, open, onClose, children }) => {
  const [previousFocus, setPreviousFocus] = useState();
  // now focus inside something, for arguments sake, the close button
  const closeBtnRef = useRef(null);
  useEffect(() => {
    if (open) {
      setPreviousFocus(document.activeElement);
      closeBtnRef?.current?.focus();
    }
    // bit of a hack putting aria label in here so triggers if another option selected.
  }, [open, ariaLabel, closeBtnRef]);
  return (
    <aside aria-label={ariaLabel} aria-hidden={open ? "false" : "true"}>
      <button
        disabled={!open}
        ref={closeBtnRef}
        onClick={() => {
          // restore previous focus
          previousFocus?.focus();
          onClose();
        }}
      >
        Close X
      </button>
      {open && children}
    </aside>
  );
};

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

На самом деле модульный тест, который проверял взаимодействие фокуса, начал давать сбой, что на самом деле довольно круто! Библиотека тестирования React продвинула модульное тестирование НАМНОГО вперед по сравнению с Enzyme, и это здорово. Если бы он мог работать по умолчанию только в браузере, а не в Jest, это было бы здорово. По умолчанию я имею в виду, что это обычное дело в отрасли, я уверен, что это достижимо.

В моем примере выше новый PR добавил эквивалент переменной Codes, которая находится в приведенном выше фрагменте:

const Codes = (
  <ul>
    {statusCodes.map((code) => (
      <li key={code}>
        <button onClick={() => setSelectedCode(code)}>{code}</button>
      </li>
    ))}
  </ul>
);
<h1>HTTP Status Cats</h1>;
{
  Codes;
}

За исключением того, что это было не то, что было добавлено, это было:

const Codes = () => (
  <ul>
    {statusCodes.map((code) => (
      <li key={code}>
        <button onClick={() => setSelectedCode(code)}>{code}</button>
      </li>
    ))}
  </ul>
);
<h1>HTTP Status Cats</h1>;
{
  <Codes />;
}

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

Вот неработающий пример: https://codesandbox.io/s/http-status-cats-broken-fiu72?file=/src/App.jsx:508-554

Здесь происходит то, что React рендерит содержимое компонента «Приложение» при каждом рендеринге, и, поскольку внутренний компонент не запоминается или в любом случае, react просто выбрасывает его в корзину и повторно рендерит. Внутри это приведет к удалению и повторному добавлению элемента DOM, что нарушит состояние фокуса, возвращаемое исходной кнопке.

Увидев это в коде, это было неочевидным исправлением, особенно при просмотре кода другого человека, это заставило меня задуматься о некоторых вещах:

  • Могли бы мы вообще поймать это, если бы у нас не было хороших тестов?
  • Смогли бы мы когда-нибудь найти причину этого несколько месяцев спустя, когда код не был так свеж в памяти?
  • Если React — это «просто Javascript», почему он так сильно нарушает одну из лучших функций JavaScript — вложенность функций и создание замыканий.
  • Почему это не передний план документации React и правил lint по умолчанию?

Я немного покопался в последних двух пунктах:

Единственная ссылка на вложенные функции, которую я смог найти в официальной документации, взята из старой классической страницы Правила использования хуков: Don’t call Hooks inside loops, conditions, or nested functions, хотя о вложенных компонентах не упоминается.

Кроме того, мне нравится тот факт, что механизм, на котором сейчас React в основном полностью основан, «хуки», требует списка правил, которые помогут вам не писать «Просто JavaScript».

Что касается правил lint, похоже, есть одно, которое вы можете включить в популярном eslint-plugin-reactno-unstable-nested-components, возможно, я предложу своей команде сделать это. Я не могу придумать вескую причину, по которой вы хотели бы вложить функциональный компонент, даже если вы проявляете фантазию и используете useMemo, конечно, вам лучше написать более простой код.

Я нахожу забавным думать, что такое крошечное изменение в коде, который выглядит таким невинным, может сильно сломать рендеринг компонентов React, и что я узнал из этого, так это то, что я действительно сделаю все возможное, чтобы убедиться, что я пишу хорошие наборы модульных тестов, которые проверяют как «взаимодействия», так и «доступность», поскольку эти вещи так легко регрессируют!

Если вам понравилось это, вам также может быть интересно прочитать:



Спасибо за чтение! Если вы хотите узнать больше о моих работах, подписывайтесь на меня в Medium или Twitter @griffadev, или принесите мне кофе, если хотите ☕.