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

Код здесь: https://codesandbox.io/s/nw4jym4n0

export default ({ name }: Props) => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter(counter + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  });

  return <h1>{counter}</h1>;
};

Проблема заключается в повторном рендеринге каждого триггера setCounter, поэтому интервал сбрасывается и создается заново. Это может выглядеть нормально, поскольку состояние (счетчик) продолжает увеличиваться, однако оно может зависнуть при объединении с другими хуками.

Как правильно это сделать? В компоненте класса это просто с переменной экземпляра, содержащей интервал.

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
38
0
57 707
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

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

Пример

const { useState, useEffect } = React;

function App() {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter(counter => counter + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return <h1>{counter}</h1>;
};

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
<script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id = "root"></div>

Более универсальным подходом было бы создание новой пользовательской ловушки, которая хранит функцию в ref и создает новый интервал, только если delay должен измениться, как это делает Дэн Абрамов в своем замечательном сообщении в блоге "Делаем setInterval декларативным с помощью React Hooks".

Пример

const { useState, useEffect, useRef } = React;

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    let id = setInterval(() => {
      savedCallback.current();
    }, delay);
    return () => clearInterval(id);
  }, [delay]);
}

function App() {
  const [counter, setCounter] = useState(0);

  useInterval(() => {
    setCounter(counter + 1);
  }, 1000);

  return <h1>{counter}</h1>;
};

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
<script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id = "root"></div>

Вам действительно нужно было сохранить его здесь с помощью useRef? Не могли бы вы просто вызвать обратный вызов, не ссылаясь на него?

RegarBoy 26.11.2021 22:53

Как уже показывает другой ответ от @YangshunTay, можно сделать так, чтобы обратный вызов useEffect запускался только один раз и работал аналогично componentDidMount. В этом случае необходимо использовать функцию обновления состояния из-за ограничений, накладываемых областями функций, иначе обновленный counter не будет доступен внутри обратного вызова setInterval.

Альтернативный вариант - запускать обратный вызов useEffect при каждом обновлении счетчика. В этом случае setInterval следует заменить на setTimeout, а обновления должны быть ограничены обновлениями counter:

export default ({ name }: Props) => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setCounter(counter + 1);
    }, 1000);

    return () => {
      clearTimeout(timeout);
    };
  }, [counter]);

  return <h1>{counter}</h1>;
};

Это не идеально, так как setTimeout будет запускаться при каждом рендеринге, и в этом нет необходимости.

Yangshun Tay 20.11.2018 17:33

Я не вижу в этом ничего плохого. Проблемы с производительностью отсутствуют. Рекурсивный setTimeout - это обычная замена setInterval в JS, когда это оправдано. Функция обновления состояния отлично выглядит в setInterval в этом конкретном случае, то есть для простого приращения. Это было бы совсем иначе, если бы было задействовано несколько государств.

Estus Flask 20.11.2018 17:43

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

Однако вам нужно будет изменить setCounter, чтобы использовать предыдущее значение counter. Причина в том, что обратный вызов, переданный в закрытие setInterval, обращается только к переменной counter в первом рендеринге, у него нет доступа к новому значению counter в последующем рендеринге, потому что useEffect() не вызывается во второй раз; counter всегда имеет значение 0 в обратном вызове setInterval.

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

function Counter() {
  const [counter, setCounter] = React.useState(0);
  React.useEffect(() => {
    const timer = setInterval(() => {
      setCounter(prevCount => prevCount + 1); // <-- Change this line!
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []); // Pass in empty array to run effect only once!

  return (
    <div>Count: {counter}</div>
  );
}

ReactDOM.render(<Counter />, document.querySelector('#app'));
<script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id = "app"></div>

Другие вопросы по теме