В данной статье рассматриваются особенности, которые связаны с обработкой нативных событий (native events) в React-приложениях. Существует проблема частичной потери контекста функционального компонента при обработке нативных событий, которые навешиваются на элементы с помощью глобальных объектов document, window или через ссылки (refs). В статье рассматривается данная проблема и предлагается способ её решения (один из вариантов).

Мотивация

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

Я достаточно часто сталкиваюсь с необходимостью обрабатывать нативные события и до определённого времени делал это не правильно, что приводило к ошибкам. На удивление я нашёл не много информации, которая помогает разобраться с такими ошибками и ещё меньше - объясняющие их природу и почему всё работает так, а не иначе.

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

Используемая версия React.js, Node.js и других зависимостей

Версия React.js - 18.2.0.

Версия Node.js - 16.20.2.

Подробный список dependencies тестового проекта:

"dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
}

Синтетические и нативные события

Для начала разберёмся что вообще такое синтетическое событие в контексте React.js.

Синтетическое событие - это кроссбраузерная обёртка SyntheticEvent, которая предназначена для унификации работы с событиями в DOM-дереве.

Синтетические события являются крайне полезными при разработке веб-приложений, которые ориентированы на множество браузеров (поддержка кроссбраузерности). Благодаря им гарантируется (насколько это возможно), что ваше веб-приложение будет одинаково обрабатывать события в Opera, Google, Safari, Mozilla Firefox, Yandex и других популярных браузерах. Также они позволяют избежать утечек памяти благодаря автоматическому управлению памятью (чего нет при обработке нативных событий), что делает синтетические события ещё более полезными.

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

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

// ...

return (
    <>
      <div
        className="wrapper"
        onMouseLeave={(e) => {
          // Обработка синтетического события ...
        }}
      >
        <div
          id="containerId"
          className="container"
          onDoubleClick={(e) => {
            // Обработка синтетического события ...
          }}
        >
          <span
            onClick={(e) => {
              // Обработка синтетического события ...
            }}
          >{value}</span>
        </div>
      </div>
    </>
);

// ...

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

Пояснения по объекту event

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

Вот код, который описывает получение и вывод этого объекта на экран (на выходе получим объект класса SyntheticBaseEvent):

// ...

<div className="wrapper">
    <div
        id="containerId"
        className="container"
        onClick={(e) => {
          // Вывод объекта класса SyntheticBaseEvent
          console.log(e);
        }}
      >
       <span>{value}</span>
    </div>
</div>

// ...

В обработчик обычно передаётся более короткая форма параметра event - e.

Рисунок 1 - "Все мы разные, но служим одной цели - обработке событий!"
Рисунок 1 - "Все мы разные, но служим одной цели - обработке событий!"

С синтетическими событиями разобрались. Что же такое нативные события?

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

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

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

Нативные события, в основном, обрабатываются с помощью стандартного метода addEventListener, который вызывается у глобальных объектов window, document или не пустых (not null) ссылок (refs).

Вот пример обработки нативных событий:

// ...

useEffect(() => {
    const container = document.getElementById("containerId");

    // Навешиваем обработчики на нативные события
    container.addEventListener("click", mouseMoveHandler);
    window.addEventListener("mousedown", mouseDownHandler);

    return () => {
      // Удаляем обработчики с нативных событий, 
      // иначе они будут работать и после выхода со страницы
      window.removeEventListener("mousedown", mouseDownHandler);

      // Кроме данной строки - здесь можно и не удалять, 
      // ведь если нет DOM-элемента, то и нет проблемы :)
      container.removeEventListener("click", mouseMoveHandler);
    };
  }, []);

// ...

В общем-то с разницей между нативными (в контексте данной статьи) и синтетическими событиями разобрались.

Кстати, стоит упомянуть о том, что при обработке синтетических событий можно получить объект event, который мы бы получили при обработке нативного события. Для этого можно обратиться к атрибуту nativeEvent в объекте event.

