Làm cách nào để so sánh oldValues ​​và newValues ​​trên React Hook sử dụngEffect?


149

Giả sử tôi có 3 đầu vào: tỷ lệ, sendAmount và receiveAmount. Tôi đặt 3 đầu vào đó vào các thông số khác nhau của useEffect. Các quy tắc là:

  • Nếu sendAmount thay đổi, tôi tính receiveAmount = sendAmount * rate
  • Nếu receiveAmount thay đổi, tôi tính sendAmount = receiveAmount / rate
  • Nếu tỷ lệ thay đổi, tôi tính receiveAmount = sendAmount * ratekhi nào sendAmount > 0hoặc tôi tính sendAmount = receiveAmount / ratekhi nàoreceiveAmount > 0

Dưới đây là mãandbox https://codesandbox.io/s/pkl6vn7x6j để trình bày vấn đề.

Có cách nào để so sánh oldValuesnewValuesthích componentDidUpdatethay vì làm 3 xử lý cho trường hợp này không?

Cảm ơn


Đây là giải pháp cuối cùng của tôi với usePrevious https://codesandbox.io/s/30n01w2r06

Trong trường hợp này, tôi không thể sử dụng nhiều useEffectvì mỗi thay đổi sẽ dẫn đến cùng một cuộc gọi mạng. Đó là lý do tại sao tôi cũng sử dụng changeCountđể theo dõi sự thay đổi. Điều này changeCountcũng hữu ích để theo dõi các thay đổi chỉ từ cục bộ, vì vậy tôi có thể ngăn cuộc gọi mạng không cần thiết vì những thay đổi từ máy chủ.


Chính xác thì thành phầnDidUpdate sẽ giúp như thế nào? Bạn vẫn sẽ cần phải viết 3 điều kiện này.
Estus Flask

Tôi đã thêm một câu trả lời với 2 giải pháp tùy chọn. Có một trong số họ làm việc cho bạn?
Cá chép Ben

Câu trả lời:


207

Bạn có thể viết một hook tùy chỉnh để cung cấp cho bạn một đạo cụ trước đó bằng cách sử dụng useRef

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

và sau đó sử dụng nó trong 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])
}

Tuy nhiên, rõ ràng hơn và có thể tốt hơn và rõ ràng hơn để đọc và hiểu nếu bạn sử dụng hai useEffectriêng biệt cho mỗi id thay đổi mà bạn muốn xử lý chúng riêng biệt


8
Cảm ơn bạn đã lưu ý sử dụng hai useEffectcuộc gọi riêng biệt. Không biết bạn có thể sử dụng nó nhiều lần!
JasonH

3
Tôi đã thử mã ở trên, nhưng eslint đang cảnh báo tôi rằng useEffectthiếu phụ thuộcprevAmount
Littlee

2
Vì tiền tố giữ giá trị cho giá trị trước đó của trạng thái / đạo cụ, bạn không cần chuyển nó dưới dạng phụ thuộc để sử dụngEffect và bạn có thể tắt cảnh báo này cho trường hợp cụ thể. Bạn có thể đọc bài viết này để biết thêm chi tiết?
Shubham Khatri

Yêu cái này - useRef luôn luôn đến để giải cứu.
Fernando Rojo

@ShubhamKhatri: Lý do cho việc sử dụng gói ref.civerse = value là gì?
xoăn_brackets

40

Bất cứ ai cũng đang tìm kiếm một phiên bản TypeScript của usePreingly:

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
Lưu ý rằng điều này sẽ không hoạt động trong tệp TSX, để làm cho nó hoạt động trong thay đổi tệp TSX const usePrevious = <T extends any>(...để làm cho trình thông dịch thấy rằng <T> không phải là JSX và là một hạn chế chung
apokryfos

1
Bạn có thể giải thích tại sao <T extends {}>sẽ không làm việc. Nó dường như đang làm việc cho tôi nhưng tôi đang cố gắng hiểu sự phức tạp của việc sử dụng nó như thế.
Karthikeyan_kk

.tsxcác tệp chứa jsx có nghĩa là giống hệt với html. Có thể là chung <T>là một thẻ thành phần không được tiết lộ.
SeedyROM

Những gì về loại trở lại? Tôi nhận đượcMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang

@SabaAhang Xin lỗi! TS sẽ sử dụng suy luận kiểu để xử lý hầu hết các loại trả về, nhưng nếu bạn bật tính năng linting, tôi đã thêm loại trả về để loại bỏ các cảnh báo.
SeedyROM

25

Tùy chọn 1 - hookCompare hook

So sánh giá trị hiện tại với giá trị trước đó là một mẫu phổ biến và biện minh cho một móc tùy chỉnh của chính nó che giấu các chi tiết triển khai.

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>;
};

