реагировать на хуки и setInterval

Есть ли альтернатива просто держать «часы» в фоновом режиме для реализации автоматического перехода (через несколько секунд) в карусель с использованием перехватчиков реакции?

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

const useCarousel = (items = []) => {
  const [current, setCurrent] = useState(
    items && items.length > 0 ? 0 : undefined
  );

  const [auto, setAuto] = useState(false);

  const next = () => setCurrent((current + 1) % items.length);
  const prev = () => setCurrent(current ? current - 1 : items.length - 1);
  const reset = () => setCurrent(0);
  const start = _ => setAuto(true);
  const stop = _ => setAuto(false);


useEffect(() => {
    const interval = setInterval(_ => {
      if (auto) {
        next();
      } else {
        // do nothing
      }
    }, 3000);
    return _ => clearInterval(interval);
  });

  return {
    current,
    next,
    prev,
    reset,
    start,
    stop
  };
};

person marco alves    schedule 30.12.2018    source источник
comment
В чем конкретно ваш вопрос? Как делать что-то во временных интервалах без часов?   -  person azium    schedule 31.12.2018
comment
Кроме того, поскольку состояние обновляется в каждом интервале и useEffect имеет функцию возврата, которая очищает его, он будет запускать и останавливать новые часы при каждой визуализации. setTimeout в этом случае должен работать точно так же   -  person azium    schedule 31.12.2018
comment
Есть ли другой способ иметь часы, не работая всегда в фоновом режиме, даже если они не используются? - Я пробовал использовать useEffect в методе start, но безрезультатно   -  person marco alves    schedule 31.12.2018
comment
Я думаю, что useEffect has a return function which clears it, it will start and stop a new clock every render. неверно согласно reactjs.org/docs/hooks-reference.html#useeffect   -  person marco alves    schedule 31.12.2018
comment
if (auto) { id = setInterval(...) } как это ты имеешь в виду?   -  person azium    schedule 31.12.2018
comment
Нет, в пределах start   -  person marco alves    schedule 31.12.2018
comment
в какой части этого документа говорится, что он неверен? именно это и делает функция очистки. он будет запускаться при каждом рендеринге, если вы не предоставите второй аргумент useEffect, который говорит, при каких изменениях значения он не должен запускаться   -  person azium    schedule 31.12.2018
comment
вы не можете вызывать useEffect или какие-либо хуки в этом отношении внутри тела другой функции или за условным выражением. они должны безоговорочно запускаться в основном теле рендеринга   -  person azium    schedule 31.12.2018
comment
Прочитать cleaning up an event раздел   -  person marco alves    schedule 31.12.2018
comment
У меня ... Я читал документацию много раз, а также широко использую крючок. вы можете легко проверить это сами, поместив журнал консоли в тело вашей функции очистки   -  person azium    schedule 31.12.2018
comment
документы даже выделены жирным шрифтом, что я прав именно в этом разделе the previous effect is cleaned up before executing the next effect.   -  person azium    schedule 31.12.2018
comment
в любом случае, если вы просто не вызываете setInterval внутри своей useEffect функции, тогда он не запускает таймер в фоновом режиме, который вы просите   -  person azium    schedule 31.12.2018
comment
Тогда текст вводит в заблуждение: The clean-up function runs before the component is removed from the UI to prevent memory leaks.   -  person marco alves    schedule 31.12.2018
comment
он делает то же самое. прочтите следующий раздел об условном срабатывании эффекта, он отвечает на ваш вопрос   -  person azium    schedule 31.12.2018
comment
Не понимаю, как это помогает. Какую зависимость использовать в этом случае?   -  person marco alves    schedule 31.12.2018


Ответы (4)


Между setInterval и setTimeout есть различия, которые вы, возможно, не захотите потерять, если всегда перезапускаете таймер при повторном рендеринге компонента. Эта скрипта показывает разницу в дрейфе между ними, когда другой код также работает. (В старых браузерах / машинах - например, когда я изначально отвечал на этот вопрос - вам даже не нужно моделировать большие вычисления, чтобы увидеть, как через несколько секунд начнется значительный дрейф.)

Ссылаясь теперь на ваш ответ, Марко, использование setInterval полностью потеряно, потому что эффекты без условий удаляются и запускаются повторно каждый раз компонент перерисовывается. Итак, в вашем первом примере использование зависимости current приводит к тому, что этот эффект удаляется и запускается повторно при каждом изменении current (каждый раз, когда выполняется интервал). Второй делает то же самое, но на самом деле каждый раз, когда изменяется какое-либо состояние (вызывая повторную визуализацию), что может привести к некоторому неожиданному поведению. Единственная причина, по которой он работает, - это то, что next() вызывает изменение состояния.

Учитывая тот факт, что вы, вероятно, не заботитесь о точном времени, проще всего использовать setTimeout простым способом, используя переменные current и auto в качестве зависимостей. Итак, чтобы повторить часть вашего ответа, сделайте следующее:

useEffect(
  () => {
    if (!auto) return;
    const interval = setTimeout(_ => {
      next();
    }, autoInterval);
    return _ => clearTimeout(interval);
  },
  [auto, current]
);

