Почему компонент ReactJS, использующий хуки, визуализируется один или два раза в зависимости от того, открыта консоль разработчика или нет?

Следующий код дважды распечатывается в консоли веб-сайта codeandbox.io (в этой версии используется StrictMode), а также в приведенном ниже фрагменте (без использования StrictMode):

const { useState, useEffect } = React;

function useCurrentTime() {
  const [timeString, setTimeString] = useState("");

  useEffect(() => {
    const intervalID = setInterval(() => {
      setTimeString(new Date().toLocaleTimeString());
    }, 100);
    return () => clearInterval(intervalID);
  }, []);

  return timeString;
}

function App() {
  const s = useCurrentTime();
  console.log(s);

  return <div className="App">{s}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.development.js"></script>

Демо: https://codesandbox.io/s/gallant-bas-3lq5w?file=/src/App.js (с использованием StrictMode)

Вот фрагмент с использованием производственных библиотек; он по-прежнему регистрируется дважды:

const { useState, useEffect } = React;

function useCurrentTime() {
  const [timeString, setTimeString] = useState("");

  useEffect(() => {
    const intervalID = setInterval(() => {
      setTimeString(new Date().toLocaleTimeString());
    }, 100);
    return () => clearInterval(intervalID);
  }, []);

  return timeString;
}

function App() {
  const s = useCurrentTime();
  console.log(s);

  return <div className="App">{s}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Однако, когда я открываю консоль разработчика, я вижу, что каждый раз распечатывается только один раз, а также в консоли codeandbox.io я вижу, что он распечатывается один раз.

А затем, если я создам автономное приложение React с помощью create-react-app и использую приведенный выше код, и каждый раз он печатается дважды.

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


person nonopolarity    schedule 08.03.2021    source источник
comment
Все: Это не похоже на StrictMode.   -  person T.J. Crowder    schedule 08.03.2021
comment
Отвечает ли это на ваш вопрос? stackoverflow.com/questions/61053432/ @ TJCrowder Вы уверены, что связанный код и ящик отображается в StrictMode, который, кажется, дважды вызывает журнал рендеринга и консоли в качестве побочного эффекта.   -  person Drew Reese    schedule 08.03.2021
comment
@DrewReese - Сначала я предполагал, что это так, но если я запустил приведенный выше фрагмент стека, в котором не используется StrictMode, я вижу время, зарегистрированное дважды ... какое-то время, а затем они просто регистрируются один раз.   -  person T.J. Crowder    schedule 08.03.2021
comment
@ T.J. Crowder Хорошо, в консоли браузера, да, я вижу то же самое. Изменилось ли это поведение в React v17? Если я редактирую тот же самый кодовый ящик и удаляю StrictMode, я все равно вижу двойное ведение журнала. Если я применяю решение консольного входа в useEffect хук, я вижу только одно журналирование, как я и ожидал.   -  person Drew Reese    schedule 08.03.2021
comment
@DrewReese - не знаю. Я даже вижу это с производственными библиотеками. Я чувствую себя толстым. :-)   -  person T.J. Crowder    schedule 08.03.2021
comment
правильно, я удалил StrictMode, и он все еще был дважды, а также в производственной сборке, якобы он не должен отображаться дважды, даже с StrictMode там   -  person nonopolarity    schedule 08.03.2021
comment
И что самое странное, если вы перейдете на другую вкладку, а затем вернетесь к первой из кодов и ящиков, вы увидите, что console.log печатался только один раз, пока вас не было. По крайней мере, в FF это так.   -  person codemonkey    schedule 08.03.2021
comment
Если я изменю обратный вызов таймера так, чтобы он запомнил последнюю временную строку и только вызовет setTimeString, если это изменение, поведение исчезнет. Итак, он вызывает setTimeString с той же строкой, которая его вызывает ... а на самом деле этого не должно быть. (Это без StrictMode и с использованием производственных, а не разрабатываемых библиотек.) Здесь определенно есть что-то, о чем я не знаю. Я думал, что вызов установщика состояния с тем же значением был практически бесполезным.   -  person T.J. Crowder    schedule 08.03.2021
comment
@DrewReese - отличие хуков в функциональных компонентах было бы очень неожиданностью. Документация, похоже, предполагает, что это не отличается: Если ваша функция обновления возвращает то же значение, что и текущее состояние, последующий повторный рендеринг будет полностью пропущен. Конечно, речь идет только о функциональном обновлении, но если я изменю код для использования функционального обновления, я все равно получу двойное ведение журнала. Кроме того, если бы это была проблема, мы бы увидели ~ 10 обновлений, а не только 2.   -  person T.J. Crowder    schedule 08.03.2021
comment
@nonopolarity - я должен отметить, что поведение, которое я вижу здесь с помощью фрагмента стека, не соответствует вашему описанию, но вызывает удивление. Я вижу два обновления состояния ... обычно ... при вызове установщика состояния с одним и тем же значением примерно ~ 10 раз. В. странно.   -  person T.J. Crowder    schedule 08.03.2021
comment
@ T.J. Crowder: Правильно, извините, я только что проверил это, и вы правы, так что я, должно быть, неправильно запомнил сценарий, который у меня был. Удаление комментария, но сохранение того, что, по моему мнению, по-прежнему актуально: я полагаю, я не уделил достаточно внимания интервальной задержке, используемой во фрагменте OP, и рассмотрел детализацию строки локали объекта даты JS. Действительно, увеличивая задержку до 1000, я вижу только единичное ведение журнала.   -  person Drew Reese    schedule 08.03.2021
comment
@DrewReese - Все хорошо - мы все просто пытаемся помочь. :-) Да, если вы переключаетесь на интервалы в 1 с, они исчезают, хотя это вызывает свои проблемы (почти секунда и т. Д.), Но для меня принципиально то, что вызов установщика состояния с тем же value не должно вызывать повторную визуализацию. Я упростил это без кастомного хука, и это все еще происходит. o_O Я имею в виду, я понимаю, что React может повторно рендерить, когда это необходимо, это просто кажется странным.   -  person T.J. Crowder    schedule 08.03.2021
comment
@DrewReese - К вашему сведению, нашел ответ.   -  person T.J. Crowder    schedule 08.03.2021