Проблема при обработке нативного события

Далее следует ввести понятие контекста React-компонента.

Контекст React-компонента (в рамках данной статьи) - это все функции, состояния, refs, переменные и константы, которые определены в рамках одного React-компонента.

Например, контекстом React-компонента (далее - контекст или контекст компонента) является всё, что определено в следующем программном коде:

// ...

const [value, setValue] = useState(0);
const [state, setState] = useState({
    arr: []
});
const timer = useRef(null);
const handler = (e) => {
    console.log(e);
}

// ...

или:

// ...

constructor(props) {
      super(props);

      this.state = {
          value: 0
      };

      this.timer = React.createRef(null);
      this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
}

// ...

В общем, можно воспринимать контекст, как окружение компонента. В классовом компоненте его роль принимает объект this.

Теперь перейдём к проблеме.

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

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

Рисунок 2 - Это рандомайзер и он генерирует ?
Рисунок 2 - Это рандомайзер и он генерирует ?

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

Следующий код реализует такой простейший рандомайзер:

// ...

/**
 * Точка входа в React-приложение
 * @returns 
 */
const App = () => {
  // Значение, генерируемое рандомайзером
  const [value, setValue] = useState(0);
  // Зафиксированное текущее значение
  const [currentValue, setCurrentValue] = useState(0);
  // Таймер
  const timer = useRef(null);

  /**
   * Обработчик синтетического события
   * @param {*} e Объект синтетического события
   */
  const clickHandler = (e) => {
    // Установка текущего значения рандомайзера
    setCurrentValue(value);
  };

  /**
   * Обработка монтирования компонента
   */
  useEffect(() => {
    // Запуск рандомайзера
    timer.current = setInterval(() => {
      setValue(Math.random());
    }, 300);

    return () => {
      timer.current && clearInterval(timer.current);
    }
  }, []);

  return (
    <>
      <div className="wrapper">
        <div
          id="containerId"
          className="container"
          onClick={clickHandler}
        >
          <span>{value}</span>
        </div>
        <div>
          <span>{currentValue}</span>
        </div>
      </div>
    </>
  );
}

export default App;

// ...

Как можно заметить, установка текущего на момент клика по блоку значения рандомайзера устанавливается в обработчике синтетического события. Всё отлично работает.

Рисунок 3 - Всё отлично работает, правда только с синтетикой ...
Рисунок 3 - Всё отлично работает, правда только с синтетикой ...

Если же мы попробуем теперь определить тот же самый обработчик на нативное событие клика по этому же блоку (с помощью refs), то ... у нас ничего не получится.

Определение обработчика на нативное событие клика:

// ...

/**
 * Точка входа в React-приложение
 * @returns 
 */
const App = () => {
  // Значение, генерируемое рандомайзером
  const [value, setValue] = useState(0);
  // Зафиксированное текущее значение
  const [currentValue, setCurrentValue] = useState(0);
  // Таймер
  const timer = useRef(null);
  // Ссылка на контейнер (условный блок)
  const container = useRef(null);

  /**
   * Обработчик нативного события
   * @param {*} e Объект нативного события
   */
  const clickHandler = (e) => {
    // Проверка для клика (мало ли ...)
    console.log("Проверка");
    // Установка текущего значения рандомайзера
    setCurrentValue(value);
  };

  /**
   * Обработка монтирования компонента
   */
  useEffect(() => {
    // Запуск рандомайзера
    timer.current = setInterval(() => {
      setValue(Math.random());
    }, 300);

    return () => {
      timer.current && clearInterval(timer.current);
    }
  }, []);

  useEffect(() => {
    // Подписка на обработку нативного события
    container.current.addEventListener("click", clickHandler);

    return () => {
      // Отписка на обработку нативного события
      container.current.removeEventListener("click", clickHandler);
    }
  }, []);

  return (
    <>
      <div className="wrapper">
        <div
          ref={container}
          id="containerId"
          className="container"
        >
          <span>{value}</span>
        </div>
        <div>
          <span>{currentValue}</span>
        </div>
      </div>
    </>
  );
}