Bản giới thiệu

Tùy chọn 2 - chạy useEffect khi giá trị thay đổi

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

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

Bản giới thiệu


Lựa chọn thứ hai làm việc cho tôi. Bạn có thể vui lòng hướng dẫn tôi tại sao tôi phải viết useEffect Twice không?
Tarun Nagpal

@TarunNagpal, bạn không nhất thiết phải sử dụng useEffect hai lần. Nó phụ thuộc vào trường hợp sử dụng của bạn. Hãy tưởng tượng rằng chúng ta chỉ muốn đăng nhập mà val đã thay đổi. Nếu chúng ta có cả val1 và val2 trong mảng phụ thuộc của mình, nó sẽ chạy mỗi khi bất kỳ giá trị nào thay đổi. Sau đó, bên trong hàm chúng ta sẽ vượt qua, chúng ta sẽ phải tìm ra val nào đã thay đổi để ghi thông điệp chính xác.
Ben Carp

Cảm ơn vi đa trả lơi. Khi bạn nói "bên trong hàm chúng ta sẽ vượt qua, chúng ta sẽ phải tìm ra giá trị nào đã thay đổi". Làm thế nào chúng ta có thể đạt được nó. Xin hướng dẫn
Tarun Nagpal

6

Tôi vừa xuất bản Reac-delta để giải quyết loại kịch bản chính xác này. Theo tôi, useEffectcó quá nhiều trách nhiệm.

Trách nhiệm

  1. Nó so sánh tất cả các giá trị trong mảng phụ thuộc của nó bằng cách sử dụng Object.is
  2. Nó chạy các cuộc gọi lại hiệu ứng / dọn dẹp dựa trên kết quả của # 1

Phá vỡ trách nhiệm

react-deltaphá vỡ useEffecttrách nhiệm của mình thành một số móc nhỏ hơn.

Trách nhiệm số 1

Trách nhiệm số 2

Theo kinh nghiệm của tôi, cách tiếp cận này linh hoạt, sạch sẽ và ngắn gọn hơn useEffect/ useRefgiải pháp.


Chỉ là những gì tôi đang tìm kiếm. Hãy thử đi!
hackerl33t

trông thực sự tốt nhưng nó không phải là một chút quá mức?
Hisato

@Hisato nó rất tốt có thể là quá mức cần thiết. Đó là một cái gì đó của một API thử nghiệm. Và thẳng thắn, tôi đã không sử dụng nó nhiều trong các đội của mình vì nó không được biết đến hoặc chấp nhận rộng rãi. Về lý thuyết nghe có vẻ tốt, nhưng trong thực tế, nó có thể không xứng đáng.
Austin Malerba

3

Vì trạng thái không được kết hợp chặt chẽ với thể hiện thành phần trong các thành phần chức năng, ví dụ, trạng thái trước đó không thể được truy cập useEffectmà không lưu nó trước useRef. Điều này cũng có nghĩa là cập nhật trạng thái có thể được thực hiện không chính xác ở vị trí sai vì trạng thái trước đó có sẵn bên trong setStatechức năng cập nhật.

Đây là trường hợp sử dụng tốt để useReducercung cấp cửa hàng giống Redux và cho phép triển khai mẫu tương ứng. Cập nhật trạng thái được thực hiện rõ ràng, do đó không cần phải tìm ra tài sản nhà nước nào được cập nhật; điều này đã rõ ràng từ hành động được gửi đi.

Đây là một ví dụ giống như:

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
});
...

Xin chào @estus, cảm ơn vì ý tưởng này. Nó cho tôi một cách suy nghĩ khác. Nhưng, tôi đã quên đề cập rằng tôi cần gọi API cho từng trường hợp. Bạn có giải pháp nào cho việc đó không?
rwinzhang

Ý anh là gì? Bên trong tay cầm Thay đổi? 'API' có nghĩa là điểm cuối API từ xa? Bạn có thể cập nhật mã và hộp từ câu trả lời với các chi tiết?
Estus Flask

