web-dev-qa-db-ja.com

間違ったReactがイベントリスナーで動作をフックする

Reactフックで遊んでいて、問題に直面しました。イベントリスナーによって処理されるボタンを使用してコンソールログに記録しようとすると、間違った状態が表示されます。

CodeSandbox:https://codesandbox.io/s/lrxw1wr97m

  1. [カードを追加]ボタンを2回クリックします
  2. 最初のカードで、Button1をクリックし、コンソールに状態のカードが2つあることを確認します(正しい動作)
  3. 最初のカードで、Button2(イベントリスナーによって処理される)をクリックし、コンソールに状態が1つのカードしかないことを確認します(不正な動作)

間違った状態を表示するのはなぜですか?最初のカードでは、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

14
Mark Lano

これは、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]);

組み込みのイベント処理

イベントリスナーがdocumentwindowに登録されていない場合、または他のイベントターゲットが現在のコンポーネントのスコープ外である場合を除き、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は現在、不変状態のみをサポートしています。

28
Estus Flask