Как правило, для тех, кто просто читает этот ответ и хочет способ сделать простой таймер, вот версия, которая не принимает во внимание исходный код OP, а также их потребность в способе запуска и остановки таймера независимо:

const [counter, setCounter] = useState(0);
useEffect(
  () => {
    const id= setTimeout(() => {
      setCounter(counter + 1); 
      // You could also do `setCounter((count) => count + 1)` instead.
      // If you did that, then you wouldn't need the dependency
      // array argument to this `useEffect` call.
    }, 1000);
    return () => {
      clearTimeout(id);
    };
  },
  [counter],
);

Однако вам может быть интересно, как использовать более точный интервал, учитывая тот факт, что setTimeout может дрейфовать больше, чем setInterval. Вот один из методов, опять же общий без использования кода OP:

// Using refs:

const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
  () => {
    const id = setInterval(() => {
      r.current.setCounter(r.current.counter + 1);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  },
  [] // empty dependency array
);

// Using the function version of `setCounter` is cleaner:

const [counter, setCounter] = useState(30);
useEffect(
  () => {
    const id = setInterval(() => {
      setCounter((count) => count + 1);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  },
  [] // empty dependency array
);

Вот что происходит выше:

(первый пример с использованием ссылок): чтобы обратный вызов setInterval всегда ссылался на текущую приемлемую версию setCounter, нам нужно какое-то изменяемое состояние. React дает нам это с useRef. Функция useRef вернет объект со свойством current. Затем мы можем установить это свойство (что будет происходить каждый раз при повторном рендеринге компонента) для текущих версий counter и setCounter.

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

(оба примера, продолжение). Затем, чтобы интервал не удалялся при каждом рендеринге, мы добавляем пустой массив зависимостей в качестве второго аргумента в useEffect. Интервал все равно будет очищен при размонтировании компонента.

Примечание. Раньше мне нравилось использовать ["once"] в качестве массива зависимостей, чтобы указать, что я принудительно настраиваю этот эффект только один раз. В то время он был удобен для чтения, но я больше не использую его по двум причинам. Во-первых, в наши дни хуки стали более понятными, и мы повсюду видели пустой массив. Во-вторых, это противоречит очень популярному правилу хуков линтер, которое довольно строго касается того, что входит в массив зависимостей.

Итак, применив то, что мы знаем к исходному вопросу OP, вы можете использовать setInterval для слайд-шоу с меньшей вероятностью дрейфа, например:

// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...

const r = useRef(null);
r.current = { next };
useEffect(
  () => {
    if (!auto) return;
    const id = setInterval(() => {
      r.current.next();
    }, autoInterval);
    return () => {
      clearInterval(id);
    };
  },
  [auto]
);
person Don    schedule 31.12.2018

Поскольку значение current будет меняться на каждом «интервале», пока оно должно работать, ваш код будет запускать и останавливать новый таймер при каждой визуализации. Вы можете увидеть это в действии здесь:

https://codesandbox.io/s/03xkkyj19w

Вы можете изменить setInterval на setTimeout, и вы получите точно такое же поведение. setTimeout - это не постоянные часы, но это не имеет значения, поскольку они оба все равно очищаются.

Если вы вообще не хотите запускать какой-либо таймер, поместите условие перед setInterval, а не внутри него.

  useEffect(
    () => {
      let id;

      if (run) {
        id = setInterval(() => {
          setValue(value + 1)
        }, 1000);
      }

      return () => {
        if (id) {
          alert(id) // notice this runs on every render and is different every time
          clearInterval(id);
        }
      };
    }
  );
person azium    schedule 31.12.2018
comment
Кажется, что codeanbox не работает - только один повторный рендеринг. Кроме того, в моем примере я не устанавливаю setInterval условно. Я условно увеличиваю значение внутри функции-обработчика. Взгляните здесь: codesandbox.io/s/5wxpnqn9kx - person marco alves; 31.12.2018
comment
Я понимаю вашу точку зрения о setInterval и setTimeout - если в useEffect нет зависимостей, тогда он будет устанавливать таймер для следующего рендеринга при каждом запуске. - person marco alves; 31.12.2018
comment
Хм, песочница у меня работает отлично, странно. Не нужно передавать какой-либо второй аргумент, чтобы это работало в теории, и, похоже, на практике это тоже работает. Не могли бы вы скопировать код и вставить его куда-нибудь? Интересно, есть ли разница в кешировании или что-то в этом роде - person azium; 02.01.2019

Пока что кажется, что оба решения ниже работают должным образом:

Условное создание таймера - для его работы требуется, чтобы useEffect зависел от auto и current.

useEffect(
    () => {
      if (!auto) return;
      const interval = setInterval(_ => {
        next();
      }, autoInterval);
      return _ => clearInterval(interval);
    },
    [auto, current]
  );

Условное выполнение обновления до состояния - не требует зависимостей useEffect

useEffect(() => {
    const interval = setInterval(_ => {
      if (auto) {
        next();
      } else {
        // do nothing
      }
    }, autoInterval);
    return _ => clearInterval(interval);
  });

Оба решения работают, если заменить setInterval на setTimeout

person marco alves    schedule 31.12.2018

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

person Vad    schedule 11.02.2019