React Hooks useEffectでoldValuesとnewValuesを比較する方法は?


149

たとえば、rate、sendAmount、およびreceiveAmountの3つの入力があるとします。その3つの入力をuseEffect diffing paramsに配置しました。ルールは次のとおりです。

  • sendAmountが変更された場合、計算します receiveAmount = sendAmount * rate
  • receiveAmountが変更された場合、計算します sendAmount = receiveAmount / rate
  • レートを変更した場合、私は計算しreceiveAmount = sendAmount * rateたときsendAmount > 0または私が計算しsendAmount = receiveAmount / rateたときにreceiveAmount > 0

これがcodesandbox https://codesandbox.io/s/pkl6vn7x6jです問題を実証するです。

比較する方法があるoldValuesnewValues上の好きは、componentDidUpdate代わりに、このような場合のために3つのハンドラを作るのは?

ありがとう


これがhttps://codesandbox.io/s/30n01w2r06の私の最終的な解決策ですusePrevious

この場合、useEffect変更ごとに同じネットワーク呼び出しが発生するため、複数を使用することはできません。そのため、私もchangeCount変更の追跡に使用しています。これchangeCountはローカルからの変更のみを追跡するのにも役立ちます。サーバーからの変更による不要なネットワーク呼び出しを防ぐことができます。


componentDidUpdateはどの程度正確に役立つはずですか?これらの3つの条件を書き込む必要があります。
Estus Flask

2つのオプションのソリューションを使用して回答を追加しました。それらの1つはあなたのために働きますか?
Ben Carp

回答:


207

カスタムフックを記述して、useRefを使用し以前の小道具を提供できます。

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

そしてそれを useEffect

const Component = (props) => {
    const {receiveAmount, sendAmount } = props
    const prevAmount = usePrevious({receiveAmount, sendAmount});
    useEffect(() => {
        if(prevAmount.receiveAmount !== receiveAmount) {

         // process here
        }
        if(prevAmount.sendAmount !== sendAmount) {

         // process here
        }
    }, [receiveAmount, sendAmount])
}

ただし、useEffect変更IDごとに2つを個別に使用して個別に処理したい場合は、それらを読んで理解するほうがより明確で、おそらくより適切で明確です。


8
2つのuseEffect通話を別々に使用することについてのメモをありがとう。複数回使用できることに気づかなかった!
JasonH

3
上記のコードを試しましたが、eslintはuseEffect依存関係がないことを警告していますprevAmount
Littlee

2
prevAmountは、state / propsの以前の値の値を保持するため、useEffectへの依存関係として渡す必要はなく、特定のケースでこの警告を無効にすることができます。詳細については、この投稿を読むことができますか?
Shubham Khatri

これが大好きです– useRefは常に助けになります。
Fernando Rojo

@ShubhamKhatri:ref.current = valueのuseEffectラッピングの理由は何ですか?
curly_brackets

40

誰かがTypeScriptバージョンのusePreviousを探している場合:

import { useEffect, useRef } from "react";

const usePrevious = <T extends {}>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

2
これはTSXファイルでは機能しないことに注意してください。これをTSXファイルで機能さconst usePrevious = <T extends any>(...せるには、インタプリタに<T>がJSXではなく一般的な制限であると思わせるように変更します
apokryfos

1
なぜ<T extends {}>うまくいかないのか説明してもらえますか?それは私のために働いているようですが、私はそのようにそれを使用することの複雑さを理解しようとしています。
Karthikeyan_kk

.tsxファイルには、htmlと同一であることを意図したjsxが含まれています。ジェネリック<T>が閉じられていないコンポーネントタグである可能性があります。
SeedyROM

戻り値の型はどうですか?取得Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
サバアハン

@SabaAhangごめんなさい!TSは型推論を使用してほとんどの場合戻り型を処理しますが、lintingを有効にしている場合は、警告を取り除くために戻り型を追加しました。
SeedyROM

25

オプション1-useCompareフック

現在の値を以前の値と比較することは一般的なパターンであり、実装の詳細を隠す独自のカスタムフックを正当化します。

const useCompare = (val: any) => {
    const prevVal = usePrevious(val)
    return prevVal !== val
}

const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
}

const Component = (props) => {
  ...
  const hasVal1Changed = useCompare(val1)
  const hasVal2Changed = useCompare(val2);
  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
    if (hasVal2Changed ) {
      console.log("val2 has changed");
    }
  });

  return <div>...</div>;
};

デモ

オプション2-値が変更されたときにuseEffectを実行する

