Почему мой компонент отображается дважды при использовании контекстного API React и хука useEffect?

#javascript #reactjs #use-effect #react-context

#javascript #reactjs #использование-эффект #реагировать-контекст

Вопрос:

Я пытаюсь понять, как контекстный API работает вместе с хуками React (в частности useEffect , и useReducer ).
Я не уверен, почему один из моих компонентов ( ContactListPage ), похоже, отображается дважды.

Минимальный пример для демонстрации:

index.js

 ReactDOM.render(
  <ContactContextProvider>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </ContactContextProvider>,
  document.getElementById("root")
);
  

contact-context.js

 export const ContactContext = createContext();

function reducer(state, action) {
  switch (action.type) {
    case "FETCH_CONTACTS": {
      return { ...state, contacts: action.payload };
    }
    default:
      throw new Error();
  }
}

export const ContactContextProvider = (props) => {
  const [state, dispatch] = useReducer(reducer, { contacts: [] });
  const store = React.useMemo(() => ({ state, dispatch }), [state, dispatch]);

  return (
    <ContactContext.Provider value={store}>
      {props.children}
    </ContactContext.Provider>
  );
};
  

ContactListPage.js

 export default function ContactListPage() {
  const { state, dispatch } = useContext(ContactContext);
  console.log(state);

  useEffect(() => {
    const fetchData = async () => {
      try {
        // Make Ajax request for contacts here
        dispatch({
          type: "FETCH_CONTACTS",
          payload: [{ name: "Bob" }]
        });
      } catch (error) {
        // handle error
      }
    };
    fetchData();
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div>
      <h1>List of Contacts</h1>
    </div>
  );
}
  

Вот приложение, запущенное в CodeSandbox.

При первом ContactListPage рендеринге компонента в консоль записывается следующее:

 Object {contacts: Array[0]}
Object {contacts: Array[1]}
  

Для меня это имеет смысл. Сначала contacts массив пуст, затем в useEffect перехвате в ContactListPage компоненте я отправляю действие для добавления контакта, поэтому компонент повторно отображается с обновленным списком контактов.

Проблема возникает, когда я ухожу от этого представления (я использую React Router), а затем возвращаюсь назад. В этом случае я вижу:

 Object {contacts: Array[1]}
Object {contacts: Array[1]}
  

Я бы ожидал console.log , что оператор сработает только один раз, поскольку массив контактов не изменился.

Я потратил весь день на поиск в Google. Ни один из других ответов, которые я мог найти на SO, не сработал для меня.

Итак, мой вопрос: почему console.log оператор всегда запускается дважды?


Примечание. Чтобы воспроизвести это в CodeSandbox:

  • Нажмите на ссылку Добавить контакт и просмотрите первый вывод в консоли CodeSandbox.
  • Нажмите на ссылку Список контактов и просмотрите второй вывод в консоли CodeSandbox.

Ответ №1:

Таким useEffect образом, реализация здесь происходит, когда компонент отображается в первый раз. Вот когда вы получаете результат:

 Object {contacts: Array[0]} Object {contacts: Array[1]}
  

Поскольку вы все еще находитесь в приложении, действие, отправленное при первом рендеринге компонента, сохраняется в состоянии. Поскольку вы удалили компонент. т.Е. Компонент больше не находится в dom, когда вы снова загружаете компонент в dom, следовательно, после первоначального рендеринга useEffect выполняется и отображает ваш компонент после завершения эффекта, потому что вы отправляете другое действие в редуктор, даже если у вас есть значение.

Поскольку отправляется новое действие, это рассматривается как изменение состояния, и именно тогда вы видите следующее:

 Object {contacts: Array[1]} Object {contacts: Array[1]}
  

Вы можете проверить это, добавив console.log в эту строку в свой редуктор:

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

Итак, я добавил следующую проверку в вашем useEffect :

  if (state.contacts.length === 0) {
      fetchData();
 }
  

И Эврика !!! На один рендеринг меньше.

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

Ответ №2:

Это потому, что ваш компонент снова монтируется при изменении маршрута. Итак, когда вы переходите от contact list к add contact :

Ваш редуктор уже установил его на name:bob

И теперь снова, когда вы возвращаетесь к своему component list компоненту. Происходит две вещи: компонент рендерится из-за useContext начального состояния (перехваты также вызывают повторный рендеринг), а другая — из-за вашего useEffect , т.е. Вы отправляете свой FETCH_CONTACTS , когда компонент смонтирован.

Если вы видите консоль при первой загрузке:

 
Object {contacts: Array[0]} 
Object {contacts: Array[1]}

  

Обратите внимание, что консоль возвращает два оператора: первое состояние — это когда контакт пуст, а второе состояние — когда контакт обновляется с помощью reducer]

Теперь, когда вы переходите со страницы добавления контакта, консоль показывает это:

 Object {contacts: Array[1]}
Object {contacts: Array[1]}

  

Оба имеют одинаковое значение, первое из которых исходит из начального состояния контекста, а второе приходит снова, поскольку reducer снова обновляет значение.

Если вы хотите увидеть себя, добавьте консоль больше для ясности. Проверьте здесь, как монтируется и размонтируется компонент: https://codesandbox.io/s/elegant-ishizaka-enck6?file=/src/components/ContactListPage.js:478-492