Run Effect hook only when both dependencies change

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

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

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.