Từ bản cập nhật tôi có thể giả sử rằng bạn muốn tìm nạp sendAmount, vv không đồng bộ thay vì tính toán chúng, phải không? Điều này thay đổi rất nhiều. Có thể làm điều này với useReducenhưng có thể khó khăn. Nếu bạn đã xử lý Redux trước khi bạn có thể biết rằng các hoạt động không đồng bộ không đơn giản ở đó. github.com/reduxjs/redux-thunk là một tiện ích mở rộng phổ biến cho Redux cho phép thực hiện các hành động không đồng bộ. Đây là bản demo tăng cường sử dụngReducer với cùng một mẫu (useThunkReducer), scriptsandbox.io/s/6z4r79ymwr . Lưu ý rằng chức năng được gửi là async, bạn có thể thực hiện các yêu cầu ở đó (nhận xét).
Estus Flask

1
Vấn đề với câu hỏi là bạn đã hỏi về một điều khác không phải là trường hợp thực sự của bạn và sau đó thay đổi nó, vì vậy bây giờ câu hỏi rất khác trong khi các câu trả lời hiện có xuất hiện như thể họ chỉ bỏ qua câu hỏi. Đây không phải là cách thực hành được khuyến nghị trên SO vì nó không giúp bạn có câu trả lời bạn muốn. Xin vui lòng, không loại bỏ các phần quan trọng khỏi câu hỏi mà câu trả lời đã đề cập (công thức tính toán). Nếu bạn vẫn gặp sự cố (sau nhận xét trước của tôi (bạn có thể làm như vậy), hãy xem xét đặt câu hỏi mới phản ánh trường hợp của bạn và liên kết với câu hỏi này như nỗ lực trước đó của bạn.
Estus Flask

Xin chào estus, tôi nghĩ rằng mã này và các mã hộp thư và hộp.io / s / 6z4r79ymwr chưa được cập nhật. Tôi sẽ thay đổi lại câu hỏi, xin lỗi vì điều đó.
rwinzhang

3

Sử dụng Ref sẽ giới thiệu một loại lỗi mới vào ứng dụng.

Hãy xem trường hợp này bằng cách sử dụng usePreviousai đó đã nhận xét trước:

  1. prop.minTime: 5 ==> ref.cản = 5 | đặt ref.c hiện tại
  2. prop.minTime: 5 ==> ref.cản = 5 | giá trị mới bằng ref.c hiện tại
  3. prop.minTime: 8 ==> ref.cản = 5 | giá trị mới KHÔNG bằng ref.c hiện tại
  4. prop.minTime: 5 ==> ref.cản = 5 | giá trị mới bằng ref.c hiện tại

Như chúng ta có thể thấy ở đây, chúng tôi không cập nhật nội bộ refvì chúng tôi đang sử dụnguseEffect


1
React có ví dụ này nhưng họ đang sử dụng state... và không phải props... khi bạn quan tâm đến các đạo cụ cũ thì lỗi sẽ xảy ra.
santomegonzalo

3

Để so sánh prop thực sự đơn giản, bạn có thể sử dụng useEffect để dễ dàng kiểm tra xem liệu prop có được cập nhật hay không.

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

}

useEffect sau đó sẽ chỉ chạy mã của bạn nếu prop thay đổi.


1
Điều này chỉ hoạt động một lần, sau propnhững thay đổi liên tiếp, bạn sẽ không thể~ do stuff here ~
Karolis arapnickis

2
Có vẻ như bạn nên gọi setHasPropChanged(false)vào cuối ~ do stuff here ~để "thiết lập lại" trạng thái của bạn. (Nhưng điều này sẽ thiết lập lại trong một lần tái xuất thêm)
Kevin Wang

Cảm ơn phản hồi, cả hai bạn đều đúng, giải pháp cập nhật
Charlie Tupman

@ AntonioPavicevac-Ortiz Tôi đã cập nhật câu trả lời cho đến bây giờ kết xuất propHasChanged là đúng mà sau đó sẽ gọi nó một lần khi kết xuất, có thể là một giải pháp tốt hơn chỉ để loại bỏ useEffect và chỉ cần kiểm tra prop
Charlie Tupman

Tôi nghĩ rằng việc sử dụng ban đầu của tôi đã bị mất. Nhìn lại mã bạn chỉ có thể sử dụng useEffect
Charlie Tupman

2

Bỏ qua câu trả lời được chấp nhận, một giải pháp thay thế không yêu cầu móc tùy chỉnh:

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
Giải pháp tốt, nhưng chứa lỗi cú pháp. useRefkhông userRef. Quên sử dụng hiện tạiprevAmount.current
Blake Plumb

0

Đây là một hook tùy chỉnh mà tôi sử dụng mà tôi tin là trực quan hơn so với sử dụng 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)
}

Bạn sẽ sử dụng useTransitionnhư sau.

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])

Mong rằng sẽ giúp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.