Как я могу получить доступ к состоянию в useEffect без повторного запуска useEffect? - PullRequest
2 голосов
/ 03 августа 2020

Мне нужно добавить несколько обработчиков событий, которые взаимодействуют с объектом вне React (например, Google Maps). Внутри этой функции-обработчика я хочу получить доступ к некоторому состоянию, которое я могу отправить этому внешнему объекту.

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

Если я не передаю состояние в качестве зависимости, обработчик добавления / удаления добавляется соответствующее количество раз ( по сути один раз), но состояние никогда не обновляется (или, точнее, обработчик не может получить последнее состояние).

Пример Codepen:

Возможно, лучше всего объяснить с помощью Codepen: https://codepen.io/cjke/pen/dyMbMYr?editors=0010

const App = () => {
  const [n, setN] = React.useState(0);

  React.useEffect(() => {
    const os = document.getElementById('outside-react')
    const handleMouseOver = () => {
      // I know innerHTML isn't "react" - this is an example of interacting with an element outside of React
      os.innerHTML = `N=${n}`
    }
    
    console.log('Add handler')
    os.addEventListener('mouseover', handleMouseOver)
    
    return () => {
      console.log('Remove handler')
      os.removeEventListener('mouseover', handleMouseOver)
    }
  }, []) // <-- I can change this to [n] and `n` can be accessed, but add/remove keeps getting invoked

  return (
    <div>
      <button onClick={() => setN(n + 1)}>+</button>
    </div>
  );
};

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

Сводка

Если список dep для эффекта равен [n], состояние обновляется, но добавление / удаление обработчика добавляется / удаляется для каждого изменение состояния. Если список dep для эффекта равен [], обработчик добавления / удаления работает отлично, но состояние всегда равно 0 (начальное состояние).

Мне нужно сочетание обоих. Доступ к состоянию, но только к useEffect один раз (как если бы зависимость была []).

Изменить: дополнительные пояснения

Я знаю, как решить эту проблему с помощью методов жизненного цикла, но не уверен, как это может работать с хуками.

Если бы это был компонент класса, он бы выглядит так:


class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = { n: 0 };
  }

  handleMouseOver = () => {
    const os = document.getElementById("outside-react");
    os.innerHTML = `N=${this.state.n}`;
  };
  
  componentDidMount() {
    console.log("Add handler");
    const os = document.getElementById("outside-react");
    os.addEventListener("mouseover", this.handleMouseOver);
  }

  componentWillUnmount() {
    console.log("Remove handler");
    const os = document.getElementById("outside-react");
    os.removeEventListener("mouseover", handleMouseOver);
  }

  render() {
    const { n } = this.state;

    return (
      <div>
        <strong>Info:</strong> Click button to update N in state, then hover the
        orange box. Open the console to see how frequently the handler is
        added/removed
        <br />
        <button onClick={() => this.setState({ n: n + 1 })}>+</button>
        <br />
        state inside react: {n}
      </div>
    );
  }
}

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

Обратите внимание на то, что обработчик добавления / удаления добавляется только один раз (очевидно, игнорируя тот факт, что компонент приложения не размонтирован), несмотря на изменение состояния.

I Я ищу способ повторить это с помощью крючков

Ответы [ 5 ]

1 голос
/ 05 августа 2020

Вы можете использовать изменяемые ссылки , чтобы отделить чтение текущего состояния от зависимостей эффектов:

const [n, setN] = useState(0);
const nRef = useRef(n); // define mutable ref

useEffect(() => { nRef.current = n }) // nRef is updated after each render
useEffect(() => {
  const handleMouseOver = () => {
    os.innerHTML = `N=${nRef.current}` // n always has latest state here
  }
 
  os.addEventListener('mouseover', handleMouseOver)
  return () => { os.removeEventListener('mouseover', handleMouseOver) }
}, []) // no need to set dependencies

