Реагировать useEffect Hook, когда изменяется только один из эффектов, но не остальные - PullRequest
3 голосов
/ 17 апреля 2019

У меня есть функциональный компонент, использующий зацепки:

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

  // 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 (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>
  );
}

Компонент получает несколько items при монтировании и сохраняет их в состояние.

Компонент получает itemId prop (от React Router).

Всякий раз, когда меняется props.itemId, я хочу, чтобы это вызывало эффект, в этом случае регистрируя его на консоли.


Проблема в том, что, посколькуэффект также зависит от items, эффект также будет запускаться всякий раз, когда изменяется items, например, когда items повторно выбирается нажатием кнопки.

Это можно исправить, сохранив предыдущийprops.itemId в отдельной переменной состояния и сравнение двух, но это похоже на хак и добавляет шаблон.Используя классы компонентов, это решается путем сравнения текущего и предыдущего реквизита в componentDidUpdate, но это невозможно при использовании функциональных компонентов, что является обязательным требованием для использования крючков.


Что является лучшимспособ вызвать эффект, зависящий от нескольких параметров, только при изменении одного из параметров?


PS.Крючки - это что-то новое, и я думаю, что мы все изо всех сил стараемся понять, как правильно с ними работать, поэтому, если мой способ мышления об этом кажется вам неправильным или неудобным, пожалуйста, укажите на это.

Ответы [ 3 ]

3 голосов
/ 17 апреля 2019

React Team говорит, что лучший способ получить предыдущие значения - это использовать 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>
  );
}

Я думаю, что это может помочь в вашем случае.

Примечание:если вам не нужно предыдущее значение, другой подход - написать еще один useEffect для props.itemId

React.useEffect(() => {
  console.log('track changes for itemId');
}, [props.itemId]);
0 голосов
/ 17 апреля 2019

Я только что попробовал это сам, и мне кажется, что вам не нужно помещать вещи в список зависимостей useEffect, чтобы иметь их обновленные версии.Это означает, что вы можете просто вставить props.itemId и по-прежнему использовать items в эффекте.

Я создал здесь фрагмент, чтобы попытаться доказать / проиллюстрировать это.Дайте мне знать, если что-то не так.

const Child = React.memo(props => {
  const [items, setItems] = React.useState([]);
  const fetchItems = () => {
    setTimeout(() => {
      setItems((old) => {
        const newItems = [];
        for (let i = 0; i < old.length + 1; i++) {
          newItems.push(i);
        }
        return newItems;
      })
    }, 1000);
  }
  
  React.useEffect(() => {
    console.log('OLD (logs on both buttons) id:', props.id, 'items:', items.length);
  }, [props.id, items]);
  
  React.useEffect(() => {
    console.log('NEW (logs on only the red button) id:', props.id, 'items:', items.length);
  }, [props.id]);

  return (
    <div
      onClick={fetchItems}
      style={{
        width: "200px",
        height: "100px",
        marginTop: "12px",
        backgroundColor: 'orange',
        textAlign: "center"
      }}
    >
      Click me to add a new item!
    </div>
  );
});

const Example = () => {
  const [id, setId] = React.useState(0);

  const updateId = React.useCallback(() => {
    setId(old => old + 1);
  }, []);

  return (
    <div style={{ display: "flex", flexDirection: "row" }}>
      <Child
        id={id}
      />
      <div
        onClick={updateId}
        style={{
          width: "200px",
          height: "100px",
          marginTop: "12px",
          backgroundColor: 'red',
          textAlign: "center"
        }}
      >Click me to update the id</div>
    </div>
  );
};

ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>
0 голосов
/ 17 апреля 2019

Из приведенного примера ваш эффект зависит не от items и itemId, а от одного из предметов из коллекции.

Да, вам нужно items и itemId, чтобы получить этот элемент, но это не значит, что у вас есть , чтобы указать их в массиве зависимостей.

Чтобы убедиться, что он выполняется только при изменении целевого элемента, вы должны передать этот элемент в массив зависимостей, используя ту же логику поиска.

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