d
React Query, useReducer y el contexto
Al final de esta parte, analizaremos algunas formas diferentes de administrar el estado de una aplicación.
Continuemos con la aplicación de notas. Nos centraremos en la comunicación con el servidor. Comencemos la aplicación desde cero. La primera versión es la siguiente:
const App = () => {
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
console.log(content)
}
const toggleImportance = (note) => {
console.log('toggle importance of', note.id)
}
const notes = []
return (
<div>
<h2>Notes app</h2>
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
{notes.map((note) => (
<li key={note.id} onClick={() => toggleImportance(note)}>
{note.content}
<strong> {note.important ? 'important' : ''}</strong>
</li>
))}
</div>
)
}
export default AppEl código inicial está en GitHub en este repositorio, en la rama part6-0.
Administrando datos en el servidor con la librería React Query
Ahora usaremos la librería React Query para almacenar y administrar los datos obtenidos del servidor. La última versión de la librería también es llamada TanStack Query, pero seguiremos usando su nombre tradicional.
Instala la librería con el comando
npm install @tanstack/react-querySe necesitan agregar algunas cosas en el archivo main.jsx para pasar las funciones de la librería a toda la aplicación:
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.jsx'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}> <App />
</QueryClientProvider>)Usemos JSON Server como en las partes anteriores para simular el backend. JSON Server está preconfigurado en el proyecto de ejemplo, y la raíz del proyecto contiene un archivo db.json que por defecto tiene dos notas. Puedes iniciar el servidor con:
npm run serverAhora podemos recuperar las notas en el componente App. El código se expande de la siguiente manera:
import { useQuery } from '@tanstack/react-query'
const App = () => {
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
console.log(content)
}
const toggleImportance = (note) => {
console.log('toggle importance of', note.id)
}
const result = useQuery({ queryKey: ['notes'], queryFn: async () => { const response = await fetch('http://localhost:3001/notes') if (!response.ok) { throw new Error('Failed to fetch notes') } return await response.json() } }) console.log(JSON.parse(JSON.stringify(result))) if (result.isLoading) { return <div>loading data...</div> } const notes = result.data
return (
// ...
)
}La obtención de datos del servidor se realiza, como en el capítulo anterior, usando el método fetch de la Fetch API. Sin embargo, la llamada al método ahora está envuelta en una query (consulta) formada con la función useQuery. La llamada a useQuery toma como parámetro un objeto con los campos queryKey y queryFn. El valor del campo queryKey es un array que contiene el string notes. Actúa como la clave para la query definida, es decir, la lista de notas.
El valor devuelto por la función useQuery es un objeto que indica el estado de la query. La salida a la consola ilustra la situación:

