18

The new React API includes useEffect(), the second argument of which takes an Object which React diffs to see if the component updated.

e.g.

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

where [props.source] is the argument in question.

My question is: can I define a custom function to run to check if the prop has changed?

I have a custom object and React can't seem to tell when it has changed.

1
  • 1
    Hey, yep,here's a link to GitHub code to where he does a custom equality check. Commented Jan 8, 2019 at 21:52

3 Answers 3

10

AFAIK it's not currently possible. There are some workarounds:

1) Do deep comparison manually inside useEffect. To store the prev. value you may use useState or, even better, useRef as demonstrated here: https://overreacted.io/a-complete-guide-to-useeffect/

2) Hashing with JSON.stringify(props.source). Can be fine, if the data is not too big. Note that stringify may produce inconsistent results (keys in objects changing order will change the output).

3) Hashing with md5(props.source) (or some other quick/light hashing). More realiable yet slower than the previous.

Sign up to request clarification or add additional context in comments.

7 Comments

Can you clarify the first point of manual deep comparison? I tried using a function which returned a custom comparison, but no luck.
The video is not free. Says I need to sign up, which costs $250 or $40 per month. It's not cool that answer here costs a minimum $40 to view!
@CraigV it was free when I posted the answer. They probably changed the terms. You can check LevelUp tutorials on YouTube as a free alternative. They've started a great series on hooks recently.
Ok. I replaced the link. Now it's Dan's Abramov article on the same topic.
I have the same doubt as @ColinRicardo . Can you clarify the first point of manual deep comparison? Using the useRef? I didn't get the idea. Way too abstract. Thanks!
|
5

Credit for this answer goes to @Tholle use object in useEffect 2nd param without having to stringify it to JSON

const { useState, useEffect, useRef } = React;
const { isEqual } = _;

function useDeepEffect(fn, deps) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps);

  useEffect(() => {
    const isSame = prevDeps.current.every((obj, index) =>
      isEqual(obj, deps[index])
    );

    if (isFirst.current || !isSame) {
      fn();
    }

    isFirst.current = false;
    prevDeps.current = deps;
  }, deps);
}

function App() {
  const [state, setState] = useState({ foo: "foo" });

  useEffect(() => {
    setTimeout(() => setState({ foo: "foo" }), 1000);
    setTimeout(() => setState({ foo: "bar" }), 2000);
  }, []);

  useDeepEffect(() => {
    console.log("State changed!");
  }, [state]);

  return <div>{JSON.stringify(state)}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Why use useRef instead of useState

By using the function returned from useState the component will be re-rendered, which is not desired in this case

Comments

1

Taking inspiration from other answers, I've been using the following hook

import React from 'react';

import IsEqual from 'lodash/isEqual';

/**
 * The role of this function is to ensure that useEffect doesn't not
 * fire unless the value of a varr (variable but "var" is a reserved word 😉)
 * changes. For examples:
 * const a = { 'hi': 5 }
 * const b = { 'hi': 5 }
 *
 * a === b // false
 * IsEqual(a, b) // true
 *
 * By default the React.useEffect(() => {}, [dependencies]) only does "===" but
 * for cases where we only want useEffect to re-fire when the true value
 * of a varr changes "IsEqual", we use this.
 *
 * @param varr: any
 * @returns {function(): *}
 */
const useMostRecentImmutableValue = (varr) => {
  let mostRecentValue = varr;
  const mostRecentPointer = React.useRef(varr);

  return () => {
    // short circuit if "shallow equality"
    if (mostRecentPointer.current === varr) {
      return mostRecentValue;
    }

    // mostRecentValue only updates when the true value of varr changes
    if (!IsEqual(varr, mostRecentPointer.current)) {
      mostRecentValue = varr;
    }

    // mostRecentPointer changes every time "shallow equality" fails but
    // after we've checked deep equality
    mostRecentPointer.current = varr;

    return mostRecentValue;
  };
};

export default useMostRecentImmutableValue;

And then in a hook I would do something like this:

import useMostRecentImmutableValue from 'use-most-recent-immutable-value.hook';

const useMyHook = (foo = { 'hi': 5 }) => {
  const mostRecentFoo = useMostRecentImmutableValue(foo);

  React.useEffect(() => {
    const unsubscribe = mySubscriptionFunction(mostRecentFoo);
    return () => {
      unsubscribe();
    };
  }, [mostRecentFoo]);

};

export default useMyHook;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.