イベントリスナーによるReactフックの間違った動作
質問
私は Reactフック で遊んでいるのですが、ある問題に直面しています。 イベントリスナーで処理されるボタンを使ってコンソールログを取ろうとすると、間違った状態が表示されるのです。
CodeSandboxです。 https://codesandbox.io/s/lrxw1wr97m
- クリックすると 'カードの追加' ボタンを2回クリック
- 1枚目のカードで ボタン1 をクリックすると、コンソールに2枚のカードがあることが表示されます (正しい動作)。
- 最初のカードで ボタン2 (をクリックすると(イベントリスナーによって処理される)、コンソールで、ステートに1つのカードしかないことがわかります(間違った動作)。
なぜ間違った状態が表示されるのでしょうか?
最初のカードでは
Button2
が表示されるはずです。
2
のカードが表示されるはずです。何かアイデアはありますか?
const { useState, useContext, useRef, useEffect } = React;
const CardsContext = React.createContext();
const CardsProvider = props => {
const [cards, setCards] = useState([]);
const addCard = () => {
const id = cards.length;
setCards([...cards, { id: id, json: {} }]);
};
const handleCardClick = id => console.log(cards);
const handleButtonClick = id => console.log(cards);
return (
<CardsContext.Provider
value={{ cards, addCard, handleCardClick, handleButtonClick }}
>
{props.children}
</CardsContext.Provider>
);
};
function App() {
const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
CardsContext
);
return (
<div className="App">
<button onClick={addCard}>Add card</button>
{cards.map((card, index) => (
<Card
key={card.id}
id={card.id}
handleCardClick={() => handleCardClick(card.id)}
handleButtonClick={() => handleButtonClick(card.id)}
/>
))}
</div>
);
}
function Card(props) {
const ref = useRef();
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
}, []);
return (
<div className="card">
Card {props.id}
<div>
<button onClick={props.handleButtonClick}>Button1</button>
<button ref={node => (ref.current = node)}>Button2</button>
</div>
</div>
);
}
ReactDOM.render(
<CardsProvider>
<App />
</CardsProvider>,
document.getElementById("root")
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id='root'></div>
React 16.7.0-alpha.0、Chrome 70.0.3538.110を使用しています。
ちなみに、CardsProviderをсlassを使って書き直すと、問題は解消されます。 クラスを使ったCodeSandbox https://codesandbox.io/s/w2nn3mq9vl
どのように解決するのですか?
これは、機能コンポーネントで
useState
フックを使用する機能的なコンポーネントに共通の問題です。同じ懸念は、コールバック関数で
useState
の状態が使用されている場合、例えば
setTimeout
または
setInterval
タイマー機能
.
イベントハンドラの扱いが異なるのは
CardsProvider
と
Card
コンポーネントで構成されます。
handleCardClick
と
handleButtonClick
で使用される
CardsProvider
機能コンポーネントは、そのスコープで定義されます。実行されるたびに新しい関数があり、それらは
cards
の状態を参照します。イベントハンドラは
CardsProvider
コンポーネントがレンダリングされるたびに再登録されます。
handleCardClick
で使用される
Card
機能コンポーネントは prop として受け取られ、コンポーネントマウント時に一旦
useEffect
. これはコンポーネントの寿命が尽きるまで同じ関数であり、その時点では新鮮だった古い状態を参照するために
handleCardClick
関数が最初に定義された時点では新鮮であった古い状態を参照しています。
handleButtonClick
はプロップとして受け取られ、それぞれの
Card
をレンダリングするたびに再登録されるため、毎回新しい関数となり、新しい状態を参照することになります。
変更可能な状態
この問題に対処するための一般的なアプローチは
useRef
の代わりに
useState
. ref は基本的に参照渡し可能なミュータブルオブジェクトを提供するレシピです。
const ref = useRef(0);
function eventListener() {
ref.current++;
}
この場合、コンポーネントは状態の更新時に再レンダリングされる必要があります。
useState
から期待されるように、コンポーネントは状態の更新時に再レンダリングされるべきであり、参照は適用されません。
状態の更新とミュータブルな状態を別々に保持することは可能ですが
forceUpdate
はクラスと関数の両方のコンポーネントでアンチパターンとされています(参考までに挙げておきます)。
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
ステート・アップデータ機能
ひとつの解決策として、ステート・アップデータ関数を使用する方法があります。この関数は、周囲のスコープから古いステートの代わりに新しいステートを受け取ります。
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
この場合、ステートは以下のような同期的な副作用のために必要です。
console.log
のような同期的な副作用のために状態が必要な場合、回避策として、更新を防ぐために同じ状態を返します。
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
return () => {
// unregister eventListener once
};
}, []);
これは非同期な副作用ではうまくいきません。特に
async
関数などです。
手動イベントリスナー再登録
もう一つの解決策は、イベントリスナーを毎回再登録することです。そうすれば、コールバックは常に周囲のスコープから新鮮な状態を取得できます。
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
組み込みのイベント処理
イベントリスナーを登録しない限り
document
,
window
など、現在のコンポーネントのスコープ外にあるイベントターゲットでは、React 自身の DOM イベントハンドリングを可能な限り使用する必要があり、これによって
useEffect
:
<button onClick={eventListener} />
最後のケースでは、イベントリスナーをさらに
useMemo
または
useCallback
を使うことで、propとして渡されたときに不要な再レンダリングを防ぐことができます。
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
-
この回答の前版では、初期状態に適用可能なミュータブルステートを使用することを提案しました。
useState
フックの実装に適用できましたが、React 16.8の最終的な実装では動作しません。useState
は現在immutable stateのみをサポートしています。
関連
-
[解決済み] React HooksでcomponentWillMount()を使用するには?
-
[解決済み】React Hooks useState()でオブジェクトを使用する。
-
[解決済み】React Hooks useEffectでoldValuesとnewValuesを比較する方法は?
-
[解決済み] jqueryでdivの要素がオーバーフローしていないかチェックする
-
[解決済み] Google maps API V3 - 同一地点に複数のマーカーを設置する。
-
[解決済み] Angularjs - 現在の日付を表示する
-
[解決済み] JSXとLoadshを使用して、ある要素をn回繰り返す方法
-
[解決済み] moment.jsでミュータビリティを回避するには?
-
[解決済み] TypeScriptプロジェクトで既存のC#クラス定義を再利用する方法
-
[解決済み] JavaScriptの文字列プリミティブとStringオブジェクトの違いは何ですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】setInterval内でReactのstateフックを使用するとstateが更新されない。
-
[解決済み] 配列からオブジェクトを生成する
-
[解決済み] 文字列がhtmlであるかどうかをチェックする
-
[解決済み] react-routerのハッシュフラグメントからクエリパラメータを取得する
-
[解決済み] TypeScriptプロジェクトで既存のC#クラス定義を再利用する方法
-
[解決済み] javascriptで文字列から関数を作成する方法はありますか?
-
[解決済み] jQueryで入力ファイルが空かどうかをチェックする方法
-
[解決済み] 文字列とラベルのローカライズとグローバリゼーションのベストプラクティス【終了しました
-
[解決済み] JavaScriptのArray.sort()メソッドでシャッフルするのは正しいのか?
-
[解決済み] querySelectorAllがない場合、ライブラリを使用せずに属性で要素を取得する?