export default App;

// ...
Рисунок 4 - Ничего не получилось, хотя обработчик один и тот же ...
Рисунок 4 - Ничего не получилось, хотя обработчик один и тот же ...

По какой причине обработчик нативного события не смог зафиксировать текущее значение рандомайзера и отобразить его в DOM-дереве?

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

Ранее я упомянул, что обработчики синтетических событий обязательно должны находиться в определении DOM-элементов (в JSX-коде). И именно благодаря тому, что они определены в JSX-коде "синтетический" код работает правильно, а "нативный" нет. Поскольку обработчики синтетических событий захватывают контекст компонента полностью, а обработчики нативные - нет (лишь частично).

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

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

Обработчики нативных событий не захватывают состояния, но захватывают refs. Можно предположить, что здесь стоит использовать useRef, но проблема в том, что value используется в DOM-дереве, а если value это ref, то никаких перерисовок не будет.

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

Следующий код демонстрирует решение этой проблемы:

// ...

/**
   * Обработчик нативного события (c мемоизацией)
   * @param {*} e Объект нативного события
   */
  const clickHandler = useCallback((e) => {
    console.log("Проверка");
    // Установка текущего значения рандомайзера
    setCurrentValue(value);
  }, [value]);

  useEffect(() => {
    // Подписка на обработку нативного события
    container.current.addEventListener("click", clickHandler);

    return () => {
      // Отписка на обработку нативного события
      container.current.removeEventListener("click", clickHandler);
    }
  }, [clickHandler]);

// ...

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

Теперь при каждом изменении значения value внутри обработчика clickHandler будет изменятся представление о контексте компонента (в том числе и о его состояниях).

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

Таким образом переподписка происходит в цепочке - значение рандомайзера -> функция обработчик -> изменение функции обработчика (его "кэша"). Что и позволяет работать данному коду.

Рисунок 5 - Всё работает, даже спустя 5-ти попыток!
Рисунок 5 - Всё работает, даже спустя 5-ти попыток!

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

Оловянный, деревянный, class component

Примечательно, что в классовых компонентах такой проблемы нет. Совсем нет. Даже не нужна переподписка на обработку нативного события, всё и так отлично работает. И не важно с помощью какого объекта был добавлен обработчик нативного события (document, window, refs, etc.), обработка всё равно будет происходить корректно (как программист задумывал изначально).

Следующий код и сопровождающий его рисунок подтверждают мои наблюдения:

// ...

/**
 * Точка входа в React-приложение (классовый компонент)
 */
class AppClass extends React.Component {
    /**
     * Конструктор
     * @param {*} props Параметры
     */
    constructor(props) {
        super(props);

        this.state = {
            value: 0,
            currentValue: 0
        };

        this.container = React.createRef(null);
        this.timer = React.createRef(null);
        this.clickHandler = this.clickHandler.bind(this);
    }

    /**
     * Монтирование компонента
     */
    componentDidMount() {
        this.timer.current = setInterval(() => {
            this.setState({
                value: Math.random()
            });
        }, 300);

        // Подписка на обработку нативного события
        this.container.current.addEventListener("click", this.clickHandler);
    }

    /**
     * Обработка размонтирования компонента
     */
    componentWillUnmount() {
        this.timer.current && clearInterval(this.timer.current);

        // Отписка на обработку нативного события
        this.container.current.removeEventListener("click", this.clickHandler);
    }

    /**
     * Обработчик клика
     */
    clickHandler() {
        const { value } = this.state;
        console.log("Я в классовом компоненте!");

        this.setState({
            currentValue: value
        });
    };

