tldr;componentDidUpdate
をシミュレートする方法、または配列でkey
を使用してコンポーネントを強制的にリセットするにはどうすればよいですか?
タイマーを表示し、ゼロに達するとコールバックを実行するコンポーネントを実装しています。その目的は、コールバックがオブジェクトのリストを更新することです。後者のコンポーネントは、新しい ReactフックuseState
およびuseEffect
で構成されています。
state
には、タイマーが開始された時刻と残りの時刻への参照が含まれます。 effect
は、残りの時間を更新し、コールバックを呼び出す必要があるかどうかを確認するために、毎秒呼び出される間隔を設定します。
コンポーネントは、タイマーを再スケジュールしたり、ゼロに達したときに間隔を維持したりするためのものではなく、コールバックを実行してアイドル状態にする必要があります。タイマーを更新するために、配列をkey
に渡してコンポーネントの状態をリセットし、タイマーを再起動することを望んでいました。残念ながら、key
は文字列と共に使用する必要があるため、配列の参照が変更されたかどうかは効果がありません。
また、懸念している配列を渡すことで、小道具に変更をプッシュしようとしましたが、状態は維持されたため、間隔はリセットされませんでした。
新しいフックAPIのみを使用して状態を強制的に更新するために、配列の浅い変化を観察するための好ましい方法は何でしょうか?
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
function getTimeRemaining(startedAt, delay) {
const now = new Date();
const end = new Date(startedAt.getTime() + delay);
return Math.max(0, end.getTime() - now.getTime());
}
function RefresherTimer(props) {
const [startedAt, setStartedAt] = useState(new Date());
const [timeRemaining, setTimeRemaining] = useState(getTimeRemaining(startedAt, props.delay));
useEffect(() => {
if (timeRemaining <= 0) {
// The component is set to idle, we do not set the interval.
return;
}
// Set the interval to refresh the component every second.
const i = setInterval(() => {
const nowRemaining = getTimeRemaining(startedAt, props.delay);
setTimeRemaining(nowRemaining);
if (nowRemaining <= 0) {
props.callback();
clearInterval(i);
}
}, 1000);
return () => {
clearInterval(i);
};
});
let message = `Refreshing in ${Math.ceil(timeRemaining / 1000)}s.`;
if (timeRemaining <= 0) {
message = 'Refreshing now...';
}
return <div>{message}</div>;
}
RefresherTimer.propTypes = {
callback: PropTypes.func.isRequired,
delay: PropTypes.number
};
RefresherTimer.defaultProps = {
delay: 2000
};
export default RefresherTimer;
key
で使用しようとしました:
<RefresherTimer delay={20000} callback={props.updateListOfObjects} key={listOfObjects} />
小道具の変更で使用しようとしました:
<RefresherTimer delay={20000} callback={props.updateListOfObjects} somethingThatChanges={listOfObjects} />
listOfObjects
はオブジェクトの配列を指します。オブジェクト自体は必ずしも変更されないため、配列を!==
と比較する必要があります。通常、値はRedux
から取得されます。アクションupdateListOfObjects
は、newListOfObjects = [...listOfObjects]
のように配列を再初期化します。
useRef
は、機能コンポーネントに「インスタンス変数」を作成します。状態を更新せずにマウントまたは更新フェーズにあるかどうかを示すフラグとして機能します。
const mounted = useRef();
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
} else {
// do componentDidUpate logic
}
});
つまり、配列の参照が変更されたときにタイマーをリセットしたいのですよね?その場合、何らかの差分メカニズムを使用する必要があります。純粋なフックベースのソリューションは、次のようにuseEffect
の2番目のパラメーターを利用します。
function RefresherTimer(props) {
const [startedAt, setStartedAt] = useState(new Date());
const [timeRemaining, setTimeRemaining] = useState(getTimeRemaining(startedAt, props.delay));
//reset part, lets just set startedAt to now
useEffect(() => setStartedAt(new Date()),
//important part
[props.listOfObjects] // <= means: run this effect only if any variable
// in that array is different from the last run
)
useEffect(() => {
// everything with intervals, and the render
})
}
この動作の詳細はこちら https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
コンポーネントを再マウントする方法は、新しいkey
プロパティを提供することです。文字列である必要はありませんが、内部的に文字列に強制変換されるため、listOfObjects
が文字列である場合、key
が内部でlistOfObjects.toString()
と比較されます。
任意のランダムキーを使用できます。 uuid
またはMath.random()
。親コンポーネントでlistOfObjects
の浅い比較を実行して、新しいキーを提供できます。 useMemo
フックは、再マウントキーを条件付きで更新するために親状態で使用できます。また、listOfObjects
は、メモする必要があるパラメーターのリストとして使用できます。 例 です:
const remountKey = useMemo(() => Math.random(), listOfObjects);
return (
<div>
<RefresherTimer delay={3000} callback={() => console.log('refreshed')} key={remountKey} />
</div>
);
キーを再マウントする代わりに、子コンポーネントは自身の状態をリセットし、コールバックを公開してリセットをトリガーできます。
子コンポーネント内でlistOfObjects
の浅い比較を行うと、親コンポーネントの実装を認識する必要があるため、アンチパターンになります。