React useEffect Hook when only one of the effect's deps changes, but not the others

The React Team says that the best way to get prev values is to use useRef: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state

function Component(props) {
  const [ items, setItems ] = useState([]);

  const prevItemIdRef = useRef();
  useEffect(() => {
    prevItemIdRef.current = props.itemId;
  });
  const prevItemId = prevItemIdRef.current;

  // In a callback Hook to prevent unnecessary re-renders 
  const handleFetchItems = useCallback(() => {
    fetchItemsFromApi().then(setItems);
  }, []);

  // Fetch items on mount
  useEffect(() => {
    handleFetchItems();
  }, []);

  // I want this effect to run only when 'props.itemId' changes,
  // not when 'items' changes
  useEffect(() => {
    if(prevItemId !== props.itemId) {
      console.log('diff itemId');
    }

    if (items) {
      const item = items.find(item => item.id === props.itemId);
      console.log("Item changed to " item.name);
    }
  }, [ items, props.itemId ])

  // Clicking the button should NOT log anything to console
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}

I think that this could help in your case.

Note: if you don't need the previous value, another approach is to write one useEffect more for props.itemId

React.useEffect(() => {
  console.log('track changes for itemId');
}, [props.itemId]);

⚠️ NOTE: This answer is currently incorrect and could lead to unexpected bugs / side-effects. The useCallback variable would need to be a dependency of the useEffect hook, therefore leading to the same problem as OP was facing.

I will address it asap

Recently ran into this on a project, and our solution was to move the contents of the useEffect to a callback (memoized in this case) - and adjust the dependencies of both. With your provided code it looks something like this:

function Component(props) {
  const [ items, setItems ] = useState([]);

  const onItemIdChange = useCallback(() => {
    if (items) {
      const item = items.find(item => item.id === props.itemId);
      console.log("Item changed to " item.name);
    }
  }, [items, props.itemId]);

  // I want this effect to run only when 'props.itemId' changes,
  // not when 'items' changes
  useEffect(onItemIdChange, [ props.itemId ]);

  // Clicking the button should NOT log anything to console
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}

So the useEffect just has the ID prop as its dependency, and the callback both the items and the ID.

In fact you could remove the ID dependency from the callback and pass it as a parameter to the onItemIdChange callback:

const onItemIdChange = useCallback((id) => {
  if (items) {
    const item = items.find(item => item.id === id);
    console.log("Item changed to " item.name);
  }
}, [items]);

useEffect(() => {
  onItemIdChange(props.itemId)
}, [ props.itemId ]) 

I am a react hooks beginner so this might not be right but I ended up defining a custom hook for this sort of scenario:

const useEffectWhen = (effect, deps, whenDeps) => {
  const whenRef = useRef(whenDeps || []);
  const initial = whenRef.current === whenDeps;
  const whenDepsChanged = initial || !whenRef.current.every((w, i) => w === whenDeps[i]);
  whenRef.current = whenDeps;
  const nullDeps = deps.map(() => null);

  return useEffect(
    whenDepsChanged ? effect : () => {},
    whenDepsChanged ? deps : nullDeps
  );
}

It watches a second array of dependencies (which can be fewer than the useEffect dependencies) for changes & produces the original useEffect if any of these change.

Here's how you could use (and reuse) it in your example instead of useEffect:

// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffectWhen(() => {
  if (items) {
    const item = items.find(item => item.id === props.itemId);
    console.log("Item changed to " item.name);
  }
}, [ items, props.itemId ], [props.itemId])

Here's a simplified example of it in action, useEffectWhen will only show up in the console when the id changes, as opposed to useEffect which logs when items or id changes.

This will work without any eslint warnings, but that's mostly because it confuses the eslint rule for exhaustive-deps! You can include useEffectWhen in the eslint rule if you want to make sure you have the deps you need. You'll need this in your package.json:

"eslintConfig": {
  "extends": "react-app",
  "rules": {
    "react-hooks/exhaustive-deps": [
      "warn",
      {
        "additionalHooks": "useEffectWhen"
      }
    ]
  }
},

and optionally this in your .env file for react-scripts to pick it up:

EXTEND_ESLINT=true