Es decir, la primera vez que se renderiza el componente, la query todavía está en estado loading, es decir, la solicitud HTTP asociada está pendiente. En esta etapa, solo se procesa lo siguiente:
<div>loading data...</div>Sin embargo, la solicitud HTTP se completa tan rápido que ni siquiera Max Verstappen podría ver el texto. Cuando se completa la solicitud, el componente se renderiza de nuevo. La query está en el estado success en la segunda renderización, y el campo data del objeto de la query contiene los datos devueltos por la solicitud, es decir, la lista de notas que se muestran en la pantalla.
Entonces, la aplicación recupera datos del servidor y los renderiza en la pantalla sin usar los Hooks de React useState y useEffect utilizados en los capítulos 2-5. Los datos en el servidor ahora están completamente bajo la administración de la librería React Query, ¡y la aplicación no necesita el estado definido con el Hook de React useState en absoluto!
Movamos la función que realiza la solicitud HTTP a su propio archivo src/requests.js
const baseUrl = 'http://localhost:3001/notes'
export const getNotes = async () => {
const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
return await response.json()
}El componente App ahora se ha simplificado un poco:
import { useQuery } from '@tanstack/react-query'
import { getNotes } from './requests'
const App = () => {
// ...
const result = useQuery({
queryKey: ['notes'],
queryFn: getNotes })
// ...
}El código actual de la aplicación está en GitHub en la rama part6-1.
Sincronizando datos con el servidor usando React Query
Los datos ya se han recuperado correctamente del servidor. A continuación, nos aseguraremos de que los datos agregados y modificados se almacenen en el servidor. Comencemos agregando nuevas notas.
Hagamos una función createNote en el archivo requests.js para guardar nuevas notas:
const baseUrl = 'http://localhost:3001/notes'
export const getNotes = async () => {
const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
return await response.json()
}
export const createNote = async (newNote) => { const options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newNote) } const response = await fetch(baseUrl, options) if (!response.ok) { throw new Error('Failed to create note') } return await response.json()}El componente App cambiará de la siguiente manera
import { useQuery, useMutation } from '@tanstack/react-query'import { getNotes, createNote } from './requests'
const App = () => {
const newNoteMutation = useMutation({ mutationFn: createNote, })
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
newNoteMutation.mutate({ content, important: true }) }
//
}Para crear una nueva nota, se define una mutación usando la función useMutation:
const newNoteMutation = useMutation({
mutationFn: createNote,
})El parámetro es la función que agregamos al archivo requests.js, que usa la Fetch API para enviar una nueva nota al servidor.
El controlador de eventos addNote realiza la mutación llamando a la función mutate del objeto de mutación y pasando la nueva nota como parámetro:
newNoteMutation.mutate({ content, important: true })Nuestra solución es buena. Excepto que no funciona. La nueva nota se guarda en el servidor, pero no se actualiza en la pantalla.
Para renderizar una nueva nota también, debemos decirle a React Query que el resultado antiguo de la query cuya clave es el string notes debe ser invalidado.
Afortunadamente, la invalidación es fácil, se puede hacer definiendo la función de callback onSuccess apropiada para la mutación:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'import { getNotes, createNote } from './requests'
const App = () => {
const queryClient = useQueryClient()
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notes'] }) }, })
// ...
}Ahora que la mutación se ha ejecutado con éxito, se realiza una llamada a la función
queryClient.invalidateQueries({ queryKey: ['notes'] })Esto a su vez hace que React Query actualice automáticamente una query con la clave notes, es decir, obtenga las notas del servidor. Como resultado, la aplicación renderiza el estado actualizado en el servidor, es decir, la nota agregada también se renderiza.
Implementemos también el cambio en la importancia de las notas. Se agrega una función para actualizar notas al archivo requests.js:
export const updateNote = async (updatedNote) => {
const options = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedNote)
}
const response = await fetch(`${baseUrl}/${updatedNote.id}`, options)
if (!response.ok) {
throw new Error('Failed to update note')
}
return await response.json()
}Actualizar la nota también se hace mediante una mutación. El componente App se expande de la siguiente manera:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getNotes, createNote, updateNote } from './requests'
const App = () => {
const queryClient = useQueryClient()
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] })
}
})
const updateNoteMutation = useMutation({ mutationFn: updateNote, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notes'] }) } })
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
newNoteMutation.mutate({ content, important: true })
}
const toggleImportance = (note) => {
updateNoteMutation.mutate({...note, important: !note.important }) }
// ...
}De nuevo, se creó una mutación que invalidó la query notes para que la nota actualizada se renderice correctamente. Usar mutaciones es fácil, el método mutate recibe una nota como parámetro, cuya importancia se cambia a la negación del valor antiguo.
El código actual de la aplicación está en GitHub en la rama part6-2.
Optimizando el rendimiento
La aplicación funciona bien y el código es relativamente simple. La facilidad para realizar cambios en la lista de notas es particularmente sorprendente. Por ejemplo, cuando cambiamos la importancia de una nota, invalidar la query notes es suficiente para que los datos de la aplicación se actualicen:
const updateNoteMutation = useMutation({
mutationFn: updateNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] }) }
})La consecuencia de esto, por supuesto, es que después de la solicitud PUT que causa el cambio de nota, la aplicación realiza una nueva solicitud GET para recuperar los datos de la query desde el servidor:

Si la cantidad de datos obtenidos por la aplicación no es grande, realmente no importa. Después de todo, desde el punto de vista de la funcionalidad del lado del navegador, hacer una solicitud HTTP GET adicional realmente no importa, pero en algunas situaciones podría generar una carga en el servidor.
Si fuera necesario, es posible también optimizar el rendimiento manualmente, actualizando el estado de la query mantenido por React Query.
El cambio para la mutación que agrega una nueva nota es el siguiente:
const App = () => {
const queryClient = useQueryClient()
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: (newNote) => { const notes = queryClient.getQueryData(['notes']) queryClient.setQueryData(['notes'], notes.concat(newNote)) }
})
// ...
}Es decir, en el callback de onSuccess, el objeto queryClient primero lee el estado existente de notes de la query y lo actualiza agregando una nueva nota, que se obtiene como parámetro de la función de callback. El valor del parámetro es el valor devuelto por la función createNote, definida en el archivo requests.js de la siguiente manera:
export const createNote = async (newNote) => {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newNote)
}
const response = await fetch(baseUrl, options)
if (!response.ok) {
throw new Error('Failed to create note')
}
return await response.json()
}Sería relativamente fácil hacer un cambio similar a una mutación que cambia la importancia de la nota, pero lo dejamos como un ejercicio opcional.
Finalmente, nota un detalle interesante. React Query vuelve a obtener todas las notas cuando cambiamos a otra pestaña del navegador y luego regresamos a la pestaña de la aplicación. Esto se puede observar en la pestaña de Red de la Consola de Desarrollador:

¿Qué está pasando? Al leer la documentación, nos damos cuenta de que la funcionalidad predeterminada de las queries de React Query es que las queries (cuyo estado es stale) se actualicen cuando cambia el window focus. Si queremos, podemos desactivar la funcionalidad creando una consulta de la siguiente manera:
const App = () => {
// ...
const result = useQuery({
queryKey: ['notes'],
queryFn: getNotes,
refetchOnWindowFocus: false })
// ...
}Si colocas un console.log en el código, podrás ver desde la consola del navegador cuántas veces React Query hace que la aplicación se vuelva a renderizar. La regla general es que el renderizado ocurre al menos cada vez que es necesario, es decir, cuando cambia el estado de la query. Puedes leer más al respecto por ejemplo aquí.
El código de la aplicación está en GitHub en la rama part6-3.
React Query es una librería versátil que, basándonos en lo que ya hemos visto, simplifica la aplicación. ¿Hace React Query que soluciones de gestión de estado más complejas como Redux sean innecesarias? No. React Query puede reemplazar parcialmente el estado de la aplicación en algunos casos, pero como lo indica la documentación:
- React Query es una librería de estado del servidor, responsable de la gestión de operaciones asíncronas entre el servidor y el cliente
- Redux, etc. son librerías de estado del cliente que se pueden usar para almacenar datos asíncronos, aunque de manera menos eficiente cuando se comparan con una herramienta como React Query
Entonces, React Query es una librería que mantiene el estado del servidor en el frontend, es decir, actúa como una caché para lo que se almacena en el servidor. React Query simplifica el procesamiento de datos en el servidor y, en algunos casos, puede eliminar la necesidad de que los datos en el servidor se guarden en el estado del frontend.
La mayoría de las aplicaciones de React no necesitan solo una forma de almacenar temporalmente los datos servidos, sino también alguna solución para cómo se maneja el resto del estado del frontend (por ejemplo, el estado de los formularios o las notificaciones).
useReducer
Entonces, incluso si la aplicación usa React Query, generalmente se necesita alguna solución para manejar el resto del estado del frontend (por ejemplo, el estado de los formularios). Con bastante frecuencia, el estado creado con useState es una solución suficiente. Usar Redux es, por supuesto, posible, pero hay otras alternativas.
Veamos una aplicación de contador sencilla. La aplicación muestra el valor del contador y ofrece tres botones para actualizar su estado:

Ahora implementaremos la gestión del estado del contador usando un mecanismo de gestión de estado similar a Redux proporcionado por el hook integrado de React useReducer.
El código inicial de la aplicación está en GitHub en la rama part6-1. El archivo App.jsx se ve de la siguiente manera:
import { useReducer } from 'react'
const counterReducer = (state, action) => {
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
case 'ZERO':
return 0
default:
return state
}
}
const App = () => {
const [counter, counterDispatch] = useReducer(counterReducer, 0)
return (
<div>
<div>{counter}</div>
<div>
<button onClick={() => counterDispatch({ type: 'INC' })}>+</button>
<button onClick={() => counterDispatch({ type: 'DEC' })}>-</button>
<button onClick={() => counterDispatch({ type: 'ZERO' })}>0</button>
</div>
</div>
)
}
export default AppEl hook useReducer proporciona un mecanismo para crear un estado para la aplicación. El parámetro para crear un estado es la función del reducer que maneja los cambios de estado y el valor inicial del estado:
const [counter, counterDispatch] = useReducer(counterReducer, 0)La función del reducer que maneja los cambios de estado es similar a los reducers de Redux, es decir, la función obtiene como parámetros el estado actual y la acción que cambia el estado. La función devuelve el nuevo estado actualizado en función del tipo y el posible contenido de la acción:
const counterReducer = (state, action) => {
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
case 'ZERO':
return 0
default:
return state
}
}En nuestro ejemplo, las acciones no tienen nada más que un tipo. Si el tipo de acción es INC, aumenta el valor del contador en uno, etc. Como los reducers de Redux, las acciones también pueden contener datos arbitrarios, que generalmente se colocan en el campo payload de la acción.
La función useReducer devuelve un array que contiene un elemento para acceder al valor actual del estado (primer elemento del array) y una función dispatch (segundo elemento del array) para cambiar el estado:
const App = () => {
const [counter, counterDispatch] = useReducer(counterReducer, 0)
return (
<div>
<div>{counter}</div> <div>
<button onClick={() => counterDispatch({ type: 'INC' })}>+</button> <button onClick={() => counterDispatch({ type: 'DEC' })}>-</button>
<button onClick={() => counterDispatch({ type: 'ZERO' })}>0</button>
</div>
</div>
)
}Como se puede ver, el cambio de estado se realiza exactamente como en Redux, la función de dispatch recibe la acción apropiada para cambiar el estado como parámetro:
counterDispatch({ type: "INC" })Pasando el estado via props
Cuando la aplicación se divide en varios componentes, el valor del contador y la función de dispatch utilizada para gestionarlo deben pasarse de alguna manera a los otros componentes también. Una solución es pasar estos como props de la manera habitual.
Definamos un componente Display separado para la aplicación, cuya responsabilidad es mostrar el valor del contador. El contenido del archivo src/components/Display.jsx debe ser:
const Display = ({ counter }) => {
return <div>{counter}</div>
}
export default DisplayAdemás, definamos un componente Button que sea responsable de los botones de la aplicación:
const Button = ({ dispatch, type, label }) => {
return (
<button onClick={() => dispatch({ type })}>
{label}
</button>
)
}
export default ButtonEl archivo App.jsx cambia de la siguiente manera:
import { useReducer } from 'react'
import Button from './components/Button'import Display from './components/Display'
const counterReducer = (state, action) => {
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
case 'ZERO':
return 0
default:
return state
}
}
const App = () => {
const [counter, counterDispatch] = useReducer(counterReducer, 0)
return (
<div>
<Display counter={counter} /> <div>
<Button dispatch={counterDispatch} type="INC" label="+" /> <Button dispatch={counterDispatch} type="DEC" label="-" /> <Button dispatch={counterDispatch} type="ZERO" label="0" /> </div>
</div>
)
}La aplicación ahora se ha dividido en varios componentes. La gestión del estado está definida en el archivo App.jsx, desde donde los valores y funciones necesarios para la gestión del estado se pasan a los componentes hijos como props.
La solución funciona, pero no es óptima. Si la estructura de los componentes se complica, por ejemplo, el despachador debe transmitirse usando props a través de muchos componentes para llegar a los componentes que lo necesitan, aunque los componentes intermedios en el árbol de componentes no necesiten al despachador. Este fenómeno se llama prop drilling.
Usando context para pasar el estado a los componentes
La API de Contexto integrada en React proporciona una solución para nosotros. El contexto de React es un tipo de estado global de la aplicación, al que se puede dar acceso directo a cualquier componente de la aplicación.
Creemos ahora un contexto en la aplicación que almacene la gestión de estado del contador.
El contexto se crea con el hook createContext de React. Creemos un contexto en el archivo src/CounterContext.jsx:
import { createContext } from 'react'
const CounterContext = createContext()
export default CounterContextEl componente App ahora puede proveer un contexto a sus componentes hijos de la siguiente manera:
import { useReducer } from 'react'
import Button from './components/Button'
import Display from './components/Display'
import CounterContext from './CounterContext'
// ...
const App = () => {
const [counter, counterDispatch] = useReducer(counterReducer, 0)
return (
<CounterContext.Provider value={{ counter, counterDispatch }}> <Display /> <div>
<Button type="INC" label="+" /> <Button type="DEC" label="-" /> <Button type="ZERO" label="0" /> </div>
</CounterContext.Provider> )
}Como se puede ver, proveer el contexto se realiza envolviendo los componentes hijos dentro del componente CounterContext.Provider y estableciendo un valor adecuado para el contexto.
El valor del contexto ahora es un objeto con los atributos counter y counterDispatch. El campo counter contiene el valor del contador y counterDispatch la función dispatch utilizada para cambiar el valor.
Otros componentes ahora pueden acceder al contexto utilizando el hook useContext. El componente Display cambia de la siguiente manera:
import { useContext } from 'react'import CounterContext from './CounterContext'
const Display = () => { const { counter } = useContext(CounterContext)
return <div>{counter}</div>
}Por lo tanto, el componente Display ya no necesita props; obtiene el valor del contador llamando al hook useContext con el objeto CounterContext como argumento.
De manera similar, el componente Button se convierte en:
import { useContext } from 'react'import CounterContext from './CounterContext'
const Button = ({ type, label }) => { const { counterDispatch } = useContext(CounterContext)
return (
<button onClick={() => counterDispatch({ type })}> {label}
</button>
)
}Por lo tanto, los componentes reciben el valor proporcionado por el proveedor de contexto. En este caso, el contexto es un objeto con un campo counter que representa el valor del contador y un campo counterDispatch que es la función dispatch utilizada para cambiar el estado del contador.
Los componentes acceden a los atributos que necesitan usando la sintaxis de desestructuración de JavaScript:
const { counter } = useContext(CounterContext)El código actual de la aplicación se encuentra en GitHub en la rama part6-2.
Definiendo el contexto del contador en un archivo separado
Nuestra aplicación tiene una característica molesta, que la funcionalidad de la gestión del estado del contador está parcialmente definida en el componente App. Ahora vamos a mover todo lo relacionado con el contador al archivo CounterContext.jsx:
import { createContext, useReducer } from 'react'
const counterReducer = (state, action) => {
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
case 'ZERO':
return 0
default:
return state
}
}
const CounterContext = createContext()
export const CounterContextProvider = (props) => {
const [counter, counterDispatch] = useReducer(counterReducer, 0)
return (
<CounterContext.Provider value={{ counter, counterDispatch }}>
{props.children}
</CounterContext.Provider>
)
}
export default CounterContextEl archivo ahora exporta, además del objeto CounterContext correspondiente al contexto, el componente CounterContextProvider, que es prácticamente un proveedor de contexto cuyo valor es un contador y un despachador utilizado para su gestión de estado.
Habilitemos el proveedor de contexto haciendo un cambio en main.jsx:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import { CounterContextProvider } from './CounterContext'
createRoot(document.getElementById('root')).render(
<StrictMode>
<CounterContextProvider> <App />
</CounterContextProvider> </StrictMode>
)Ahora el contexto que define el valor y la funcionalidad del contador está disponible para todos los componentes de la aplicación.
El componente App se simplifica a la siguiente forma:
import Button from './components/Button'
import Display from './components/Display'
const App = () => {
return (
<div>
<Display />
<div>
<Button type="INC" label="+" />
<Button type="DEC" label="-" />
<Button type="ZERO" label="0" />
</div>
</div>
)
}
export default AppEl contexto todavía se usa de la misma manera, y no se necesitan cambios en los otros componentes. Por ejemplo, el componente Button se define de la siguiente manera:
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const Button = ({ type, label }) => {
const { counterDispatch } = useContext(CounterContext)
return (
<button onClick={() => counterDispatch({ type })}>
{label}
</button>
)
}
export default ButtonLa solución es bastante elegante. Todo el estado de la aplicación, es decir, el valor del contador y el código para gestionarlo, ahora está aislado en el archivo CounterContext. Los componentes acceden a la parte del contexto que necesitan usando el hook useContext y la sintaxis de desestructuración de JavaScript.
El código final de la aplicación se encuentra en GitHub en la rama part6-3.
¿Qué solución de gestión de estado elegir?
En los capítulos 1-5, toda la gestión de estado de la aplicación se realizó utilizando el hook de React useState. Las llamadas asíncronas al backend requerían el uso del hook useEffect en algunas situaciones. En principio, no se necesita nada más.
Un problema sutil con una solución basada en un estado creado con el hook useState es que si alguna parte del estado de la aplicación se necesita en varios componentes de la aplicación, el estado y las funciones para manipularlo deben pasarse via props a todos los componentes que manejan el estado. A veces, las props deben pasar por varios componentes, y los componentes a lo largo del camino pueden ni siquiera estar interesados en el estado de ninguna manera. Este fenómeno algo desagradable se llama prop drilling.
A lo largo de los años, se han desarrollado varias soluciones alternativas para la gestión de estado de aplicaciones React, que se pueden usar para aliviar situaciones problemáticas (por ejemplo, prop drilling). Sin embargo, ninguna solución ha sido "final", todas tienen sus propias ventajas y desventajas, y se están desarrollando nuevas soluciones todo el tiempo.
La situación puede confundir a un principiante e incluso a un desarrollador web experimentado. ¿Qué solución se debe usar?
Para una aplicación simple, useState es sin duda un buen punto de partida. Si la aplicación está comunicándose con el servidor, la comunicación se puede manejar de la misma manera que en los capítulos 1-5, utilizando el estado de la aplicación misma. Sin embargo, recientemente se ha vuelto más común mover la comunicación y la gestión asociada del estado al menos parcialmente bajo el control de React Query (o alguna otra librería similar). Si estás preocupado por useState y el prop drilling que conlleva, usar context puede ser una buena opción. También hay situaciones donde puede tener sentido manejar parte del estado con useState y parte con contextos.
La solución de gestión de estado más completa y robusta es Redux, que es una forma de implementar la llamada arquitectura Flux. Redux es ligeramente más antigua que las soluciones presentadas en esta sección. La rigidez de Redux ha sido la motivación para muchas nuevas soluciones de gestión de estado, como el useReducer de React. Algunas de las críticas a la rigidez de Redux ya se han vuelto obsoletas gracias al Redux Toolkit.
A lo largo de los años, también se han desarrollado otras librerías de gestión de estado que son similares a Redux, como el recién llegado Recoil y el ligeramente más antiguo MobX. Sin embargo, según Npm trends, Redux todavía domina claramente, y de hecho parece estar aumentando su ventaja:

También, Redux no tiene que ser usado en su totalidad en una aplicación. Puede tener sentido, por ejemplo, gestionar el estado de los formularios fuera de Redux, especialmente en situaciones donde el estado de un formulario no afecta al resto de la aplicación. También es perfectamente posible usar Redux y React Query juntos en la misma aplicación.
La pregunta de qué solución de gestión de estado se debe usar no es para nada sencilla. Es imposible dar una sola respuesta correcta. También es probable que la solución de gestión de estado seleccionada pueda resultar ser subóptima a medida que la aplicación crece hasta tal punto que la solución tenga que cambiarse incluso si la aplicación ya ha sido puesta en uso de producción.