const Component = (props) => {
  ...
  useEffect(() => {
    console.log("val1 has changed");
  }, [val1]);
  useEffect(() => {
    console.log("val2 has changed");
  }, [val2]);

  return <div>...</div>;
};

デモ


2番目のオプションは私のために働いた。useEffect Twiceを作成する必要がある理由を教えてください。
Tarun Nagpal

@ TarunNagpal、useEffectを2回使用する必要はありません。ユースケースによって異なります。どのvalが変更されたかをログに記録したいとします。依存関係の配列にval1とval2の両方がある場合、いずれかの値が変更されるたびに実行されます。次に、渡す関数内で、正しいメッセージをログに記録するためにどのvalが変更されたかを調べる必要があります。
Ben Carp

返信いただきありがとうございます。「渡した関数内で、どのvalに変更があったかを把握する必要があります」と言った場合。どうやってそれを達成できるか。ご案内ください
Tarun Nagpal

6

このようなシナリオを正確に解決するreact-deltaを公開しました。私の意見でuseEffectは、責任が多すぎます。

責任

  1. 次のコマンドを使用して、依存関係配列のすべての値を比較します Object.is
  2. #1の結果に基づいて、エフェクト/クリーンアップコールバックを実行します

責任の分割

react-delta休憩useEffectいくつかの小さなフックへの責任。

責任#1

責任#2

私の経験では、このアプローチはより柔軟でクリーンで簡潔です useEffect / useRefソリューション。


ちょうど私が探しているもの。やってみよう!
hackerl33t

本当に良さそうですが、ちょっとやりすぎではありませんか?
Hisato

@Hisatoやりすぎかもしれません。これは実験的なAPIのようなものです。率直に言って、広く知られていたり採用されたりしていないため、チーム内ではあまり使用していません。理論的にはそれはいい感じに聞こえますが、実際にはそれだけの価値はないかもしれません。
オースティンマレルバ

3

状態は機能コンポーネントのコンポーネントインスタンスと密に結合されていないuseEffectため、たとえばを使用して最初に保存しないと、以前の状態に到達できませんuseRef。これはまた、以前の状態が内部で利用できるため、状態の更新が間違った場所に誤って実装された可能性があることも意味しますsetStateアップデーター関数ます。

これはuseReducerReduxのようなストアを提供し、それぞれのパターンを実装できる良いユースケースです。状態の更新は明示的に実行されるため、どの状態プロパティが更新されているかを知る必要はありません。これはディスパッチされたアクションからすでに明らかです。

以下はそのです。

function reducer({ sendAmount, receiveAmount, rate }, action) {
  switch (action.type) {
    case "sendAmount":
      sendAmount = action.payload;
      return {
        sendAmount,
        receiveAmount: sendAmount * rate,
        rate
      };
    case "receiveAmount":
      receiveAmount = action.payload;
      return {
        sendAmount: receiveAmount / rate,
        receiveAmount,
        rate
      };
    case "rate":
      rate = action.payload;
      return {
        sendAmount: receiveAmount ? receiveAmount / rate : sendAmount,
        receiveAmount: sendAmount ? sendAmount * rate : receiveAmount,
        rate
      };
    default:
      throw new Error();
  }
}

function handleChange(e) {
  const { name, value } = e.target;
  dispatch({
    type: name,
    payload: value
  });
}

...
const [state, dispatch] = useReducer(reducer, {
  rate: 2,
  sendAmount: 0,
  receiveAmount: 0
});
...

こんにちは@estus、このアイデアをありがとう。それは私に別の考え方を与えます。しかし、それぞれのケースでAPIを呼び出す必要があることを忘れていました。そのための解決策はありますか?
rwinzhang

どういう意味ですか?内部のhandleChange?「API」はリモートAPIエンドポイントを意味しますか?答えからcodesandboxを詳細で更新できますか?
Estus Flask

更新から、sendAmountそれらを計算するのではなく非同期にフェッチするなどしたいと思いますよね?これは大きく変わります。これを行うことは可能ですuseReduceが、注意が必要な場合があります。Reduxを扱う前に、非同期操作が単純ではないことがわかるかもしれません。github.com/reduxjs/redux-thunkは、非同期アクションを可能にするReduxの人気のある拡張機能です。これは、同じパターン(useThunkReducer)でuseReducerを拡張するデモ、codesandbox.io / s / 6z4r79ymwrです。ディスパッチされた関数はasyncであることに注意してください。そこでコメントをリクエストできます。
Estus Flask