Ответы (1)


Насколько я могу судить, наблюдаемый нами двойной вызов App является ожидаемым поведением. Из useState документации:

Выход из состояния обновления

Если вы обновите State Hook до того же значения, что и текущее состояние, React выйдет из строя без рендеринга дочерних элементов или эффектов запуска. (React использует алгоритм сравнения Object.is.)

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

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

И действительно, если я обновлю ваш пример так, чтобы он использовал дочерний компонент для отображения строки времени, мы увидим, что этот дочерний компонент вызывается только один раз для каждого значения timeString, а не в два раза, как App, даже если дочерний компонент не заключен в React.memo:

const { useState, useEffect } = React;

function useCurrentTime() {
    const [timeString, setTimeString] = useState("");
  
    useEffect(() => {
        const intervalID = setInterval(() => {
            setTimeString(new Date().toLocaleTimeString());
        }, 100);
        return () => clearInterval(intervalID);
    }, []);
  
    return timeString;
}

function ShowTime({timeString}) {
    console.log("ShowTime", timeString);
    return <div className="App">{timeString}</div>;
}

function App() {
    const s = useCurrentTime();
    console.log("App", s);
  
    return <ShowTime timeString={s} />;
}

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Когда я запускаю это, я вижу:

App 09:57:14
ShowTime 09:57:14
App 09:57:14
App 09:57:15
ShowTime 09:57:15
App 09:57:15
App 09:57:16
ShowTime 09:57:16
App 09:57:16
App 09:57:17
ShowTime 09:57:17
App 09:57:17
App 09:57:18
ShowTime 09:57:18
App 09:57:18
App 09:57:19
ShowTime 09:57:19
App 09:57:19
App 09:57:20
ShowTime 09:57:20
App 09:57:20

Обратите внимание, что, хотя App вызывается дважды для каждого значения timeString, ShowTime вызывается только один раз.

Я должен отметить, что это более автоматический процесс, чем это было с class компонентами. Эквивалентный компонент class будет обновляться 10 раз в секунду, если вы не реализуете _16 _. :-)

Кое-что из того, что вы видели на CodeSandbox, вполне могло быть связано с StrictMode, подробнее об этом здесь. Но два вызова App для каждого значения timeString - это просто React, делающий свое дело.

person T.J. Crowder    schedule 08.03.2021
comment
Что касается того, почему React вызывает его дважды ... может это быть связано с тем, что если интервал или setTimeout длинный, например 33 или выше, это не должно быть проблемой (Google Chrome имеет степень детализации 4 или 5 мс), но что если это 0 или 2 мс, и каждый раз в useEffect()? запускаются setInterval и clearInterval? - person nonopolarity; 08.03.2021
comment
Если задуматься, если мы перейдем на другую вкладку, то console.log появится только один раз. Может быть, React по какой-то причине параноик, если данные отображаются на экране, это могут быть неправильные данные, поэтому он делает это дважды, чтобы убедиться, - в отличие от, если он не отображается, но просто setState и, возможно, просто чтобы вызвать какие-либо эффекты или AJAX или что-то в этом роде, тогда безопасно визуализировать компонент только один раз? - person nonopolarity; 08.03.2021
comment
@nonopolarity - я подозреваю, что причина, по которой вы видите только один вызов, когда вы переключаетесь с вкладки, заключается в том, что Chrome регулирует интервальный таймер, что Chrome делает довольно агрессивно. Я не думаю, что это имеет какое-либо отношение к длине интервала или к минимальному времени интервала. Я думаю, что React вызывает вашу App функцию, чтобы убедиться, что она не отображается по-разному, несмотря на то же значение состояния. Поскольку ваш App этого не делает, он не вызывает его снова до следующего фактического изменения состояния. - person T.J. Crowder; 08.03.2021