javascript reactjs react-hooks

javascript - Reacción incorrecta engancha el comportamiento con el detector de eventos



reactjs react-hooks (1)

Estoy jugando con los ganchos React y enfrenté un problema. Muestra el estado incorrecto cuando intento iniciar la consola con el botón manejado por el detector de eventos.

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

  1. Haga clic en el botón ''Agregar tarjeta'' 2 veces
  2. En la primera tarjeta, haga clic en Button1 y vea en la consola que hay 2 tarjetas en estado (comportamiento correcto)
  3. En la primera tarjeta, haga clic en Button2 (manejado por el detector de eventos) y vea en la consola que solo hay 1 tarjeta en estado (comportamiento incorrecto)

¿Por qué muestra el estado equivocado? En la primera tarjeta, Button2 debería mostrar 2 tarjetas en la consola. ¿Algunas ideas?

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") );

Yo uso React 16.7.0-alpha.0 y Chrome 70.0.3538.110

Por cierto, si reescribo el CardsProvider usando una clase, el problema desaparece. CodeSandbox utilizando la clase: https://codesandbox.io/s/w2nn3mq9vl


Este es un problema común para los componentes funcionales que utilizan useState hook. Las mismas preocupaciones son aplicables a cualquier función de devolución de llamada donde se useState estado de estado de uso, por ejemplo, las funciones de temporizador setTimeout o setInterval .

Los controladores de eventos se tratan de forma diferente en los componentes CardsProvider y Card .

handleCardClick y handleButtonClick utilizados en el componente funcional CardsProvider se definen en su alcance. Hay nuevas funciones cada vez que se ejecuta, se refieren al estado de las cards que se obtuvo en el momento en que se definieron. Los controladores de eventos se vuelven a registrar cada vez que se CardsProvider componente CardsProvider .

handleCardClick utilizado en el componente funcional de la Card se recibe como apoyo y se registra una vez en el montaje del componente con useEffect . Es la misma función durante toda la vida útil del componente y se refiere al estado obsoleto que estaba handleCardClick en el momento en que se definió la función handleCardClick la primera vez. handleButtonClick se recibe como prop y se vuelve a registrar en cada procesamiento de Card , es una función nueva cada vez y se refiere a un estado nuevo.

Estado mutable

Un enfoque común que resuelve este problema es usar useRef lugar de useState . Una referencia es básicamente una receta que proporciona un objeto mutable que se puede pasar por referencia:

const ref = useRef(0); function eventListener() { ref.current++; }

En caso de que un componente se vuelva a representar en la actualización de estado como se espera de useState , las referencias no son aplicables.

Es posible mantener las actualizaciones de estado y el estado mutable por separado, pero forceUpdate se considera un antipatrón en los componentes de clase y función (enumerados solo como referencia):

const useForceUpdate = () => { const [, setState] = useState(); return () => setState({}); } const ref = useRef(0); const forceUpdate = useForceUpdate(); function eventListener() { ref.current++; forceUpdate(); }

Función de actualización de estado

Una solución es utilizar la función de actualización de estado que recibe un estado nuevo en lugar de un estado obsoleto desde el ámbito del contenedor:

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

En caso de que se necesite un estado para efectos secundarios síncronos como console.log , una solución es devolver el mismo estado para evitar una actualización.

function eventListener() { setState(freshState => { console.log(freshState); return freshState; }); } useEffect(() => { // register eventListener once }, []);

Esto no funciona bien con los efectos secundarios asíncronos, en particular las funciones async .

Registro de oyente de eventos manual

Otra solución es volver a registrar el detector de eventos cada vez, por lo que una devolución de llamada siempre obtiene un estado nuevo desde el alcance del contenedor:

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

Manejo de eventos incorporado

A menos que el detector de eventos esté registrado en el document , la window u otros objetivos del evento estén fuera del alcance del componente actual, el manejo de eventos del propio DOM de React debe usarse siempre que sea posible, esto elimina la necesidad de useEffect :

<button onClick={eventListener} />

En el último caso, el detector de eventos se puede memorizar adicionalmente con useMemo o useCallback para evitar repeticiones innecesarias cuando se pasa como un accesorio:

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

La edición anterior de la respuesta sugirió usar el estado mutable que es aplicable a la implementación inicial de useState enganche en la versión React 16.7.0-alpha, pero no es viable en la implementación final de React 16.8. Actualmente, useState solo admite el estado inmutable.