1
質問の問題は、あなたが本当のことではない別のことについて尋ね、それを変更したことです。そのため、既存の回答は質問を無視したかのように見えますが、今は非常に異なる質問です。これはSOで推奨される方法ではありません。これは、必要な答えを得るのに役立たないためです。回答がすでに参照している質問(計算式)から重要な部分を削除しないでください。。あなたはまだ問題が(私の以前のコメント(あなたはおそらく行う)した後、以前の試みとして、これとあなたのケースとのリンクを反映した新しい質問を考慮している場合
Estusフラスコ

こんにちはestus、私はこのcodesandbox codesandbox.io/s/6z4r79ymwrがまだ更新されていないと思います。申し訳ありませんが、質問を元に戻します。
rwinzhang

3

Refを使用すると、アプリに新しい種類のバグが発生します。

usePrevious以前に誰かがコメントしたことを使用して、このケースを見てみましょう:

  1. prop.minTime:5 ==> ref.current = 5 | 参照電流を設定
  2. prop.minTime:5 ==> ref.current = 5 | 新しい値はref.currentに等しい
  3. prop.minTime:8 ==> ref.current = 5 | 新しい値はref.currentと等しくありません
  4. prop.minTime:5 ==> ref.current = 5 | 新しい値はref.currentに等しい

ここでわかるように、ref使用しているので内部を更新していませんuseEffect


1
Reactにはこの例がありますが、古いプロップについて気にかけると... stateではなく...を使用しているためprops、エラーが発生します。
サントメゴンザロ

3

本当に簡単な小道具比較の場合、useEffectを使用して、小道具が更新されたかどうかを簡単に確認できます。

const myComponent = ({ prop }) => {
  useEffect(() => {
     ---Do stuffhere----
    }
  }, [prop])

}

その後、propが変更された場合にのみuseEffectがコードを実行します。


1
これは1回のみ機能しpropます。連続して変更すると、機能しなくなります~ do stuff here ~
KarolisŠarapnickis'10年

2
setHasPropChanged(false)最後に~ do stuff here ~を呼び出して状態を「リセット」する必要があるようです。(ただし、これは追加の再レンダリングでリセットされます)
Kevin Wang

フィードバックをありがとう、あなたはどちらも正しい、更新されたソリューション
Charlie Tupman

@ AntonioPavicevac-Ortiz答えを更新して、propHasChangedをtrueとしてレンダリングします。これにより、レンダリング時に一度呼び出され、useEffectを取り出してプロップをチェックするだけの優れたソリューションとなる可能性があります
Charlie Tupman

これの私の元の使用は失われたと思います。コードを振り返ると、useEffectを使用できます
Charlie Tupman

2

受け入れられた答えを出して、カスタムフックを必要としない代替ソリューション:

const Component = (props) => {
  const { receiveAmount, sendAmount } = props;
  const prevAmount = useRef({ receiveAmount, sendAmount }).current;
  useEffect(() => {
    if (prevAmount.receiveAmount !== receiveAmount) {
     // process here
    }
    if (prevAmount.sendAmount !== sendAmount) {
     // process here
    }
    return () => { 
      prevAmount.receiveAmount = receiveAmount;
      prevAmount.sendAmount = sendAmount;
    };
  }, [receiveAmount, sendAmount]);
};

1
良い解決策ですが、構文エラーが含まれています。 useRefないuserRef。currentの使用を忘れたprevAmount.current
Blake Plumb

0

これは私が使用するカスタムフックで、を使用するよりも直感的ですusePrevious

import { useRef, useEffect } from 'react'

// useTransition :: Array a => (a -> Void, a) -> Void
//                              |_______|  |
//                                  |      |
//                              callback  deps
//
// The useTransition hook is similar to the useEffect hook. It requires
// a callback function and an array of dependencies. Unlike the useEffect
// hook, the callback function is only called when the dependencies change.
// Hence, it's not called when the component mounts because there is no change
// in the dependencies. The callback function is supplied the previous array of
// dependencies which it can use to perform transition-based effects.
const useTransition = (callback, deps) => {
  const func = useRef(null)

  useEffect(() => {
    func.current = callback
  }, [callback])

  const args = useRef(null)

  useEffect(() => {
    if (args.current !== null) func.current(...args.current)
    args.current = deps
  }, deps)
}

useTransition次のように使用します。

useTransition((prevRate, prevSendAmount, prevReceiveAmount) => {
  if (sendAmount !== prevSendAmount || rate !== prevRate && sendAmount > 0) {
    const newReceiveAmount = sendAmount * rate
    // do something
  } else {
    const newSendAmount = receiveAmount / rate
    // do something
  }
}, [rate, sendAmount, receiveAmount])

お役に立てば幸いです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.