Неправильное поведение React перехватывает с прослушивателем событий - PullRequest
0 голосов
/ 19 декабря 2018

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

CodeSandbox: https://codesandbox.io/s/lrxw1wr97m

  1. Нажмите на 'Кнопка добавления карты 2 раза
  2. В первой карте нажмите кнопку 1 и увидите в консоли, что в состоянии находятся 2 карты (правильное поведение)
  3. В первой карте нажмите кнопку 2 (обрабатываетсяпрослушиватель событий) и увидите в консоли, что в состоянии только 1 карта (неправильное поведение)

Почему отображается неправильное состояние?В первой карте Button2 должен отображать 2 карты в консоли.Есть идеи?

import React, { useState, useContext, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const CardsContext = React.createContext();

const CardsProvider = props => {
  const [cards, setCards] = useState([]);

  const addCard = () => {
    const id = cards.length;
    setCards([...cards, { id: id, json: {} }]);
  };

  const handleCardClick = id => console.log(cards);
  const handleButtonClick = id => console.log(cards);

  return (
    <CardsContext.Provider
      value={{ cards, addCard, handleCardClick, handleButtonClick }}
    >
      {props.children}
    </CardsContext.Provider>
  );
};

function App() {
  const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
    CardsContext
  );

  return (
    <div className="App">
      <button onClick={addCard}>Add card</button>
      {cards.map((card, index) => (
        <Card
          key={card.id}
          id={card.id}
          handleCardClick={() => handleCardClick(card.id)}
          handleButtonClick={() => handleButtonClick(card.id)}
        />
      ))}
    </div>
  );
}

function Card(props) {
  const ref = useRef();

  useEffect(() => {
    ref.current.addEventListener("click", props.handleCardClick);
    return () => {
      ref.current.removeEventListener("click", props.handleCardClick);
    };
  }, []);

  return (
    <div className="card">
      Card {props.id}
      <div>
        <button onClick={props.handleButtonClick}>Button1</button>
        <button ref={node => (ref.current = node)}>Button2</button>
      </div>
    </div>
  );
}

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

Я использую React 16.7.0-alpha.0 и Chrome 70.0.3538.110

Кстати, если я переписываю CardsProvider с помощью класса, проблема исчезнет,CodeSandbox используя класс: https://codesandbox.io/s/w2nn3mq9vl

1 Ответ

0 голосов
/ 19 декабря 2018

Это общая проблема для функциональных компонентов, которые используют useState hook.Те же проблемы применимы к любым функциям обратного вызова, где используется состояние useState, например, setTimeout или setInterval функции таймера .

Обработчики событий обрабатываются по-разному в CardsProvider иCard компоненты.

handleCardClick и handleButtonClick, используемые в CardsProvider функциональном компоненте, определены в его области действия.Каждый раз, когда он запускается, появляются новые функции, они ссылаются на состояние cards, которое было получено в момент их определения.Обработчики событий перерегистрируются каждый раз при визуализации компонента CardsProvider.

handleCardClick, используемый в Card функциональном компоненте, принимается как реквизит и регистрируется один раз при монтировании компонента с помощью useEffect.Это та же самая функция в течение всего срока службы компонента, и она относится к устаревшему состоянию, которое было свежим в то время, когда функция handleCardClick была определена впервые.handleButtonClick принимается как реквизит и перерегистрируется при каждом рендере Card, это новая функция каждый раз и относится к свежему состоянию.

Изменяемое состояние

Общий подход, который обращается кэта проблема заключается в использовании useRef вместо useState.Ссылка - это, по сути, рецепт, который предоставляет изменяемый объект, который может быть передан по ссылке:

const ref = useRef(0);

function eventListener() {
  ref.current++;
}

В случае, если компонент должен быть повторно визуализирован при обновлении состояния, как это ожидается от useState, refs arenНеприменимо.

Можно хранить обновления состояния и изменяемое состояние отдельно, но forceUpdate считается антипаттерном как для компонентов класса, так и для компонентов функций (перечислены только для справки):

const useForceUpdate = () => {
  const [, setState] = useState();
  return () => setState({});
}

const ref = useRef(0);
const forceUpdate = useForceUpdate();

function eventListener() {
  ref.current++;
  forceUpdate();
}

Функция обновления состояния

Одним из решений является использование функции обновления состояния, которая получает свежее состояние вместо устаревшего состояния из охватывающей области действия:

function eventListener() {
  // doesn't matter how often the listener is registered
  setState(freshState => freshState + 1);
}

В случае, если требуется состояние для синхронного побочного эффекта, такого как console.log, обходной путь - вернуть то же состояние, чтобы предотвратить обновление.

function eventListener() {
  setState(freshState => {
    console.log(freshState);
    return freshState;
  });
}

useEffect(() => {
  // register eventListener once
}, []);

Это не очень хорошо работает с асинхронными побочными эффектами, в частности async функциями.

Ручная перерегистрация прослушивателя событий

Другим решением является повторная регистрациярегистрируйте прослушиватель событий каждый раз, поэтому обратный вызов всегда получает новое состояние из окружающей области:

function eventListener() {
  console.log(state);
}

useEffect(() => {
  // register eventListener on each state update
}, [state]);

Встроенная обработка событий

Если прослушиватель событий не зарегистрирован в document, windowили другие цели событий находятся вне области действия текущего компонента, при необходимости следует использовать собственную обработку событий DOM в React, что устраняет необходимость в useEffect:

<button onClick={eventListener} />

В последнем случае слушатель событий можетдополнительно запомните с помощью useMemo или useCallback, чтобы предотвратить ненужные повторные рендеринг, когда он передается как реквизит:

const eventListener = useCallback(() => {
  console.log(state);
}, [state]);

В предыдущей редакции ответа предлагалось использовать изменяемое состояние, применимое кначальная реализация useState hook в версии React 16.7.0-alpha, но не работоспособна в финальной реализации React 16.8.useState в настоящее время поддерживает только неизменяемое состояние.

...