    render() {
        const { value, currentValue } = this.state;

        return (
            <>
                <div className="wrapper">
                    <div
                        ref={this.container}
                        id="containerId"
                        className="container"
                    >
                        <span>{value}</span>
                    </div>
                    <div>
                        <span>{currentValue}</span>
                    </div>
                </div>
            </>
        );
    }
}

export default AppClass;

// ...

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

Рисунок 6 - А в классовом компоненте всё работает ...
Рисунок 6 - А в классовом компоненте всё работает ...

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

// ...

/**
  * Обработчик клика без this
  */
clickHandler = () => {
    const { value } = this.state;
    console.log("Я в классовом компоненте!");

    this.setState({
        currentValue: value
    });
  };

// ...

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

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

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

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

Заключение

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

Была рассмотрена проблема отсутствия актуальных значений состояний (state) в обработчике нативного события и продемонстрирован способ решения данной проблемы с помощью мемоизации и переподписке на нативные события.

Список использованных источников

  1. Введение в React, которого нам не хватало.

  2. How to (really) remove eventListeners in React.

  3. Event listener functions changing when using React hooks.

Комментарии (10)


  1. fransua
    24.04.2024 20:12
    +1

    Дело тут, конечно, не в нативных/синтетических событиях, а в списке зависимостей useEffect. В первом случае Вы создаете clickHandler при каждом рендере и удаляете/добавляете нативный обработчик (внутри onclick={clickHandler}) Аналогичный код с нативными событиями будет, если в useEffect убрать [] и выполнять его на каждый render.


    1. dan_sw Автор
      24.04.2024 20:12

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

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

      Пример с useEffect без массива зависимостей тут не очень подходит, т.к. переподписка на события будет при вообще любом рендере (необязательно при изменении стейта value). В общем-то можно использовать в массиве зависимостей и просто стейт value:

      // ...
      
      useEffect(() => {
          // Подписка на обработку нативного события
          container.current.addEventListener("click", clickHandler);
      
          return () => {
            // Отписка на обработку нативного события
            container.current.removeEventListener("click", clickHandler);
          }
        }, [value]);
      
      // ...

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


      1. fransua
        24.04.2024 20:12
        +1

        Я про первый кейс с синтетическими событиями

         /**
           * Обработчик синтетического события
           * @param {*} e Объект синтетического события
           */
          const clickHandler = (e) => {
            // Установка текущего значения рандомайзера
            setCurrentValue(value);
          };
        

        Тут мемоизации нет, поэтому value актуальный. Если тут добавить useCallback(..., []) то возникнет та же проблема - value будет старым. И точно так же, если добавить value в deps все станет хорошо.


  1. profesor08
    24.04.2024 20:12

    События тут вообще ни при чем. Тут все упирается в непонимание того как работает компонент. При каждом его обновлении, вызывается его функция, и все, что объявлено в этой функции будет заново объявлено. Все ссылки станут новыми, все старые ссылки никуда не делись, и продолжают висеть в памяти. В примере из статьи, этой самой ссылкой является clickHandler . Следовательно, надо вручную отследить, отписать от неактуальной и подписать на актуальную. Сделать все то, что делает сам реакт под капотом.


    1. dan_sw Автор
      24.04.2024 20:12

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

      Ранее я упомянул, что обработчики синтетических событий обязательно должны находиться в определении DOM-элементов (в JSX-коде). И именно благодаря тому, что они определены в JSX-коде "синтетический" код работает правильно, а "нативный" нет. Поскольку обработчики синтетических событий захватывают контекст компонента полностью, а обработчики нативные - нет (лишь частично).

      В целом то, что Вы описали (проблема с функциональным компонентом) есть в статье, только я оперирую введённым понятием "контекст React-компонента" и не упомянул ничего про память, думаю это хорошее дополнение. Суть проблемы не меняется - обработчики синтетических событий находятся в JSX-коде и при перерисовке они захватывают все актуальные состояния, а при обработке нативных событий этого не происходит и внутри обработчиков мы всегда имеем начальное значение состояния. Да и не всегда оно начальное. Если использовать hot reload, то можно добиться такого поведения, что в примере из статьи значение currentValue установится на какое-нибудь рандомное единожды, но не более.

      Я использовал термин "контекст React-компонента" потому что обработчики событий всё-таки захватывают, например, refs. Поэтому нельзя сказать что вообще все ссылки в этом обработчике не захватываются. Захватываются, только ссылки refs. Ну и ещё можно дополнить этот список "исключений" по мере необходимости.

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

      По-видимому в классовых компонентах этот пример работает потому что ссылка на функцию обработчик постоянно меняется и нет даже необходимости делать переподписку на обработку нативного события.


      1. profesor08
        24.04.2024 20:12

        onClick в обработке, в react, ничем не отличается от className . Как и не отличаются div и Counter. Проверится предыдущее значение, провериться следующее значение, если какой-то параметр изменился, то функция компонента выполнится и результат уйдет на рендер. То, как результат попадет в реальный DOM, уже не так важно.

        Ивенты тут вообще ни при чем, потому что вместо clickHandler может быть какой-то onSomeAction , который должен вызываться, если в компоненте что-то поменялось, и это никаким образом не будет связано с ивентами. Но работать будет идентично, и проблемы будут идентичные.


  1. Yozi
    24.04.2024 20:12
    +1

    Стиль изложения методички из университета

    предлагаю читателю поразмышлять
    внимательный читатель заметит...

    Содержание нам том же "высоком" уровне. Видно что автор "разбирается" в теме.

    1. Изобретена какая-то своя терминология: Обработчики событий почему-то называются событиями.

    2. Браузерному Event отказывают в праве называться нативным потому что там могут быть другие свойства не из спецификации (а обоснование этого "предлагаю читателю поразмышлять"). Что за своя терминология? Нативность и спецификация вообще разные свойства. Как цвет и вкус.

    3. Синтетические события - это круто и лучше чем нативные потому что синтетические нас защищают от утечки памяти. Классный довод, хорошо бы ещё раскрыть, а то мантра какая-то (читателю поразмышлять?)

    4. вот смотрите этот код работает хорошо, потому что он синтетический. А этот не работает, потому что он не синтетический. Довод офигенный. Оказывается дело не в том что автор написал код с тривиальным багом который линтер с плагином для react не пропустит.

    5. "Внимательный читатель..." + "Обработчики нативных событий не захватывают состояния, но захватывают refs". Вау блин. А "Синтетические" (в терминологии автора конечно) захватывают что ли? Оба варианта замыканий работают одинаково (это javascript всё таки, автор понимает как он работает?) Нативный обработчик автор значит замемоизировал с багом (тем который видит eslint) указав неверные dependencies, а синтетический обработчик он бодро пересоздает каждую перерисовку и радуется

    Это какая-то вредная статья. Прочтёт её джун, пойдёт на собеседование и начнёт такую же чушь нести наивно :(


    1. dan_sw Автор
      24.04.2024 20:12

      Вообще, можно разделить критику на две группы (так проще): токсичная и здоровая.

      Токсичная критика в целом не несёт ничего хорошего как для автора статьи, как для самой статьи, так и для автора токсичного комментария изложенного выше.

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

      Я считаю, что Ваша критика не совсем здорова. Ваши высмеивания издержек из моего труда (которые, к слову, ошибочны) это демонстрируют.

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

      Теперь к самому комментарию.

      Изобретена какая-то своя терминология: Обработчики событий почему-то называются событиями

      Я ничего нового не изобрёл. Укажите где Вы увидели путаницу между событиями и обработчиками событий? Признаюсь, я проверял статью несколько раз и не увидел, чтобы где-то было это нарушено, потому что если столько раз писать о событиях и иметь определённый эмпирический опыт в их написании, то невольно можно ошибиться. Я разделял в статье события и их обработку. Укажите пожалуйста место, где я этого не сделал - поправлю.

      Синтетические события - это круто и лучше чем нативные потому что синтетические нас защищают от утечки памяти. Классный довод, хорошо бы ещё раскрыть, а то мантра какая-то (читателю поразмышлять?)

      Где я написал, что синтетические события это "круто" и что они "лучше" нативных? Вообще не к этой статье относится. Может прочитали что-то одно, потом пришли почитать эту статью и у Вас всё смешалось? Такое бывает, это нормально. Я указал на чёткую разницу между синтетическими и нативными событиями. И то, что для решения описанной мной проблемы пользователь может захотеть переписать функциональные компоненты на классовые - это просто факт. Ну нет охоты у читателя переподписку делать, проще классовые компоненты сделать. Этот пункт Вы просто сами придумали (кроме утечки памяти, это в статье действительно было).

      Браузерному Event отказывают в праве называться нативным потому что там могут быть другие свойства не из спецификации (а обоснование этого "предлагаю читателю поразмышлять"). Что за своя терминология? Нативность и спецификация вообще разные свойства. Как цвет и вкус

      Где я отказывал в праве браузерному событию называться нативным? Я просто высказал своё мнение. Внимание, это было моё мнение, а не навязывание какой-то позиции. Я никакой позиции своей тут не навязываю. Спецификация это одно, а реализация этой спецификации под разные браузеры - это другое. Именно это я и отразил в своём скромном мнении о нативных событиях.

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

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

      Опять придуманный пункт. Я поясняю почему он обработчик нативного события не работает, а синтетического - работает. Почитайте комментарии выше, может это Вам что-то даст в понимании (в том числе мои цитаты к статье).

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

      "Внимательный читатель..." + "Обработчики нативных событий не захватывают состояния, но захватывают refs". Вау блин. А "Синтетические" (в терминологии автора конечно) захватывают что ли? Оба варианта замыканий работают одинаково (это javascript всё таки, автор понимает как он работает?) Нативный обработчик автор значит замемоизировал с багом (тем который видит eslint) указав неверные dependencies, а синтетический обработчик он бодро пересоздает каждую перерисовку и радуется

      Речи о замыканиях тут вообще не шло. Речь шла о захвате контекста компонента. Не более. На счёт "мемоизации с багом" - ну, как свой eslint настроите, так он и будет работать. У меня всё работает так, как я и ожидаю. Рекомендую ознакомиться с темой мемоизации и кэширования в React.js (+ чистые функции), может быть Вы к такому коду отнесётесь более сдержанно, потому что в массив зависимостей useEffect'а добавлять обёрнутую в useCallback функцию можно, иначе ничего бы не работало :).

      Это какая-то вредная статья. Прочтёт её джун, пойдёт на собеседование и начнёт такую же чушь нести наивно :(

      Не согласен с Вами. Одна из целей статьи - "повышение осведомлённости" о такой проблеме. В своё время я часто забывал об этой проблеме и совершал ошибки. Если бы где-то было понятное описание этой проблемы и способу её решения - я был бы рад. Не нашёл таких статей - написал сам. Теперь любой желающий может найти эту статью в поисковике и ознакомиться с проблемой, её решением и исключениях. Может быть, даже получит мотивацию для дальнейшего исследования этого вопроса.

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

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

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

      А Ваш комментарий в текущем виде... ну, не знаю, пользы от него нет.

      Ругать можно что угодно, сложно найти в чём угодно хорошее, как и положительно влиять на авторов статей, чтобы они писали их лучше :)


  1. Kingas
    24.04.2024 20:12

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

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

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


  1. Alexandroppolus
    24.04.2024 20:12

    Чтобы эффект не вызывался при изменении value и не делал бесполезные переподписки, можно воспользоваться хуком useEvent.

    const clickHandler = useEvent(() => {
        console.log("Проверка");
        // Установка текущего значения рандомайзера
        setCurrentValue(value);
      });
    
    useEffect(() => {
      // ...
    }, [clickHandler]);