const App = () => {
  const [n, setN] = React.useState(0);
  const nRef = React.useRef(n); // define mutable ref

  React.useEffect(() => { nRef.current = n }) // nRef.current is updated after each render
  React.useEffect(() => {
    const os = document.getElementById('outside-react')
    const handleMouseOver = () => { 
      os.innerHTML = `N=${nRef.current}`  // n always has latest state here
    } 

    os.addEventListener('mouseover', handleMouseOver)
    return () => { os.removeEventListener('mouseover', handleMouseOver) }
  }, []) // no need to set dependencies 

  return (
    <div>
      <button onClick={() => setN(prev => prev + 1)}>+</button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<div id="outside-react">div</div>
<p>Update counter with + button, then mouseover the div to see recent counter state.</p>

Слушатель событий будет добавлен / удален только один раз при монтировании / размонтировании. Текущее состояние n можно прочитать внутри useEffect, не устанавливая его как зависимость ([] deps), поэтому повторного запуска при изменениях не происходит.

Вы можете думать о useRef как об изменяемом экземпляре переменные для функциональных компонентов и хуков. Эквивалентом в компонентах класса будет контекст this - поэтому this.state.n в handleMouseOver примера компонента класса всегда возвращает последнее состояние и работает.

Есть отличный пример от Дан Абрамов демонстрирует вышеуказанный образец с setInterval. Сообщение в блоге также иллюстрирует потенциальные проблемы с useCallback и когда слушатель событий считывается / удаляется при каждом изменении состояния.

Другими полезными примерами являются (глобальные) обработчики событий, такие как os.addEventListener или интеграция с внешними библиотеками / фреймворки по краям React.

Примечание: Документы React рекомендуют экономно использовать этот шаблон . С моей точки зрения, это жизнеспособная альтернатива в ситуациях, когда вам просто нужно «последнее состояние» - независимо от обновлений цикла рендеринга React. Используя изменяемые переменные, мы выходим из области закрытия функции с потенциально устаревшими значениями закрытия.

Запись состояния независимо от зависимостей имеет другие альтернативы - вы можете взглянуть на Как для регистрации события с помощью хуков useEffect? ​​ для получения дополнительной информации

1 голос
/ 05 августа 2020

Что происходит, функция закрывается на n, но хотя при закрытии обычно видны обновления переменной, переменные-ловушки воссоздаются все время, задерживая закрытие.

В компонентах на основе хуков состояние равно присваивает новую переменную для каждого рендеринга, который функция слушателя никогда не закрывается, а закрытие не обновляется, поскольку вы создаете функцию только один раз при монтировании (с пустым массивом зависимостей). Напротив, в компонентах на основе классов this остается неизменным, поэтому закрытие может видеть изменения.

Я не считаю постоянное добавление и удаление слушателей проблемой. Учтите тот факт, что если вы не используете useCallback() для создания своих обработчиков событий (что следует делать только с запомненными дочерними элементами, иначе это преждевременная оптимизация) в повседневных событиях реакции, React сам буквально сделает именно это, а именно удалит предыдущую функцию и установить новую функцию.

0 голосов
/ 05 августа 2020

Есть некоторые проблемы с тем, как вы пытаетесь решить эту конкретную проблему.

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

Это работает, потому что функции обработчика обновляются до cc. до последнего значения n при вызове useeffect.

Если я не передаю состояние в качестве зависимости, обработчик добавления / удаления добавляется соответствующее количество раз (по существу, один раз ), но состояние никогда не обновляется (или, точнее, обработчик не может получить последнее состояние).

Это потому, что функции-обработчики не получили текущее значение для n

Использование refs здесь может быть преимуществом, так как значение будет сохраняться при ч / б рендеринге. Посмотрите этот пример здесь: https://codesandbox.io/s/wispy-pond-j80j7?file= / src / App. js

export default function App() {
  const [n, setN] = React.useState(0);
  const nRef = React.useRef(0);
  const outsideReactRef = React.useRef(null);

  const handleMouseOver = React.useCallback(() => {
    outsideReactRef.current.innerHTML = `N=${nRef.current}`;
  }, []);

  React.useEffect(() => {
    outsideReactRef.current = document.getElementById("outside-react");

    console.log("Add handler");
    outsideReactRef.current.addEventListener("mouseover", handleMouseOver);

    return () => {
      console.log("Remove handler");
      outsideReactRef.current.removeEventListener("mouseover", handleMouseOver);
    };
  }, []); // <-- I can change this to [n] and `n` can be accessed, but add/remove keeps getting invoked

  return (
    <div>
      <button
        onClick={() =>
          setN(n => {
            const newN = n + 1;
            nRef.current = newN;
            return newN;
          })
        }
      >
        +
      </button>
    </div>
  );
}
0 голосов
/ 05 августа 2020

Единственный способ получить последнее значение - указать его как зависимость, и вот причины этого

  1. зачем добавлять или удалять вызываемые снова и снова?

    Каждый при изменении зависимости она повторно выполняет всю функцию

  2. Почему значение n не обновляется?

    каждый раз при визуализации функционального компонента все назначения будут повторно - происходит так же, как обычная функция, поэтому значение ссылочного объекта, в котором хранится 'n = 0', останется неизменным, и при каждом последующем рендеринге будет создаваться новый объект, который будет указывать на обновленное значение

0 голосов
/ 05 августа 2020

ну, вы можете использовать window объект или global объект, чтобы назначить переменную, которую вы хотите, используя useEffect, например:

try{
  const App = () => {
  const [n, setN] = React.useState(0);
    
    React.useEffect(()=>{
      window.num = n
    },[n])

  React.useEffect(() => {
      const os = document.getElementById('outside-react')
     const handleMouseOver =() => {
      os.innerHTML = `N=${window.num}`
    }
    console.log('Add handler')
    os.addEventListener('mouseover', handleMouseOver)
    return () => {
      console.log('Remove handler')
      os.removeEventListener('mouseover', handleMouseOver)
    }
  }, []) // <-- I can change this to [n] and it works, but add/remove keeps getting invoked

  return (
    <div>
      <strong>Info:</strong> Click button to update N in state, then hover the orange box. Open the console to see how frequently the handler is added/removed
      <br/>
      <button onClick={() => setN(n + 1)}>+</button>
      <br/>
      state inside react: {n}
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
}
catch(error){
console.log(error.message)
}
<div id="outside-react">OUTSIDE REACT - hover to get state</div>
<div id="root"></div>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

это изменится window.num при изменении состояния n

...