反応フックを使用してカルーセルに自動ネクスト(数秒後)を実装するために、バックグラウンドで「クロック」を保持するだけの代替手段はありますか?
以下のカスタム反応フックは、カルーセルの現在の(アクティブ)インデックスを変更するための手動(次へ、前へ、リセット)および自動(開始、停止)メソッドをサポートするカルーセルの状態を実装します。
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
};
};
setInterval
とsetTimeout
の違いは、コンポーネントが再表示されるときに常にタイマーを再起動することで失われたくない場合があります。 このフィドル は、2つのドリフトの違いを示します(そして、Reactが実行しているすべての計算を考慮に入れていませんもう少し)。
マルコの your answer を参照すると、setInterval
の使用は完全に失われています。条件のないエフェクトは、コンポーネントが再レンダリングされるたびに破棄され、再実行されるためです。したがって、最初の例では、current
依存関係を使用すると、current
が変更されるたびに(間隔が実行されるたびに)その効果が破棄され、再実行されます。 2つ目も同じことを行いますが、実際には状態が変化するたびに(再レンダリングの原因)、予期しない動作が発生する可能性があります。機能する唯一の理由は、next()
によって状態が変化するためです。
exactタイミングにおそらく関係がないという事実を考慮すると、setTimeout
およびcurrent
変数を依存関係として使用して、auto
を単純な方法で使用するのが最もクリーンです。 。答えの一部を言い直すには、次のようにします。
useEffect(
() => {
if (!auto) return;
const interval = setInterval(_ => {
next();
}, autoInterval);
return _ => clearInterval(interval);
},
[auto, current]
);
current
の依存関係をnext
で置き換えると、useEffect
内で行っていることをより直接的に表すことができるので、注意してください。しかし、Reactがこれらの依存関係をどのように差分するかは100%わかりません。そのため、そのままにしておきます。
一般的に、この答えを読んでいて単純なタイマーを実行する方法が必要な場合は、OPの元のコードを考慮しないバージョンもあり、タイマーを個別に開始および停止する方法も必要ありません。
const [counter, setCounter] = useState(0);
useEffect(
() => {
const id= setTimeout(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearTimer(id);
};
},
[counter],
);
ただし、setTimeout
がsetInterval
よりもドリフトが大きいという事実を考えると、より正確な間隔を使用する方法について疑問に思うかもしれません。ここでも、OPのコードを使用しない一般的な方法を1つ示します。
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);
};
},
['once'],
);
ここで何が起きてるの?さて、setInterval
のコールバックを取得して常に現在受け入れ可能なバージョンのsetCounter
を参照するには、いくつかの変更可能な状態が必要です。 Reactは、useRef
でこれを提供します。useRef
関数は、current
プロパティを持つオブジェクトを返します。次に、そのプロパティ(コンポーネントが再レンダリングされるたびに発生します)を現在のcounter
および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],
);
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);
}
};
}
);
これまでのところ、以下の両方のソリューションは期待どおりに機能するようです:
条件付きでタイマーを作成する —これは、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
に置き換えると、両方のソリューションが機能します
指定したミリ秒数後にuseTimeout
を返すtrue
フックを使用できます。