15

I have a React component that fetches data using the useEffect hook like so:

const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
    const [data, setData] = useState();
    useEffect(() => {
        const fetchedData; // fetch data using key and options
        setData(fetchedData);
        cache[key] = fetchedData;
    }, [key, options])
    return <p>{data}</p>;
}

This runs the hook every time key or options change. However, I'm also caching the data locally, and only want the effect to run when both key AND options change (since for each key/options combination the data will always be the same).

Is there a clean way to depend on the combination of key AND options rather than key OR options using React Hooks?

vis
  • 494
  • 3
  • 9

4 Answers4

11

You can create this sort of logic with useRef(). Consider the following example and sandbox: https://codesandbox.io/s/react-hooks-useeffect-with-multiple-reqs-6ece5

const App = () => {
  const [name, setName] = useState();
  const [age, setAge] = useState();

  const previousValues = useRef({ name, age });

  useEffect(() => {
    if (
      previousValues.current.name !== name &&
      previousValues.current.age !== age
    ) {
      //your logic here
      console.log(name + " " + age);
      console.log(previousValues.current);

      //then update the previousValues to be the current values
      previousValues.current = { name, age };
    }
  });

  return (
    <div>
      <input
        placeholder="name"
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <input
        placeholder="age"
        value={age}
        onChange={e => setAge(e.target.value)}
      />
    </div>
  );
};

Workflow:

  1. We create a ref object for the two values we want to keep track of, in this case its a name and age. The ref object is previousValues.
  2. useEffect is defined but we do not provide it any dependencies. Instead, we just have it execute whenever there is a state-change to name or age.
  3. Now inside useEffect we have conditional logic to check whether the previous/initial values of both name and age are different than their corresponding state-values. If they are then good we execute our logic (console.log).
  4. Lastly after executing the logic, update the ref object (previousValues) to the current values (state).
Chris Ngo
  • 15,460
  • 3
  • 23
  • 46
7

In order to run the effect when both values change, you need to make use of the previous values and compare them within the hook when either key or options change.

You can write a usePrevious hook and compare old and previous state as mentioned in this post:

How to compare oldValues and newValues on React Hooks useEffect?

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

const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
    const [data, setData] = useState();
    const previous = usePrevious({key, options});
    useEffect(() => {
        if(previous.key !== key && previous.options !== options) {
            const fetchedData; // fetch data using key and options
            setData(fetchedData);
            cache[key] = fetchedData;
        }
    }, [key, options])
    return <p>{data}</p>;
}
montrealist
  • 5,593
  • 12
  • 46
  • 68
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
2

All provided solutions are perfectly fine, However there are some more complex situation e.g., When useEffect function should be called ONLY when dependency A and B changed while it also depends on C's value.

So I suggest using sequence of useEffects and intermediate States to provide more space for future logics. Implementation of this approach for asked question would be:

const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
    const [data, setData] = useState();

    const [needsUpdate, setNeedsUpdate] = useState(()=>({key:false, option:false}));

    useEffect(()=>{
      setNeedsUpdate((needsUpdate)=>({...needsUpdate, key:true}));
    },[key])

    useEffect(()=>{
      setNeedsUpdate((needsUpdate)=>({...needsUpdate, options:true}));
    },[options])

    useEffect(() => {
      if (needsUpdate.key && needsUpdate.options){
        const fetchedData; // fetch data using key and options
        setData(fetchedData);
        cache[key] = fetchedData;
        setNeedsUpdate(()=>({key:false, option:false}));
      }
    }, [needsUpdate, key, options])
    return <p>{data}</p>;
}

In this way we can apply almost any logic on our useEffect dependencies, However it has own drawbacks which is few more rendering cycle.

Makan
  • 641
  • 8
  • 13
0

You can create a new custom hook which calls the callback with an argument with index/names of dependencies

const useChangesEffect = (callback, dependencies, dependencyNames = null) => {
  const prevValues = useRef(dependencies);

  useEffect(() => {
    const changes = [];
    for (let i = 0; i < prevValues.current.length; i++) {
      if (!shallowEqual(prevValues.current[i], dependencies[i])) {
        changes.push(dependencyNames ? dependencyNames[i] : i);
      }
    }
    callback(changes);
    prevValues.current = dependencies;
  }, dependencies);
};


useChangesEffect((changes) => {
  if (changes.includes(0)) {
    console.log('dep1 changed');
  }
  if (changes.includes(1)) {
    console.log('dep2 changed');
  }
}, [dep1, dep2]);
Ever Dev
  • 1,882
  • 2
  • 14
  • 34