Reactフックで遊んでいて、問題に直面しました。イベントリスナーによって処理されるボタンを使用してコンソールログに記録しようとすると、間違った状態が表示されます。
CodeSandbox:https://codesandbox.io/s/lrxw1wr97m
間違った状態を表示するのはなぜですか?最初のカードでは、Button2はコンソールに2枚のカードを表示する必要があります。何か案は?
import React, { useState, useContext, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
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")
);
React 16.7.0-alpha.0およびChrome 70.0.3538.110を使用します
ところで、сlassを使用してCardsProviderを書き換えると、問題はなくなりました。クラスを使用するCodeSandbox: https://codesandbox.io/s/w2nn3mq9vl
これは、useState
フックを使用する機能コンポーネントの一般的な問題です。同じ懸念は、useState
状態が使用されるコールバック関数にも適用できます。 setTimeout
またはsetInterval
タイマー関数 。
イベントハンドラーは、CardsProvider
およびCard
コンポーネントで異なる方法で処理されます。
handleCardClick
機能コンポーネントで使用されるhandleButtonClick
およびCardsProvider
は、そのスコープで定義されます。実行するたびに新しい関数があり、それらは定義されたときに取得されたcards
状態を参照します。イベントハンドラは、CardsProvider
コンポーネントがレンダリングされるたびに再登録されます。
handleCardClick
機能コンポーネントで使用されるCard
は、小道具として受け取られ、useEffect
を使用してコンポーネントマウントに一度登録されます。これは、コンポーネントの寿命全体で同じ関数であり、handleCardClick
関数が最初に定義された時点で新鮮だった古い状態を指します。 handleButtonClick
はプロップとして受け取られ、各Card
レンダリングで再登録されます。これは毎回新しい関数であり、新しい状態を参照します。
この問題に対処する一般的なアプローチは、useRef
の代わりにuseState
を使用することです。 refは基本的に、参照によって渡すことができる可変オブジェクトを提供するレシピです。
const ref = useRef(0);
function eventListener() {
ref.current++;
}
useState
から予想されるような状態の更新時にコンポーネントを再レンダリングする必要がある場合、refは適用されません。
状態の更新と変更可能な状態を別々に保持することは可能ですが、forceUpdate
はクラスおよび関数コンポーネントの両方でアンチパターンと見なされます(参照用にのみリストされています)。
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
解決策の1つは、エンクロージングスコープから古い状態の代わりに新しい状態を受け取る状態更新機能を使用することです。
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
}, []);
これは非同期の副作用、特にasync
関数ではうまく機能しません。
別の解決策は、毎回イベントリスナーを再登録することです。そのため、コールバックは常にスコープを囲むことで新しい状態を取得します。
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
}, [state]);
イベントリスナーがdocument
、window
に登録されていない場合、または他のイベントターゲットが現在のコンポーネントのスコープ外である場合を除き、React独自のDOMイベント処理を可能な限り使用する必要があります。これにより、useEffect
:
<button onClick={eventListener} />
最後のケースでは、イベントリスナーをuseMemo
またはuseCallback
でさらにメモして、小道具として渡されたときに不必要な再レンダリングを防ぐことができます。
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
React 16.7.0-alphaバージョンの初期useState
フック実装に適用できるが、機能しない可変状態を使用することを提案した回答の以前の版最終的なReact 16.8実装。 useState
は現在、不変状態のみをサポートしています。