React.useState NO es la única herramienta para manejar el estado en React. Estudiarlas a profundidad te ayudará a descifrar cuándo vale la pena usar cada una.
En este tutorial vamos a estudiar cómo aplicar la programación declarativa al manejo del estado con React Hooks.
Tanto useState como useReducer tienen el mismo objetivo: manejar el estado local de nuestros componentes. La diferencia está en el camino que nos ofrecen para llegar a una solución. No hay ganadores, simplemente hay herramientas y paradigmas que se adaptan mejor a nuestros objetivos.
La palabra paradigma significa forma de pensar. Los paradigmas de programación son formas de pensar para resolver un problema con código. Entender que NO hay formas de pensar correctas o incorrectas es fundamental para crecer profesionalmente y convertirte en la mejor desarrolladora que puedes ser.
Hay paradigmas que se adaptan muy bien a diferentes situaciones, así que usarlos nos da cierta facilidad para resolver cierto tipo de problemas. Saber cuándo usar un paradigma u otro nos hace muy eficientes para resolver todo tipo de problemas. Pero cerrarnos a una sola forma de pensar nos hará mil veces menos eficientes.
Teniendo esto claro, vamos a estudiar los diferentes paradigmas que representan los React Hooks que manejan el estado de nuestros componentes: useState y useReducer.
Nuestro proyecto será una caja de confirmación con los siguientes requerimientos:
Aquí puedes ver el demo de la aplicación: juandc.co/usestate-vs-usereducer/. El código de seguridad es “paradigma”. 😉
Primero nos encargaremos de la UI. Luego añadiremos la lógica y el estado a nuestro componente.
<>
{isActive && (
{/* … */}
)}
{!isActive && (
{/* … */}
)}
</>
{isActive && (
<divclassName="ActiveBox"><h2className="Box-title">Eliminar UseState</h2>
{!isPasswordCorrect && (
{/* … */}
)}
{isPasswordCorrect && (
{/* … */}
)}
</div>
)}
{!isPasswordCorrect && (
<>
<p>
Por favor, escribe el código de seguridad para comprobar que quieres eliminar.
</p>
{isLoading && <p>Cargando...</p>}
{hasError && <p>Error...</p>}
<input placeholder="Código de Seguridad" />
<button onClick={/* … */}>Comprobar</button>
</>
)}
<button onClick={/*
- si la contraseña es correcta, `isPasswordCorrect` debe
volverse false
- si estamos esperando una respuesta de la API, `isLoading` debe
volverse true
- si la API indica que el código es incorrecto, `hasError` debe
volverse false
*/}>
{isPasswordCorrect && (
<><p>¿Seguro que quieres eliminar?</p><buttononClick={/* isActivedebevolversetrue */}>
Sí, eliminar
</button><buttononClick={/*
debemosvolveralestadoinicial, elusuariodebevolveraescribirlacontraseñaynohaymensajesdeerrorocargando
*/}>
No, volver
</button></>
)}
{!isActive && (
<div><h2>Eliminación exitosa</h2><buttononClick={/* isActivedebevolversetrue */}>
Recuperar, cancelar la eliminación
</button></div>
)}
Puedes ver el código completo (hasta el momento) aquí: github.com/juandc/usestate-vs-usereducer/blob/master/ui.js.
A partir de aquí debemos construir la lógica de nuestro componente. Vamos a hacerlo primero con useState y luego con useReducer.
Los componentes creados como clases solo tienen un estado, el que guardamos en this.state
. Pero useState
funciona un poco diferente. Podemos crear todos los estados independientes que necesitemos.
Veamos un ejemplo con el input donde el usuario va a escribir el código de seguridad:
// Con clases:classComponentextendsReact.Component{
state = { confirmationCode: '' };
setConfirmationCode = (e) => {
this.setState({
confirmationCode: e.currentTarget.value,
});
}
render {
return (
<>
{/* ... */}
<input
value={this.state.confirmationCode}
onChange{this.setConfirmationCode}
/>
{/* ... */}
</>
);
}
}
// Con Hooks:
function Component(props) {
const [confirmationCode, setConfirmationCode] = useState('');
const setConfirmationCodeOnChange = (e) => {
setConfirmationCode(e.currentTarget.value);
}
return (
<>
{/* ... */}
<input
value={confirmationCode}
onChange{setConfirmationCodeOnChange}
/>
{/* ... */}
</>
);
}
Ahora añadamos los controladores de los mensajes de error y cargando:
// Con clases:classComponentextendsReact.Component{
state = {
confirmationCode: '',
isLoading: false,
hasError: false,
};
setConfirmationCode = (e) => {/* … */}
toggleLoading = (newIsLoading) => {
this.setState({ isLoading: newIsLoading });
}
toggleError = (newError) => {
this.setState({ hasError: newError });
}
render {
const {
confirmationCode,
isLoading,
hasError,
} = this.state;
return (
<>
{/* ... */}
{isLoading && <p>Cargando...</p>}
{hasError && <p>Error...</p>}
<input
value={confirmationCode}
onChange{this.setConfirmationCode}
/>
{/* ... */}
</>
);
}
}
// Con Hooks:
function Component(props) {
const [confirmationCode, setConfirmationCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const setConfirmationCodeOnChange = (e) => {
setConfirmationCode(e.currentTarget.value);
};
return (
<>
{/* ... */}
{isLoading && <p>Cargando...</p>}
{hasError && <p>Error...</p>}
<input
value={confirmationCode}
onChange{setConfirmationCodeOnChange}
/>
{/* ... */}
</>
);
}
Cuando usamos Clases debemos entender el estado como un solo objeto con todo el estado del componente. En cambio, al usar Hooks podemos manejar independientemente cada elemento de nuestro componente por defecto.
Vamos a dejar hasta aquí la comparación entre Clases y Hooks. Solo recuerda que useState
nos permite separar la lógica de cada elemento de nuestro componente, algo que en muchos casos puede resultar bastante cómodo.
👉 ¿Sabías que compilar un componente de tipo clase es más trabajo para Babel?
👉 ¿Cuándo crear un Componente? Estructura, Organización y Tipos de Componentes en React
Sigamos construyendo nuestro componente. Recuerda que nuestra UI es muy interactiva, así que debemos crear la lógica de muchos más controladores.
Continuemos con el botón encargado de comprobar que el código que ha escrito el usuario sea correcto. Para esto necesitamos un nuevo estado que nos indique si el usuario ha escrito bien la contraseña, lo llamaremos isPasswordCorrect
. Y también vamos a crear una función para manejar la lógica de este botón, la llamaremos confirmPasswordOnClick
.
¡Hola! Vengo del pasado para recordarte que si la contraseña es correcta,
isPasswordCorrect
debe volverse false; si estamos esperando una respuesta de la API (o lo que se supone que debería ser una API),isLoading
debe volverse true; y si la API indica que el código es incorrecto,hasError
debe volverse false.
Recuerda que esta función debe ser asíncrona, ya que (en teoría) aquí llamamos una API. Y antes de la comprobación debemos dejar al componente en estado de carga.
functionComponent(props) {
const [confirmationCode, setConfirmationCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [isPasswordCorrect, setIsPasswordCorrect] = useState(false);
const setConfirmationCodeOnChange = (e) => {/* … */};
const confirmPasswordOnClick = async () => {
// Empezamos cargando. Aún no mostramos si hay errores,// apenas vamos a comprobar si la clave es correcta:
setIsLoading(true);
setHasError(false);
// `sampleFetch` debería ser la petición a la API,// yo voy a usar la funcion “sleep”:// https://flaviocopes.com/javascript-sleep/const isCorrect = await sampleFetch(confirmationCode);
if (isCorrect) {
setIsPasswordCorrect(true);
} else {
setIsPasswordCorrect(false);
setHasError(true);
}
// Confirmamos el código, podemos quitar el mensaje// de cargando:
setIsLoading(false);
};
return (
<>
{/* ... */}
{isLoading && <p>Cargando...</p>}
{hasError && <p>Error...</p>}
<input
value={confirmationCode}
onChange{setConfirmationCodeOnChange}
/>
<button onClick={confirmPasswordOnClick}>
Comprobar
</button>
{/* ... */}
</>
);
}
Bueno, tampoco es taaaan complicado. Solo debemos modificar los valores de 3 estados diferentes dependiendo de 3 posibles situaciones diferentes. ¡Pero no te emociones! Aún falta una última confirmación antes de poder realizar la eliminación.
Cuando isPasswordCorrect
sea true debemos esconder el formulario. Y en su lugar, le mostraremos al usuario los botones de continuar o cancelar.
<button
onClick={/* isActive debe volverse true */}
>
Sí, eliminar
</button>
<button onClick={
{/*
debemos volver al estado inicial, el usuario
debe volver a escribir la contraseña y no hay
mensajes de error o cargando
*/}
}>
No, volver
</button>
Enfoquémonos en el botón de cancelar.
El usuario se está arrepintiendo. Esto en código significa que debemos retroceder un paso atrás y cambiar el valor de isPasswordCorrect
a false
para retroceder hasta la pantalla de escribir el código de seguridad.
¡PEEEERO!
Ir SOLO un paso atrás sería un error, ya que nuestro input seguiría teniendo el código correcto. ¡No lo hemos borrado! No hemos reseteado el valor de confirmationCode
. Así que eso es exactamente lo que vamos a hacer: una función para devolver todos nuestros estados a su valor inicial.
functionComponent(props) {
// ...const resetAllStates = () => {
setConfirmationCode('');
setIsLoading(false);
setHasError(false);
setIsPasswordCorrect(false);
};
return (
{/* ... */}
{isPasswordCorrect && (
<><p>¿Seguro que quieres eliminar?</p><buttononClick={/* … */}>
Sí, eliminar
</button><buttononClick={resetAllStates}>
No, volver
</button></>
)}
{/* ... */}
);
}
Ahora necesitamos un nuevo estado para verificar si la eliminación ha sido realizada, lo llamaremos isActive
. Al crearlo tendremos todo listo para confirmar la eliminación: la función setIsActive
.
functionComponent(props) {
const [confirmationCode, setConfirmationCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [isPasswordCorrect, setIsPasswordCorrect] = useState(false);
const [isActive, setIsActive] = useState(true);
const setConfirmationCodeOnChange = (e) => {/* … */};
const confirmPasswordOnClick = (e) => {/* … */};
const resetAllStates = () => {/* … */};
return (
{/* ... */}
{isPasswordCorrect && (
<><p>¿Seguro que quieres eliminar?</p><buttononClick={() => setIsActive(false)}>
Sí, eliminar
</button><buttononClick={resetAllStates}>
No, volver
</button></>
)}
{/* ... */}
);
}
¡Perfecto! Tenemos todo listo para construir la última sección de nuestro componente: la pantalla de la eliminación completada. No hay nuevos estados que crear. Solo debemos convertir isActive
en true
.
functionComponent(props) {
// ...return (
{/* ... */}
{!isActive && (
<div><h2>Eliminación exitosa</h2><buttononClick={() => setIsActive(true)>
Recuperar, cancelar la eliminación
</button></div>
)}
{/* ... */}
);
}
¡Listo! Pero, espera. Cuando usamos el botón de recuperar solo volvemos a la segunda pantalla, la que nos pregunta si estamos seguros de eliminar.
¿Por que pasa esto?
Así como antes, todos los estados siguen con sus últimos valores. Debemos avisarles que, de nuevo, deben volver a sus valores iniciales. Hay dos formas de hacerlo:
La primera (y un poco más compleja) es usar el Hook useEffect
para “escuchar” los cambios de isActive
y llamar a la función de resetAllStates
cuando isActive
se esté convirtiendo en true
.
useEffect(() => {
if (isActive) {
resetAllStates();
}
}, [isActive]);
Esta es la mejor solución si debemos comunicarnos con alguna API para revertir los últimos cambios, porque podrás hacer el llamado a la API justo antes de resetear todos los estados. Incluso podrías mostrar un mensaje de loading mientras obtienes respuestas.
Pero hay otra forma mucho más sencilla y que podemos usar cuando NO necesitemos hacer llamados al backend para volver al estado anterior. ¿Recuerdas la función resetAllStates
? Bueno, podemos modificarla un poco para resetear también el estado de isActive
.
const reset = () => {
setConfirmationCode('');
setIsLoading(false);
setHasError(false);
setIsPasswordCorrect(false);
setIsActive(true);
};
¡Y listo! Ahora sí nuestro componente funciona totalmente. Solo te aviso que si los requerimientos cambian, situación para la que debemos estar preparados, tendremos que hacer cambios en muchos lugares.
Esto se debe a que estamos trabajando con estados independientes, pero que se relacionan muy fuertemente con una interfaz que cambia mucho y que a final de cuentas depende de todos estos estados.
Pero no te preocupes. Vamos a refactorizar un poco con ayuda de useReducer
.
Puedes encontrar el código completo de nuestro componente en: github.com/juandc/usestate-vs-usereducer/blob/master/usestate.js.
¡Te dije que esto iba de paradigmas!
Al programar de forma imperativa debemos escribir código que nos indica paso a paso cómo evoluciona nuestra aplicación. En cambio, al programar de forma declarativa le damos más importancia a qué vamos hacer, luego nos encargaremos de cómo lo debemos hacer.
Apliquemos este concepto en el estado de nuestros componentes.
Nuestro problema es que trabajamos con muchos estados independientes, pero debemos cambiarlos a casi todos cuando algo pasa en nuestra interfaz. Y si los requerimientos cambian, vamos a necesitar nuevos estados y aumentar la complejidad de nuestro componente.
Pero podemos verlo desde otro punto de vista.
¿Qué tal si pensamos como un usuario de nuestra aplicación? ¿Qué tal si no tuviéramos que llamar a todas esas funciones para actualizar cada uno de los estados? ¿Qué tal si pensamos en eventos en vez de valores de los estados?
En otras palabras, pensemos en las acciones o eventos que los usuarios perciben de la aplicación: escribir la contraseña, confirmar si es correcta, mostrar un mensaje de error si es incorrecta, cancelar, recuperar, etc.
Esta es la forma de pensar con useReducer
.
Lo primero que haremos será crear una función reducer para definir las acciones o eventos de nuestro componente. Recibimos dos parámetros:
state
: el estado de nuestro componente antes de realizar algún cambio.action
: un objeto con estos otros dos elementos.type
el nombre del evento que cambiará nuestro estado.payload
: es opcional, lo podemos utilizar para enviar información extra a la actualización de estado.En esta función vamos a definir todos los eventos de nuestro componente utilizando un switch.
functionreducer(state, action) {
switch(action.type) {
case ‘NOMBRE_DEL_EVENTO’: {
return {/* NUEVO ESTADO */};
}
case ‘NOMBRE_DE_OTRO_EVENTO’: {
return {/* OTRO NUEVO ESTADO */};
}
}
}
Esta función la debemos enviar como primer argumento del Hook useReducer. Además, debemos definir un objeto con el estado inicial. Este Hook nos devolverá el estado (state) y una función dispatch
para enviar eventos y actualizar el estado.
const initialState = { /* … */ };
const [state, dispatch] = useReducer(reducer, initialState);
El primer paso para implementar useReducer
es definir el estado inicial. Vamos a usar los mismos nombres que usamos en useState
.
const initialState = {
confirmationCode: '',
isLoading: false,
hasError: false,
isPasswordCorrect: false,
isActive: true,
};
Luego de esto vamos a crear una función confirmationReducer
para definir las acciones o eventos que perciben los usuarios en la aplicación.
WRITE_CONFIRMATION_CODE
: llamaremos a este evento cada vez que el usuario escriba el código de confirmación.START_CONFIRMATION
: el usuario debe ver un mensaje de “cargando”. Aún no sabemos si el código es correcto o incorrecto, así que no mostraremos ningún error.CONFIRMATION_FAILED
: si el código de confirmación es incorrecto debemos apagar el mensaje de cargando, asegurarnos de que isPasswordCorrect
siga siendo false
y mostrar un mensaje de error para que el usuario vuelva a intentar.CONFIRMATION_SUCCESS
: si el código de confirmación es correcto debemos cambiar isPasswordCorrect
a true
, ocultar todos los mensajes de “cargando” o “error” y mostrar la vista de “¿estás seguro de que quieres eliminar?”.DEACTIVATE
: si el usuario acepta eliminar debemos convertir isActive
a false
.RESET
: el usuario debe poder cancelar la eliminación en cualquier momento.const confirmationReducer = (state, action) => {
switch (action.type) {
case'WRITE_CONFIRMATION_CODE': {
return {
...state,
confirmationCode: action.payload,
};
}
case'START_CONFIRMATION': {
return {
...state,
isLoading: true,
hasError: false,
};
}
case'CONFIRMATION_FAILED': {
return {
...state,
isPasswordCorrect: false,
isLoading: false,
hasError: true,
};
}
case'CONFIRMATION_SUCCESS': {
return {
...state,
isPasswordCorrect: true,
isLoading: false,
hasError: false,
};
}
case'DEACTIVATE': {
return {
...state,
isActive: false,
};
}
case'RESET': {
return { ...initialState };
}
default: return state;
}
}
Ahora podemos llamar a la función dispatch
cada vez que el usuario o la aplicación lo necesiten.
const Component = () => {
const [state, dispatch] = useReducer(confirmationReducer, initialState);
const {
confirmationCode,
isLoading,
hasError,
isPasswordCorrect,
isActive,
} = state;
// …return (/* … */);
};
Primer dispatch: el usuario escribe el código de seguridad.
const Component = () => {
// ...const setConfirmationCodeOnChange = (e) => {
dispatch({
type: ‘WRITE_CONFIRMATION_CODE’,
payload: e.currentTarget.value,
});
}
return (
<>
{/* ... */}
<input
value={confirmationCode}
onChange{setConfirmationCodeOnChange}
/>
{/* ... */}
</>
);
};
Segundo dispatch: comprobar si el código de seguridad es correcto y mostrar los mensajes de error o cargando.
const Component = () => {
// …const confirmPasswordOnClick = async () => {
dispatch({ type: 'START_CONFIRMATION' });
const isCorrect = await sampleFetch(confirmationCode);
if (isCorrect) {
dispatch({ type: 'CONFIRMATION_SUCCESS' });
} else {
dispatch({ type: 'CONFIRMATION_FAILED' });
}
};
return (
<>
{/* ... */}
{isLoading && <p>Cargando...</p>}
{hasError && <p>Error...</p>}
<input
value={confirmationCode}
onChange{setConfirmationCodeOnChange}
/>
<button onClick={confirmPasswordOnClick}>
Comprobar
</button>
{/* ... */}
</>
);
};
Tercer y cuarto dispatch: botones de confirmar o cancelar la eliminación.
const Component = () => {
// …return (
{/* ... */}
{isPasswordCorrect && (
<><p>¿Seguro que quieres eliminar?</p><buttononClick={() => {
dispatch({ type: 'DEACTIVATE' });
}}
>
Sí, eliminar
</button><buttononClick={() => {
dispatch({ type: 'RESET' });
}}
>
No, volver
</button></>
)}
{/* ... */}
);
};
Quinto (y último) dispatch: botón para deshacer la eliminación y volver al estado inicial.
{!isActive && (
<div><h2>Eliminación exitosa</h2><buttononClick={() => {
dispatch({ type: ‘RESET’ });
}}
>
Recuperar, cancelar la eliminación
</button></div>
)}
¡Y listo! Esto fue la implementación de useReducer
para manejar el estado de nuestro componente.
Puedes encontrar el código completo de nuestro componente en: github.com/juandc/usestate-vs-usereducer/blob/master/usereducer.js.
👉 https://youtu.be/o-nCM1857AQ
👉 https://kentcdodds.com/blog/should-i-usestate-or-usereducer
👉 https://dev.to/spukas/3-reasons-to-usereducer-over-usestate-43ad
👉 https://dev.to/macmacky/usereducer-redux-s-reducer-4jee
👉 https://www.simplethread.com/cant-replace-redux-with-hooks/
👉 https://www.robinwieruch.de/redux-javascript
👉 https://www.robinwieruch.de/redux-vs-usereducer
Como te dije al principio, esta comparación no es entre useState
y useReducer
. Más bien, es un recordatorio de que no podemos limitar nuestra mente a un solo paradigma. Y en este caso, manejar tantos estados independientes que dependen unos de otros puede no ser la mejor opción.
Te reto a resolver los siguientes ejercicios:
useState
.useReducer
. Yo lo llamaré useReducerPlagio
.Finalmente, te invito a tomar el Curso React.js: Manejo Profesional del Estado. Vamos a descubrir la diversidad de herramientas y paradigmas que nos ofrece React para manejar el estado y cómo han evolucionado estas soluciones a lo largo del tiempo.
#NuncaParesDeAprender 🤓💚
¡Increíble post Juan! 😮
¡Gracias, Facu!
Mi mejor resumen sería: “useState nos permite crear estados independientes ilimitados. Y useReducer soluciona los problemas de exagerar la cantidad”. 😄
muy buen aporte, y claro con ejemplos y todo me ayudo a entender unas cosas que no tenia muy claras gracias!
Gracias Juan David hapasado un tiempo desde tu publicación y sigues ayudando … Thanks.
Excelente, me ayuda a entender mejor useReducer
¡Ya me quedo clarísimo, gracias Juan eres grande!
Muchas gracias por la explicación. 😄
Muy claro la explicacion, muchas gracias profe.
Excelente Juan, me ayudaste men
Excelente post, justo lo que necesitaba gracias 😄
Tengo una duda increible
Tengo un contenedor llamdo Wrapper
import React from “react”;
import { Header } from ‘…/Header’;
import { DataContainer } from “…/DataContainer”;
import ‘./Wrapper.css’
function Wrapper(){
return(
<div className=“Wrapper” id=“Wrapper”>
<Header></Header>
<DataContainer></DataContainer>
</div>
)
}
export { Wrapper };
que llama al contenedor DataContainer
import React from "react"
import { Login } from “…/Login”
// import Register from "…/Register"
import ‘./DataContainer.css’
function DataContainer(){
return(
<div className=“DataContainer” id=“DataContainer”>
<Login
name=“active”
>
</Login>
{/* <Register></Register> */}
</div>
)
}
export { DataContainer };
Que llama al login
import ‘./Login.css’;
import React, { useState, useEffect } from ‘react’;
function Login(props){
const [state, setState] = React.useState({
value:’’,
})
console.log(‘state:’+state.value)
console.log(‘state:’+state)
console.log(‘setState:’+setState)
if(state.value ===false){
setState({
…state,
value: true,
})
}
return(
<div>
{state.value && (
<h1>Hola mundo<h1>
}
</div>
)
}
export { Login };
Hasta aquí todo bien.
El problema es que en el Header que es otro archivo creo el quetiene unos links creo el onClick
import React from ‘react’;
import ‘./Header.css’;
import Fondo_noche from '…/assets/icons/Logo.svg’
import { Login } from ‘…/Login’;
function Header() {
return (
<div>
<a className=“header__container__user__container__ingresar"
id=“header__container__user__container__ingresar” href=”#"
onClick={()=>Login()}
>Ingresar</a>
</div>
)
}
export { Header }
Cuando doy clic en el link sale esto y ya he leido y no doy con el problema
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
at Object.throwInvalidHookError (react-dom.development.js:16227:1)
at Object.useState (react.development.js:1622:1)
at Login (index.js:7:1)
at onClick (index.js:22:1)
at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1)
at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1)
at invokeGuardedCallback (react-dom.development.js:4277:1)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4291:1)
at executeDispatch (react-dom.development.js:9041:1)
at processDispatchQueueItemsInOrder (react-dom.development.js:9073:1)
throwInvalidHookError @ react-dom.development.js:16227
useState @ react.development.js:1622
Login @ index.js:7
onClick @ index.js:22
callCallback @ react-dom.development.js:4164
invokeGuardedCallbackDev @ react-dom.development.js:4213
invokeGuardedCallback @ react-dom.development.js:4277
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:4291
executeDispatch @ react-dom.development.js:9041
processDispatchQueueItemsInOrder @ react-dom.development.js:9073
processDispatchQueue @ react-dom.development.js:9086
dispatchEventsForPlugins @ react-dom.development.js:9097
(anónimas) @ react-dom.development.js:9288
batchedUpdates$1 @ react-dom.development.js:26140
batchedUpdates @ react-dom.development.js:3991
dispatchEventForPluginEventSystem @ react-dom.development.js:9287
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ react-dom.development.js:6465
dispatchEvent @ react-dom.development.js:6457
dispatchDiscreteEvent @ react-dom.development.js:6430