c
React Query y Context API
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.reset()
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.important ? <strong>{note.content}</strong> : note.content}
<button onClick={() => toggleImportance(note.id)}>
{note.important ? 'make not important' : 'make important'}
</button>
</li>
))}
</div>
)
}
export default AppEl código inicial está en GitHub en este repositorio, en la rama part6-0.
Gestión de datos del servidor con la librería TanStack Query
Ahora utilizaremos la librería TanStack Query para almacenar y gestionar los datos obtenidos del servidor.
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.reset()
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.isPending) { 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 TanStack 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.
Sincronización de datos con el servidor mediante TanStack 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.reset()
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 TanStack 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 TanStack 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.reset()
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 TanStack 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. TanStack 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 TanStack 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 TanStack 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í.
Hook personalizado useNotes
Nuestra solución es bastante buena, pero resulta algo incómodo que muchos detalles de implementación de TanStack Query estén directamente en el componente de React. Extraigámoslos a un hook personalizado:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getNotes, createNote, updateNote } from '../requests'
export const useNotes = () => {
const queryClient = useQueryClient()
const result = useQuery({
queryKey: ['notes'],
queryFn: getNotes,
refetchOnWindowFocus: false
})
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: (newNote) => {
const notes = queryClient.getQueryData(['notes'])
queryClient.setQueryData(['notes'], notes.concat(newNote))
}
})
const updateNoteMutation = useMutation({
mutationFn: updateNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] })
}
})
return {
notes: result.data,
isPending: result.isPending,
addNote: (content) => newNoteMutation.mutate({ content, important: true }),
toggleImportance: (note) => updateNoteMutation.mutate({
...note, important: !note.important
}),
}
}El hook encapsula todo el código relacionado con TanStack Query: la query que obtiene las notas y las dos mutaciones que las crean y actualizan. Estos detalles quedan ocultos para quien utiliza el hook, ya que la función devuelve un objeto sencillo con:
- notes: la lista de notas
- isPending: indica si los datos siguen cargándose
- addNote: una función para añadir una nota a partir de su contenido
- toggleImportance: una función para cambiar la importancia de una nota
El componente App se simplifica considerablemente:
import { useNotes } from './hooks/useNotes'
const App = () => {
const { notes, isPending, addNote: addNoteToServer, toggleImportance } = useNotes()
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.reset()
addNoteToServer(content)
}
if (isPending) {
return <div>loading data...</div>
}
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}>
{note.important ? <strong>{note.content}</strong> : note.content}
<button onClick={() => toggleImportance(note)}>
{note.important ? 'make not important' : 'make important'}
</button>
</li>
))}
</div>
)
}El código de la aplicación está en GitHub en la rama part6-3.
TanStack Query es una librería versátil que, basándonos en lo que ya hemos visto, simplifica la aplicación. ¿Hace TanStack Query que soluciones de gestión de estado más complejas como Redux sean innecesarias? No. TanStack Query puede reemplazar parcialmente el estado de la aplicación en algunos casos, pero como lo indica la documentación:
- TanStack Query es una librería de estado del servidor, responsable de la gestión de operaciones asíncronas entre el servidor y el cliente
- Zustand y otras soluciones similares son librerías de estado del cliente que pueden almacenar datos asíncronos, aunque con menor eficiencia que una herramienta como TanStack Query
Entonces, TanStack 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. TanStack 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).
Context API
Volvamos a la conocida aplicación de contador. La aplicación se define de la siguiente manera:
import { useState } from 'react'
import Display from './components/Display'
import Controls from './components/Controls'
const App = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<Display counter={counter} />
<Controls counter={counter} setCounter={setCounter} />
</div>
)
}El componente App define el estado de la aplicación y se lo pasa a Display, que renderiza el valor del contador:
const Display = ({ counter }) => {
return (
<div>{counter}</div>
)
}y al componente Controls, que renderiza los botones:
const Controls = ({ counter, setCounter }) => {
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
const zero = () => setCounter(0)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}La aplicación crece:

El papel de App cambia: sigue conservando el estado, pero ya no renderiza directamente los componentes que lo utilizan:
const App = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<Navbar />
<Panel counter={counter} setCounter={setCounter} />
<Footer />
</div>
)
}El nuevo componente Panel renderiza los componentes que muestran el contador y los botones:
import Display from './Display'
import Controls from './Controls'
const Panel = ({ counter, setCounter }) => {
return (
<div>
<Display counter={counter} />
<Controls counter={counter} setCounter={setCounter} />
</div>
)
}La jerarquía de componentes es la siguiente:
App (state)
├── Panel
│ ├── Display
│ └── Controls
└── FooterEl estado continúa en App. Para que Display y Controls accedan al contador, tanto el estado como su función de actualización deben atravesar Panel como props, aunque Panel no los necesite. Esta situación aparece fácilmente al utilizar estado creado con useState y se denomina prop drilling.
La Context API integrada en React ofrece una solución. Un contexto de React es una especie de estado global de la aplicación al que se puede dar acceso directo a cualquier componente.
Creemos un contexto que almacene la gestión del estado del contador.
El contexto se crea con createContext. Lo definiremos en el archivo src/CounterContext.jsx:
import { createContext } from 'react'
const CounterContext = createContext()
export default CounterContextEl componente App puede proporcionar el contexto a sus componentes hijos así:
// ...
import CounterContext from './components/CounterContext'
const App = () => {
const [counter, setCounter] = useState(0)
return (
<CounterContext.Provider value={{counter, setCounter}}> <Panel /> <Footer />
</CounterContext.Provider> )
}El contexto se proporciona envolviendo los componentes hijos con CounterContext.Provider y asignándole un valor adecuado.
Su valor es ahora un objeto con los atributos counter y setCounter: el estado del contador y la función que lo actualiza.
Como Panel ya no recibe props relacionadas con el contador, se simplifica:
const Panel = () => {
return (
<div>
<Display />
<Controls />
</div>
)
}Los demás componentes pueden acceder al contexto mediante el hook useContext. Display cambia de la siguiente manera:
import { useContext } from 'react'import CounterContext from './CounterContext'
const Display = () => { const { counter } = useContext(CounterContext)
return <div>{counter}</div>
}Display ya no necesita props. Obtiene el contador llamando a useContext con el objeto CounterContext como parámetro.
De forma análoga, Controls pasa a ser:
import { useContext } from 'react'import CounterContext from './CounterContext'
const Controls = () => {
const { counter, setCounter } = useContext(CounterContext)
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
const zero = () => setCounter(0)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}
export default ControlsLos componentes ya tienen acceso al contenido establecido por el proveedor: el estado del contador y su función de actualización.
Extraen los atributos que necesitan mediante la desestructuración de JavaScript:
const { counter } = useContext(CounterContext)Definir el contexto del contador en su propio archivo
La aplicación aún tiene un aspecto poco agradable: la gestión del estado del contador está definida dentro de App. Traslademos todo el código relacionado al archivo CounterContext.jsx:
import { createContext, useState } from 'react'
const CounterContext = createContext()
export default CounterContext
export const CounterContextProvider = (props) => { const [counter, setCounter] = useState(0) return ( <CounterContext.Provider value={{ counter, setCounter }}> {props.children} </CounterContext.Provider> )}El archivo exporta tanto CounterContext como CounterContextProvider, que es esencialmente un proveedor cuyo valor contiene el contador y su función de actualización.
Utilicemos el proveedor directamente 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(
<CounterContextProvider> <App />
</CounterContextProvider>)El contexto que define el valor y la funcionalidad del contador está ahora disponible para todos los componentes.
App se simplifica:
import Panel from './components/Panel'
import Footer from './components/Footer'
const App = () => {
return (
<div>
<Navbar />
<Panel />
<Footer />
</div>
)
}
export default AppEl contexto sigue utilizándose igual y los demás componentes no necesitan cambios. Por ejemplo, Controls permanece así:
const Controls = () => {
const { counter, setCounter } = useContext(CounterContext)
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
const zero = () => setCounter(0)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}La solución es bastante buena. Todo el estado de la aplicación —el valor del contador— queda aislado en CounterContext. Cada componente accede exactamente a la parte que necesita mediante useContext y la desestructuración.
Hagamos una pequeña mejora y definamos también las funciones increment, decrement y zero dentro del contexto:
import { createContext, useState } from 'react'
const CounterContext = createContext()
export default CounterContext
export const CounterContextProvider = (props) => {
const [counter, setCounter] = useState(0)
const increment = () => setCounter(counter + 1) const decrement = () => setCounter(counter - 1) const zero = () => setCounter(0)
return (
<CounterContext.Provider value={{ counter, increment, decrement, zero }}> {props.children}
</CounterContext.Provider>
)
}Ahora podemos usar las funciones obtenidas del contexto directamente como controladores de eventos:
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const Controls = () => {
const { increment, decrement, zero } = useContext(CounterContext)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}Todavía podemos mejorarlo. Al observar el uso del contexto, vemos el mismo código repetitivo en ambos componentes que lo consumen:
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const Display = () => {
const { counter } = useContext(CounterContext)
// ...
}import { useContext } from 'react'
import CounterContext from '../CounterContext'
const Controls = () => {
const { increment, decrement, zero } = useContext(CounterContext) // ...
}Podemos avanzar un paso más creando un hook personalizado que devuelva directamente el contexto. Añadámoslo al archivo hooks/useCounter.js:
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const useCounter = () => useContext(CounterContext)
export default useCounterEl uso del contexto queda aún más sencillo:
import { useCounter } from '../hooks/useCounter'
const Display = () => {
const { counter } = useCounter()
// ...
}
import { useCounter } from '../hooks/useCounter'
const Controls = () => {
const { increment, decrement, zero } = useCounter()
// ...
}La solución nos satisface. Aísla toda la gestión del estado dentro del contexto. Los componentes que lo usan desconocen cómo se implementa; gracias al hook personalizado, ni siquiera necesitan saber que la solución se basa en la Context API.
El código de la aplicación está en el repositorio de GitHub https://github.com/fullstack-hy2020/context-counter.
¿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 TanStack 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.
Durante mucho tiempo, la solución más popular y completa fue Redux, una forma de implementar la arquitectura Flux. Sin embargo, Redux es conocido por su complejidad y la abundancia de código repetitivo, lo que motivó la aparición de alternativas más recientes. En este curso Redux se ha sustituido por Zustand, que ofrece una funcionalidad equivalente con una API mucho más sencilla. Zustand se ha convertido en una opción popular cuando hace falta algo más que useState, pero toda la maquinaria de Redux resulta excesiva. Parte de las críticas a la rigidez de Redux han quedado obsoletas gracias a Redux Toolkit, y Redux todavía se usa mucho, especialmente en proyectos grandes.
Ni Zustand ni Redux tienen que utilizarse en toda la aplicación. Por ejemplo, puede ser razonable gestionar fuera de ellos el estado de los formularios cuando no afecta al resto de la aplicación. También es perfectamente posible combinar Zustand o Redux con TanStack Query.
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.


