d
Redux (heredado)
Hasta ahora, hemos seguido las convenciones de gestión de estado recomendadas por React. Hemos colocado el estado y las funciones para manejarlo en el nivel superior de la estructura de componentes de la aplicación. A menudo, la mayoría del estado de la aplicación y los métodos para modificarlo residen directamente en el componente raíz. Luego, el estado y sus métodos de control se han pasado a otros componentes con props. Esto funciona hasta cierto punto, pero cuando las aplicaciones crecen, la gestión del estado se vuelve desafiante.
Arquitectura de Flux
Facebook desarrolló la arquitectura Flux para facilitar la gestión del estado. En Flux, el estado se separa completamente de los componentes de React en sus propios stores(almacenes). El estado en el store no se cambia directamente, sino con diferentes actions(acciones).
Cuando una acción cambia el estado de un store, las vistas se vuelven a generar:

Si alguna acción en la aplicación, por ejemplo presionar un botón, provoca la necesidad de cambiar el estado, el cambio se realiza con una acción. Esto hace que se vuelva a renderizar la vista:

Flux ofrece una manera estándar de cómo y dónde se mantiene el estado de la aplicación y cómo se modifica.
Redux
Facebook tiene una implementación para Flux, pero usaremos la librería Redux. Funciona con el mismo principio, pero es un poco más sencilla. Facebook también usa Redux ahora en lugar de su Flux original.
Conoceremos Redux implementando una aplicación de contador una vez más:

Crea una nueva aplicación Vite e instala redux con el comando
npm install reduxComo en Flux, en Redux el estado también se almacena en un store.
Todo el estado de la aplicación se almacena en un objeto JavaScript en el store. Debido a que nuestra aplicación solo necesita el valor del contador, lo guardaremos directamente en el store. Si el estado fuera más complicado, diferentes elementos del estado se guardarían como campos separados del objeto.
El estado del store se cambia con acciones. Las acciones son objetos que tienen al menos un campo que determina el tipo de acción. Nuestra aplicación necesita, por ejemplo, la siguiente acción:
{
type: 'INCREMENT'
}Si hay datos relacionados con la acción, se pueden declarar otros campos según sea necesario. Sin embargo, nuestra aplicación de contador es tan simple que las acciones están bien con solo el campo de tipo.
El impacto de la acción sobre el estado de la aplicación se define mediante un reducer. En la práctica, un reducer es una función a la que se le da el estado actual y una acción como parámetros. Devuelve un nuevo estado.
Definamos ahora un reducer para nuestra aplicación:
const counterReducer = (state, action) => {
if (action.type === 'INCREMENT') {
return state + 1
} else if (action.type === 'DECREMENT') {
return state - 1
} else if (action.type === 'ZERO') {
return 0
}
return state
}El primer parámetro es el estado en el store. El reducer devuelve un nuevo estado basado en el tipo de acción. Entonces, por ejemplo, cuando el tipo de acción es INCREMENT, el estado obtiene el valor antiguo más uno. Si el tipo de acción es ZERO, el nuevo valor del estado es cero.
Cambiemos un poco el código. Hemos utilizado declaraciones if-else para responder a una acción y cambiar el estado. Sin embargo, la declaración switch es el enfoque más común para escribir un reducer.
También definamos un valor predeterminado de 0 para el parámetro state. Ahora, el reducer funciona incluso si el estado del store aún no se ha inicializado.
const counterReducer = (state = 0, action) => { switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
case 'ZERO':
return 0
default: // if none of the above matches, code comes here
return state
}
}El reducer nunca debe ser llamado directamente desde el código de la aplicación. Solo es proporcionado como parámetro a la función createStore que crea el store:
import { createStore } from 'redux'
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
case 'ZERO':
return 0
default:
return state
}
}
const store = createStore(counterReducer)El store ahora usa el reducer para manejar acciones, que son dispatched o 'enviadas' al store con su método dispatch(envío).
store.dispatch({ type: 'INCREMENT' })Puedes averiguar el estado del store utilizando el método getState.
Por ejemplo, el siguiente código:
// ...
const store = createStore(counterReducer)
console.log(store.getState())store.dispatch({type: 'INCREMENT'})store.dispatch({type: 'INCREMENT'})store.dispatch({type: 'INCREMENT'})console.log(store.getState())store.dispatch({type: 'ZERO'})store.dispatch({type: 'DECREMENT'})console.log(store.getState())imprimiría lo siguiente en la consola
0
3
-1porque al principio el estado del store es 0. Después de tres acciones INCREMENT el estado es 3. Al final, después de las acciones ZERO y DECREMENT, el estado es -1.
El tercer método importante que tiene el store es subscribe, que se utiliza para crear funciones callback que el store llama cuando cambia su estado.
Si, por ejemplo, añadiéramos la siguiente función para suscribirnos, todos los cambios en el store se imprimirían en la consola.
store.subscribe(() => {
const storeNow = store.getState()
console.log(storeNow)
})entonces el código
// ...
const store = createStore(counterReducer)
store.subscribe(() => { const storeNow = store.getState() console.log(storeNow)})
store.dispatch({ type: 'INCREMENT' })store.dispatch({ type: 'INCREMENT' })store.dispatch({ type: 'INCREMENT' })store.dispatch({ type: 'ZERO' })store.dispatch({ type: 'DECREMENT' })causaría que se imprima lo siguiente:
1
2
3
0
-1El código de nuestra aplicación de contador es el siguiente. Todo el código se ha escrito en el mismo archivo, por lo que store está directamente disponible para el código React. Más adelante conoceremos mejores formas de estructurar el código React/Redux.
import ReactDOM from 'react-dom/client'
import { createStore } from 'redux'
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
case 'ZERO':
return 0
default:
return state
}
}
const store = createStore(counterReducer)
const App = () => {
return (
<div>
<div>{store.getState()}</div>
<button onClick={() => store.dispatch({ type: 'INCREMENT' })}>
plus
</button>
<button onClick={() => store.dispatch({ type: 'DECREMENT' })}>
minus
</button>
<button onClick={() => store.dispatch({ type: 'ZERO' })}>
zero
</button>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'))
const renderApp = () => {
root.render(<App />)
}
renderApp()
store.subscribe(renderApp)Hay algunas cosas notables en el código. App muestra el valor del contador solicitándolo al store con el método store.getState(). Los controladores de acciones de los botones envían (dispatch) las acciones correctas al store.
Cuando se cambia el estado del store, React no puede volver a re-renderizar automáticamente la aplicación. Por lo tanto, hemos registrado una función renderApp , que renderiza toda la aplicación, para escuchar cambios en el store con el método store.subscribe. Ten en cuenta que tenemos que invocar inmediatamente al método renderApp. Sin la invocación, el primer renderizado de la aplicación nunca se produciría.
Una nota sobre el uso de createStore
Los más atentos notarán que el nombre de la función createStore está tachado. Si pasas el mouse sobre el nombre, aparecerá una explicación

La explicación completa es la siguiente:
Recomendamos utilizar el método configureStore del paquete @reduxjs/toolkit, que reemplaza a createStore.
Redux Toolkit es nuestro enfoque recomendado para escribir la lógica de Redux hoy, incluida la configuración de store, reducers, la obtención de datos y más.
Para obtener más detalles, lea esta página de documentación de Redux: https://redux.js.org/introduction/why-rtk-is-redux-today
configureStore de Redux Toolkit es una versión mejorada de createStore que simplifica la configuración y ayuda a evitar errores comunes.
No deberías usar el paquete principal de redux por sí solo hoy en día, excepto con fines de aprendizaje. El método createStore del paquete core de redux no se eliminará, pero alentamos a todos los usuarios a migrar al uso de Redux Toolkit para todo el código de Redux.
Entonces, en lugar de la función createStore, se recomienda usar la función un poco más "avanzada" configureStore, y también la usaremos cuando nos hayamos hecho cargo de la funcionalidad básica de Redux.
Nota adicional: createStore se define como "obsoleto", lo que generalmente significa que la función se eliminará en alguna versión más nueva de la librería. La explicación anterior y esta discusión revelan que createStore no se eliminará y se le ha dado el estado obsoleto, quizás por motivos ligeramente incorrectos. Por lo tanto, la función no está obsoleta, pero hoy en día existe una forma nueva y preferible de hacer casi lo mismo.
Redux-notas
Nuestro objetivo es modificar nuestra aplicación de notas para utilizar Redux para la gestión del estado. Sin embargo, primero cubramos algunos conceptos clave a través de una aplicación de notas simplificada.
La primera versión de nuestra aplicación, escrita en el archivo main.jsx, se ve de la siguiente manera:
import ReactDOM from 'react-dom/client'
import { createStore } from 'redux'
const noteReducer = (state = [], action) => {
switch (action.type) {
case 'NEW_NOTE':
state.push(action.payload)
return state
default:
return state
}
}
const store = createStore(noteReducer)
store.dispatch({
type: 'NEW_NOTE',
payload: {
content: 'the app state is in redux store',
important: true,
id: 1
}
})
store.dispatch({
type: 'NEW_NOTE',
payload: {
content: 'state changes are made with actions',
important: false,
id: 2
}
})
const App = () => {
return (
<div>
<ul>
{store.getState().map(note => (
<li key={note.id}>
{note.content} <strong>{note.important ? 'important' : ''}</strong>
</li>
))}
</ul>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'))
const renderApp = () => {
root.render(<App />)
}
renderApp()
store.subscribe(renderApp)Hasta el momento la aplicación no tiene la funcionalidad para agregar nuevas notas, aunque es posible hacerlo enviando acciones NEW_NOTE.
Ahora las acciones tienen un tipo y un campo payload (carga), que contiene la nota a agregar:
{
type: 'NEW_NOTE',
payload: {
content: 'state changes are made with actions',
important: false,
id: 2
}
}La elección del nombre del campo es arbitraria. La convención es que las acciones tengan exactamente dos campos, type diciendo el tipo y payload conteniendo los datos incluidos en la acción.
Funciones puras, inmutables
La versión inicial del reducer es muy sencilla:
const noteReducer = (state = [], action) => {
switch (action.type) {
case 'NEW_NOTE':
state.push(action.payload)
return state
default:
return state
}
}El estado ahora es un Array. Las acciones de tipo NEW_NOTE hacen que se agregue una nueva nota al estado con el método push.
La aplicación parece estar funcionando, pero el reducer que hemos declarado es malo. Rompe el supuesto básico de que los reducers deben ser funciones puras.
Las funciones puras son aquellas que no causan ningún efecto secundario y siempre deben devolver la misma respuesta cuando se llaman con los mismos parámetros.
Agregamos una nueva nota al estado con el método state.push(action.payload) que cambia el estado del objeto-estado. Esto no está permitido. El problema se resuelve fácilmente utilizando el método concat, que crea un nuevo array, que contiene todos los elementos del array anterior y el nuevo elemento:
const noteReducer = (state = [], action) => {
switch (action.type) {
case 'NEW_NOTE':
return state.concat(action.payload) default:
return state
}
}El estado de un reducer debe estar compuesto por objetos inmutables. Si hay un cambio en el estado, el objeto antiguo no se cambia, sino que se reemplaza por un objeto nuevo modificado. Esto es exactamente lo que hicimos con el nuevo reducer: el array anterior se reemplaza por el nuevo.
Ampliemos nuestro reducer para que pueda manejar el cambio de importancia de una nota:
{
type: 'TOGGLE_IMPORTANCE',
payload: {
id: 2
}
}Dado que todavía no tenemos ningún código que utilice esta funcionalidad, estamos expandiendo el reducer en la forma 'test driven' (guiada por pruebas).
Configurando el entorno de pruebas
Tenemos que configurar primero la biblioteca de pruebas Vitest para el proyecto. Vamos a instalarla como una dependencia de desarrollo para la aplicación:
npm install --save-dev vitestExpandamos package.json con un script para ejecutar las pruebas:
{
// ...
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest" },
// ...
}Para hacer las pruebas más fáciles, primero trasladaremos el código del reducer a su propio módulo, al archivo src/reducers/noteReducer.js:
const noteReducer = (state = [], action) => {
switch (action.type) {
case 'NEW_NOTE':
return state.concat(action.payload)
default:
return state
}
}
export default noteReducerEl archivo main.jsx cambia de la siguiente manera:
import ReactDOM from 'react-dom/client'
import { createStore } from 'redux'
import noteReducer from './reducers/noteReducer'
const store = createStore(noteReducer)
// ...También agregaremos la librería deep-freeze, que se puede usar para garantizar que el reducer se haya definido correctamente como una función inmutable. Instalemos la librería como una dependencia de desarrollo:
npm install --save-dev deep-freezeAhora estamos listos para escribir pruebas.
Pruebas para noteReducer
Comencemos creando una prueba para manejar la acción NEW_NOTE. La prueba, que definimos en el archivo src/reducers/noteReducer.test.js, tiene el siguiente contenido:
import deepFreeze from 'deep-freeze'
import { describe, expect, test } from 'vitest'
import noteReducer from './noteReducer'
describe('noteReducer', () => {
test('returns new state with action NEW_NOTE', () => {
const state = []
const action = {
type: 'NEW_NOTE',
payload: {
content: 'the app state is in redux store',
important: true,
id: 1
}
}
deepFreeze(state)
const newState = noteReducer(state, action)
expect(newState).toHaveLength(1)
expect(newState).toContainEqual(action.payload)
})
})Ejecuta la prueba con npm test. La prueba asegura que el nuevo estado devuelto por el reducer es un array que contiene un solo elemento, que es el mismo objeto que el que está en el campo payload de la acción.
El comando deepFreeze(state) asegura que el reducer no cambie el estado del store que se le dio como parámetro. Si el reducer usa el comando push para manipular el estado, la prueba no pasará

Ahora crearemos una prueba para la acción TOGGLE_IMPORTANCE:
test('returns new state with action TOGGLE_IMPORTANCE', () => {
const state = [
{
content: 'the app state is in redux store',
important: true,
id: 1
},
{
content: 'state changes are made with actions',
important: false,
id: 2
}
]
const action = {
type: 'TOGGLE_IMPORTANCE',
payload: {
id: 2
}
}
deepFreeze(state)
const newState = noteReducer(state, action)
expect(newState).toHaveLength(2)
expect(newState).toContainEqual(state[0])
expect(newState).toContainEqual({
content: 'state changes are made with actions',
important: true,
id: 2
})
})Entonces la siguiente acción
{
type: 'TOGGLE_IMPORTANCE',
payload: {
id: 2
}
}tiene que cambiar la importancia de la nota con el id 2.
El reducer se expande de la siguiente manera
const noteReducer = (state = [], action) => {
switch(action.type) {
case 'NEW_NOTE':
return state.concat(action.payload)
case 'TOGGLE_IMPORTANCE': { const id = action.payload.id const noteToChange = state.find(n => n.id === id) const changedNote = { ...noteToChange, important: !noteToChange.important } return state.map(note => (note.id !== id ? note : changedNote)) } default:
return state
}
}Creamos una copia de la nota cuya importancia ha cambiado con la sintaxis de la parte 2, y reemplazamos el estado con un nuevo estado que contiene todas las notas que no han cambiado y la copia de la nota cambiada changedNote.
Recapitulemos lo que sucede en el código. Primero, buscamos un objeto de nota específico, cuya importancia queremos cambiar:
const noteToChange = state.find(n => n.id === id)luego creamos un nuevo objeto, que es una copia de la nota original, solo el valor del campo important se ha cambiado a lo opuesto de lo que era:
const changedNote = {
...noteToChange,
important: !noteToChange.important
}Entonces se devuelve un nuevo estado. Lo creamos tomando todas las notas del estado anterior, excepto la nota deseada, que reemplazamos con su copia ligeramente alterada:
state.map(note => (note.id !== id ? note : changedNote))Array spread syntax
Debido a que ahora tenemos pruebas bastante buenas para el reducer, podemos refactorizar el código de forma segura.
Agregar una nueva nota crea el estado devuelto por la función de Arrays concat. Echemos un vistazo a cómo podemos lograr lo mismo usando la sintaxis array spread de JavaScript:
const noteReducer = (state = [], action) => {
switch(action.type) {
case 'NEW_NOTE':
return [...state, action.payload] case 'TOGGLE_IMPORTANCE': {
// ...
}
default:
return state
}
}La sintaxis spread funciona de la siguiente manera. Si declaramos
const numbers = [1, 2, 3]...numbers divide el array en elementos individuales, que se pueden colocar en otro array.
[...numbers, 4, 5]y el resultado es un array [1, 2, 3, 4, 5].
Si hubiéramos colocado el array en otro array sin el spread
[numbers, 4, 5]el resultado habría sido [ [1, 2, 3], 4, 5].
Cuando tomamos elementos de un array mediante la desestructuración, se usa una sintaxis similar para juntar el resto de los elementos:
const numbers = [1, 2, 3, 4, 5, 6]
const [first, second, ...rest] = numbers
console.log(first) // prints 1
console.log(second) // prints 2
console.log(rest) // prints [3, 4, 5, 6]Formulario no controlado
Agreguemos la funcionalidad para agregar nuevas notas y cambiar su importancia:
// ...
const generateId = () => Number((Math.random() * 1000000).toFixed(0))
const App = () => {
const addNote = event => { event.preventDefault() const content = event.target.note.value event.target.note.value = '' store.dispatch({ type: 'NEW_NOTE', payload: { content, important: false, id: generateId() } }) }
const toggleImportance = id => { store.dispatch({ type: 'TOGGLE_IMPORTANCE', payload: { id } }) }
return (
<div>
<form onSubmit={addNote}> <input name="note" /> <button type="submit">add</button> </form> <ul>
{store.getState().map(note => (
<li key={note.id} onClick={() => toggleImportance(note.id)}> {note.content} <strong>{note.important ? 'important' : ''}</strong>
</li>
))}
</ul>
</div>
)
}
// ...La implementación de ambas funcionalidades es sencilla. Cabe señalar que no hemos vinculado el estado de los campos del formulario al estado del componente App como lo hicimos anteriormente. React llama a este tipo de formulario no controlado.
Los formularios no controlados tienen ciertas limitaciones (por ejemplo, no son posibles los mensajes de error dinámicos o la desactivación del botón de envío en función de input). Sin embargo, son adecuados para nuestras necesidades actuales.
Puedes leer más sobre formularios no controlados aquí.
El método para agregar nuevas notas es simple, simplemente envía la acción para agregar notas:
addNote = event => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
store.dispatch({
type: 'NEW_NOTE',
payload: {
content,
important: false,
id: generateId()
}
})
}Podemos obtener el contenido de la nueva nota directamente desde el campo del formulario. Debido a que el campo tiene un nombre, podemos acceder al contenido a través del objeto del evento event.target.note.value.
const content = event.target.note.value<form onSubmit={addNote}>
<input name="note" /> <button type="submit">add</button>
</form>La importancia de una nota se puede cambiar haciendo clic en su nombre. El controlador de eventos es muy simple:
toggleImportance = id => {
store.dispatch({
type: 'TOGGLE_IMPORTANCE',
payload: { id }
})
}Action creators
Comenzamos a notar que, incluso en aplicaciones tan simples como la nuestra, usar Redux puede simplificar el código de la interfaz. Sin embargo, podemos hacerlo mucho mejor.
En realidad, no es necesario que los componentes de React conozcan los tipos y formas de acción de Redux. Separemos la creación de acciones en sus propias funciones:
const createNote = content => {
return {
type: 'NEW_NOTE',
payload: {
content,
important: false,
id: generateId()
}
}
}
const toggleImportanceOf = id => {
return {
type: 'TOGGLE_IMPORTANCE',
payload: { id }
}
}Las funciones que crean acciones se denominan action creators (creadores de acciones).
El componente App ya no tiene que saber nada sobre la representación interna de las acciones, solo obtiene la acción correcta llamando a la función creadora:
const App = () => {
const addNote = event => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
store.dispatch(createNote(content))
}
const toggleImportance = id => {
store.dispatch(toggleImportanceOf(id)) }
// ...
}Reenviando Redux-Store a varios componentes
Aparte del reducer, nuestra aplicación está en un solo archivo. Esto, por supuesto, no es sensato, y deberíamos separar App en su propio módulo.
Ahora la pregunta es, ¿cómo puede App acceder al store después de moverlo? Y en términos más generales, cuando un componente está compuesto por muchos componentes más pequeños, debe haber una forma para que todos los componentes accedan al store. Hay varias formas de compartir el store redux con los componentes. Primero veremos la forma más nueva, y posiblemente la más fácil, usando la api de hooks de la librería react-redux.
Primero instalamos react-redux
npm install react-reduxA continuación, organicemos el código de la aplicación de forma más sensata en varios archivos. Después de los cambios, main.jsx queda así:
import ReactDOM from 'react-dom/client'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './App'
import noteReducer from './reducers/noteReducer'
const store = createStore(noteReducer)
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)Ten en cuenta que la aplicación ahora se define como un elemento secundario de un componente Provider (proveedor) proporcionado por la librería react-redux. El store de la aplicación se entrega al Provider como su atributo store.
const store = createStore(noteReducer)
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}> <App />
</Provider>)La definición de los action creators se ha movido al archivo reducers/noteReducer.js donde se define el reducer. El archivo se ve así:
const noteReducer = (state = [], action) => {
switch (action.type) {
case 'NEW_NOTE':
return [...state, action.payload]
case 'TOGGLE_IMPORTANCE': {
const id = action.payload.id
const noteToChange = state.find(n => n.id === id)
const changedNote = {
...noteToChange,
important: !noteToChange.important
}
return state.map(note => (note.id !== id ? note : changedNote))
}
default:
return state
}
}
const generateId = () =>
Number((Math.random() * 1000000).toFixed(0))
export const createNote = (content) => {
return {
type: 'NEW_NOTE',
payload: {
content,
important: false,
id: generateId()
}
}
}
export const toggleImportanceOf = (id) => {
return {
type: 'TOGGLE_IMPORTANCE',
payload: { id }
}
}
export default noteReducerSi la aplicación tiene muchos componentes que necesitan el store, el componente App debe pasar store como props a todos esos componentes.
El módulo ahora tiene varios comandos de export.
La función del reducer todavía se devuelve con el comando de export default, por lo que el reducer se puede importar de la forma habitual:
import noteReducer from './reducers/noteReducer'Un módulo solo puede tener un default export, pero varias exportaciones "normales"
export const createNote = (content) => {
// ...
}
export const toggleImportanceOf = (id) => {
// ...
}Las funciones exportadas normalmente (no como los default) se pueden importar con la sintaxis de llaves:
import { createNote } from '../../reducers/noteReducer'Código para el componente App
import { createNote, toggleImportanceOf } from './reducers/noteReducer'
import { useSelector, useDispatch } from 'react-redux'
const App = () => {
const dispatch = useDispatch()
const notes = useSelector(state => state)
const addNote = (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
dispatch(createNote(content))
}
const toggleImportance = (id) => {
dispatch(toggleImportanceOf(id))
}
return (
<div>
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
<ul>
{notes.map(note =>
<li
key={note.id}
onClick={() => toggleImportance(note.id)}
>
{note.content} <strong>{note.important ? 'important' : ''}</strong>
</li>
)}
</ul>
</div>
)
}
export default AppHay algunas cosas a tener en cuenta en el código. Anteriormente, el código despachaba acciones invocando al método dispatch de redux-store:
store.dispatch({
type: 'TOGGLE_IMPORTANCE',
payload: { id }
})Ahora lo hace con la función dispatch del hook useDispatch.
import { useSelector, useDispatch } from 'react-redux'
const App = () => {
const dispatch = useDispatch() // ...
const toggleImportance = (id) => {
dispatch(toggleImportanceOf(id)) }
// ...
}El hook useDispatch proporciona acceso a cualquier componente de React a la función dispatch de redux-store definida en main.jsx. Esto permite que todos los componentes realicen cambios en el estado de Redux store.
El componente puede acceder a las notas almacenadas en el store con el hook useSelector de la librería react-redux.
import { useSelector, useDispatch } from 'react-redux'
const App = () => {
// ...
const notes = useSelector(state => state) // ...
}useSelector recibe una función como parámetro. La función busca o selecciona datos del store de Redux. Aquí necesitamos todas las notas, por lo que nuestra función de selector devuelve el estado completo:
state => stateque es una abreviatura de
(state) => {
return state
}Por lo general, las funciones de selector son un poco más interesantes y solo devuelven partes seleccionadas del contenido del store redux. Por ejemplo, podríamos devolver solo notas marcadas como importantes:
const importantNotes = useSelector(state => state.filter(note => note.important)) La versión actual de la aplicación se puede encontrar en GitHub, en la rama part6-0.
Más componentes
Separemos el formulario responsable de crear una nueva nota en su propio componente en el archivo src/components/NoteForm.jsx:
import { useDispatch } from 'react-redux'
import { createNote } from '../reducers/noteReducer'
const NoteForm = () => {
const dispatch = useDispatch()
const addNote = (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
dispatch(createNote(content))
}
return (
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
)
}
export default NoteFormA diferencia del código de React que hicimos sin Redux, el controlador de eventos para cambiar el estado de la aplicación (que ahora vive en Redux) se ha movido de App a un componente hijo. La lógica para cambiar el estado en Redux todavía está claramente separada de toda la parte de React de la aplicación.
También separaremos la lista de notas y mostraremos una sola nota en sus propios componentes. Coloquemos ambos en el archivo src/components/Notes.jsx:
import { useDispatch, useSelector } from 'react-redux'
import { toggleImportanceOf } from '../reducers/noteReducer'
const Note = ({ note, handleClick }) => {
return (
<li onClick={handleClick}>
{note.content}
<strong> {note.important ? 'important' : ''}</strong>
</li>
)
}
const Notes = () => {
const dispatch = useDispatch()
const notes = useSelector(state => state)
return (
<ul>
{notes.map(note => (
<Note
key={note.id}
note={note}
handleClick={() => dispatch(toggleImportanceOf(note.id))}
/>
))}
</ul>
)
}
export default NotesLa lógica para cambiar la importancia de una nota ahora está en el componente que administra la lista de notas.
Solo queda una pequeña cantidad de código en el archivo App.jsx:
import NoteForm from './components/NoteForm'
import Notes from './components/Notes'
const App = () => {
return (
<div>
<NoteForm />
<Notes />
</div>
)
}
export default AppNote, responsable de representar una sola nota, es muy simple y no es consciente de que el controlador de eventos que obtiene como props despacha una acción. Este tipo de componentes se denominan presentacionales en la terminología de React.
Notes, por otro lado, es un componente contenedor, ya que contiene cierta lógica de aplicación: define lo que hacen los controladores de eventos de los componentes Note y coordina la configuración de los componentes presentacionales, es decir, los Notes.
El código de la aplicación Redux se puede encontrar en GitHub, en la rama part6-1.
Continuemos nuestro trabajo con la versión Redux simplificada de nuestra aplicación de notas.
Para facilitar nuestro desarrollo, cambiemos nuestro reducer para que el store se inicialice con un estado que contenga un par de notas:
const initialState = [ { content: 'reducer defines how redux store works', important: true, id: 1, }, { content: 'state of store can contain any data', important: false, id: 2, },]
const noteReducer = (state = initialState, action) => { // ...
}
// ...
export default noteReducerStore con estado complejo
Implementemos el filtrado de las notas que se muestran al usuario. La interfaz de usuario para los filtros se implementará con botones de radio:

Comencemos con una implementación muy simple y directa:
import NoteForm from './components/NoteForm'
import Notes from './components/Notes'
const App = () => {
const filterSelected = (value) => { console.log(value) }
return (
<div>
<NoteForm />
<div> <input type="radio" name="filter" onChange={() => filterSelected('ALL')} /> all <input type="radio" name="filter" onChange={() => filterSelected('IMPORTANT')} /> important <input type="radio" name="filter" onChange={() => filterSelected('NONIMPORTANT')} /> nonimportant </div> <Notes />
</div>
)
}Dado que el atributo name de todos los botones de radio es el mismo, estos forman un button group (grupo de botones) en el que solo se puede seleccionar una opción.
Los botones tienen un controlador de cambios que actualmente solo imprime el string asociado con el botón en el que se hizo clic en la consola.
En la siguiente sección, vamos a implementar el filtrado almacenando las notas y el valor del filtro en el store de redux. Cuando terminemos, nos gustaría que el estado del store se viera así:
{
notes: [
{ content: 'reducer defines how redux store works', important: true, id: 1},
{ content: 'state of store can contain any data', important: false, id: 2}
],
filter: 'IMPORTANT'
}Solo el array de notas se almacenaba en el estado de la implementación anterior de nuestra aplicación. En la nueva implementación, el objeto de estado tiene dos propiedades, notes que contienen el array de notas y filter que contiene un string que indica qué notas deben mostrarse al usuario.
Reducers combinados
Podríamos modificar nuestro reducer actual para hacer frente a la nueva forma del estado. Sin embargo, una mejor solución en esta situación es definir un nuevo reducer separado para el estado del filtro:
const filterReducer = (state = 'ALL', action) => {
switch (action.type) {
case 'SET_FILTER':
return action.payload
default:
return state
}
}
export const filterChange = filter => {
return {
type: 'SET_FILTER',
payload: filter
}
}
export default filterReducerLas acciones para cambiar el estado del filtro se ven así:
{
type: 'SET_FILTER',
payload: 'IMPORTANT'
}Podemos crear el reducer que nuestra aplicación realmente utilizara al combinar los dos reducers existentes con la función combineReducers.
Definamos el reducer combinado en el archivo main.jsx:
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { createStore, combineReducers } from 'redux'
import App from './App'
import filterReducer from './reducers/filterReducer'
import noteReducer from './reducers/noteReducer'
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer
})
const store = createStore(reducer)
console.log(store.getState())
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<div />
</Provider>
)Dado que nuestra aplicación se rompe por completo en este punto, renderizamos un elemento div vacío en lugar del componente App.
El estado del store se imprime en la consola:

Como podemos ver en el resultado, ¡el store tiene la forma exacta que queríamos!
Echemos un vistazo más de cerca a cómo se crea el reducer combinado:
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer,
})El estado del store definido por este reducer es un objeto con dos propiedades: notes y filter. El valor de la propiedad notes es definido por noteReducer, que no tiene que lidiar con las otras propiedades del estado. Asimismo, la propiedad filter es administrada por filterReducer.
Antes de realizar más cambios en el código, echemos un vistazo a cómo las diferentes acciones cambian el estado del store definido por el reducer combinado. Agreguemos lo siguiente al archivo main.jsx:
// ...
const store = createStore(reducer)
console.log(store.getState())
import { createNote } from './reducers/noteReducer'import { filterChange } from './reducers/filterReducer'
store.subscribe(() => console.log(store.getState()))store.dispatch(filterChange('IMPORTANT'))store.dispatch(createNote('combineReducers forms one reducer from many simple reducers'))
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<div />
</Provider>
)Al simular la creación de una nota y cambiar el estado del filtro de esta manera, el estado del store se muestra en la consola después de cada cambio que se realiza en el store:

En este punto es bueno darse cuenta de un pequeño pero importante detalle. Si agregamos un console log al comienzo de ambos reducers (noteReducer y filterReducer):
const filterReducer = (state = 'ALL', action) => {
console.log('ACTION: ', action) // ...
}Según el resultado de la consola, uno podría tener la impresión de que cada acción se duplica:

¿Hay algún bug en nuestro código? No. El reducer combinado funciona de tal manera que cada acción es controlada en cada parte del reducer combinado, o en otras palabras, cada reducer "escucha" a todas las acciones despachadas y hace algo con ellas si así se lo hemos instruido. Normalmente, solo un reducer está interesado en una acción determinada, pero hay situaciones en las que varios reducers cambian sus respectivas partes del estado en función de la misma acción.
Terminando los filtros
Terminemos la aplicación para que utilice el reducer combinado. Comenzamos cambiando la renderización de la aplicación y conectando el store a la aplicación en el archivo main.jsx:
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { createStore, combineReducers } from 'redux'
import App from './App'
import filterReducer from './reducers/filterReducer'
import noteReducer from './reducers/noteReducer'
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer
})
const store = createStore(reducer)
console.log(store.getState())
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)A continuación, solucionemos un error causado por el código que espera que la store de aplicaciones sea un array de notas:

Es una solución fácil. Debido a que las notas están en el campo notes del store, solo tenemos que hacer un pequeño cambio en la función de selector:
const Notes = () => {
const dispatch = useDispatch()
const notes = useSelector(state => state.notes)
return(
<ul>
{notes.map(note =>
<Note
key={note.id}
note={note}
handleClick={() =>
dispatch(toggleImportanceOf(note.id))
}
/>
)}
</ul>
)
}Anteriormente, la función de selector devolvía el estado completo del store:
const notes = useSelector(state => state)Y ahora devuelve solo su campo notes
const notes = useSelector(state => state.notes)Extraigamos el filtro de visibilidad en su propio componente src/components/VisibilityFilter.jsx:
import { useDispatch } from 'react-redux'
import { filterChange } from '../reducers/filterReducer'
const VisibilityFilter = () => {
const dispatch = useDispatch()
return (
<div>
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('ALL'))}
/>
all
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('IMPORTANT'))}
/>
important
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('NONIMPORTANT'))}
/>
nonimportant
</div>
)
}
export default VisibilityFilterCon el nuevo componente, App se puede simplificar de la siguiente manera:
import NoteForm from './components/NoteForm'
import Notes from './components/Notes'
import VisibilityFilter from './components/VisibilityFilter'
const App = () => {
return (
<div>
<NoteForm />
<VisibilityFilter />
<Notes />
</div>
)
}
export default AppLa implementación es bastante sencilla. Al hacer clic en los diferentes radio buttons, cambia el estado de la propiedad filter del store.
Cambiemos el componente Notes para incorporar el filtro:
const Notes = () => {
const dispatch = useDispatch()
const notes = useSelector(state => { if (state.filter === 'ALL') { return state.notes } return state.filter === 'IMPORTANT' ? state.notes.filter(note => note.important) : state.notes.filter(note => !note.important) })
return (
<ul>
{notes.map(note => (
<Note
key={note.id}
note={note}
handleClick={() => dispatch(toggleImportanceOf(note.id))}
/>
))}
</ul>
)
}Solo realizamos cambios en la función de selector, que solía ser
useSelector(state => state.notes)Simplifiquemos el selector desestructurando los campos del estado que recibe como parámetro:
const notes = useSelector(({ filter, notes }) => {
if ( filter === 'ALL' ) {
return notes
}
return filter === 'IMPORTANT'
? notes.filter(note => note.important)
: notes.filter(note => !note.important)
})Hay un pequeño defecto cosmético en nuestra aplicación. Aunque el filtro está configurado en ALL de forma predeterminada, el radio button asociado no está seleccionado. Naturalmente, este problema se puede solucionar, pero como se trata de un error desagradable pero, en última instancia, inofensivo, dejaremos la solución para más adelante.
La versión actual de la aplicación se puede encontrar en GitHub, en la rama part6-2.
Redux Toolkit y refactorizando la configuración del Store
Como hemos visto hasta ahora, la implementación de la gestión del estado y la configuración de Redux requiere bastante esfuerzo. Esto se manifiesta, por ejemplo, en el código relacionado con el reducer y el action creator, que tiene un código un tanto repetitivo. Redux Toolkit es una librería que resuelve estos problemas comunes relacionados con Redux. La librería, por ejemplo, simplifica enormemente la configuración del store de Redux y ofrece una gran variedad de herramientas para facilitar la gestión del estado.
Comencemos a usar Redux Toolkit en nuestra aplicación refactorizando el código existente. Primero, necesitaremos instalar la librería:
npm install @reduxjs/toolkitA continuación, abre el archivo main.jsx que actualmente crea la store de Redux. En lugar de la función createStore de Redux, creemos el Store usando la función configureStore de Redux Toolkit:
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'
import App from './App'
import filterReducer from './reducers/filterReducer'
import noteReducer from './reducers/noteReducer'
const store = configureStore({ reducer: { notes: noteReducer, filter: filterReducer }})
console.log(store.getState())
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)Ya nos deshicimos de algunas líneas de código, ya no necesitamos la función combineReducers para crear el reducer del store. Pronto veremos que la función configureStore tiene muchos beneficios adicionales, como la integración sin esfuerzo de herramientas de desarrollo y muchas librerías de uso común sin necesidad de configuración adicional.
Limpiemos aún más el archivo main.jsx moviendo el código relacionado con la creación del store de Redux a su propio archivo. Creemos un nuevo archivo src/store.js:
import { configureStore } from '@reduxjs/toolkit'
import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'
const store = configureStore({
reducer: {
notes: noteReducer,
filter: filterReducer
}
})
export default storeDespués de los cambios, el contenido del archivo main.jsx es el siguiente:
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)Redux Toolkit y refactorizando los reducers
Pasemos a refactorizar los reducers, lo que trae consigo los beneficios de Redux Toolkit. Con Redux Toolkit, podemos crear fácilmente reducers y action creators relacionados utilizando la función createSlice. Podemos usar la función createSlice para refactorizar el reducer y los action creators en el archivo reducers/noteReducer.js de la siguiente manera:
import { createSlice } from '@reduxjs/toolkit'
const initialState = [
{
content: 'reducer defines how redux store works',
important: true,
id: 1,
},
{
content: 'state of store can contain any data',
important: false,
id: 2,
},
]
const generateId = () =>
Number((Math.random() * 1000000).toFixed(0))
const noteSlice = createSlice({ name: 'notes', initialState, reducers: { createNote(state, action) { const content = action.payload state.push({ content, important: false, id: generateId(), }) }, toggleImportanceOf(state, action) { const id = action.payload const noteToChange = state.find(n => n.id === id) const changedNote = { ...noteToChange, important: !noteToChange.important } return state.map(note => note.id !== id ? note : changedNote ) } },})
export const { createNote, toggleImportanceOf } = noteSlice.actionsexport default noteSlice.reducerEl parámetro name de la función createSlice define el prefijo que se utiliza en los valores de tipo de la acción. Por ejemplo, la acción createNote definida más adelante tendrá el valor de tipo notes/createNote. Es una buena práctica dar al parámetro un valor que sea único entre los reducers. De esta forma no habrá colisiones inesperadas entre los valores de tipo de acción de la aplicación. El parámetro initialState define el estado inicial del reducer. El parámetro reducers toma al propio reducer como un objeto, cuyas funciones manejan los cambios de estado causados por ciertas acciones. Ten en cuenta que action.payload en la función contiene el argumento proporcionado al llamar al creador de la acción:
dispatch(createNote('Redux Toolkit is awesome!'))Esta llamada a dispatch equivale a enviar el siguiente objeto:
dispatch({ type: 'notes/createNote', payload: 'Redux Toolkit is awesome!' })Si has prestado atención, es posible que hayas notado que dentro de la acción createNote, parece suceder algo que viola el principio de inmutabilidad de los reducers mencionado anteriormente:
createNote(state, action) {
const content = action.payload
state.push({
content,
important: false,
id: generateId(),
})
}Estamos mutando el array del argumento state al llamar al método push en lugar de devolver una nueva instancia del array. ¿De qué se trata todo esto?
Redux Toolkit utiliza la librería Immer con reducers creados por la función createSlice, lo que hace posible mutar el argumento state dentro del reducer. Immer usa el estado mutado para producir un nuevo estado inmutable y, por lo tanto, los cambios de estado permanecen inmutables. Ten en cuenta que state se puede cambiar sin "mutarlo", como hemos hecho con la acción toggleImportanceOf. En este caso, la función devuelve el nuevo estado directamente. Sin embargo, mutar el estado a menudo será útil, especialmente cuando se necesita actualizar un estado complejo.
La función createSlice devuelve un objeto que contiene al reducer así como a los action creators definidos por el parámetro reducers. Se puede acceder al reducer mediante la propiedad noteSlice.reducer, mientras que a los action creators mediante la propiedad noteSlice.actions. Podemos producir las exportaciones del archivo de la siguiente manera:
const noteSlice = createSlice({
// ...
})
export const { createNote, toggleImportanceOf } = noteSlice.actionsexport default noteSlice.reducerLas importaciones en otros archivos funcionarán igual que antes:
import noteReducer, { createNote, toggleImportanceOf } from './reducers/noteReducer'Necesitamos modificar los nombres de los tipos de las acciones en las pruebas debido a las convenciones de ReduxToolkit:
import deepFreeze from 'deep-freeze'
import { describe, expect, test } from 'vitest'
import noteReducer from './noteReducer'
describe('noteReducer', () => {
test('returns new state with action notes/createNote', () => { const state = []
const action = {
type: 'notes/createNote', payload: 'the app state is in redux store' }
deepFreeze(state)
const newState = noteReducer(state, action)
expect(newState).toHaveLength(1)
expect(newState.map(note => note.content)).toContainEqual(action.payload) })
})
test('returns new state with action notes/toggleImportanceOf', () => { const state = [
{
content: 'the app state is in redux store',
important: true,
id: 1
},
{
content: 'state changes are made with actions',
important: false,
id: 2
}
]
const action = {
type: 'notes/toggleImportanceOf', payload: 2 }
deepFreeze(state)
const newState = noteReducer(state, action)
expect(newState).toHaveLength(2)
expect(newState).toContainEqual(state[0])
expect(newState).toContainEqual({
content: 'state changes are made with actions',
important: true,
id: 2
})
})Puedes encontrar el código de nuestra aplicación actual en su totalidad en la rama part6-3 de este repositorio de GitHub.
Redux Toolkit y console.log
Como hemos aprendido, console.log es una herramienta extremadamente poderosa, por lo general siempre nos salva de problemas.
Intentemos imprimir el estado del store de Redux en la consola en medio del reducer creado con la función createSlice:
const noteSlice = createSlice({
name: 'notes',
initialState,
reducers: {
// ...
toggleImportanceOf(state, action) {
const id = action.payload
const noteToChange = state.find(n => n.id === id)
const changedNote = {
...noteToChange,
important: !noteToChange.important
}
console.log(state)
return state.map(note =>
note.id !== id ? note : changedNote
)
}
},
})Lo siguiente se imprime en la consola

Lo que vemos es interesante pero no muy útil. Esto tiene que ver con la librería Immer que mencionamos anteriormente y es utilizada por Redux Toolkit internamente para guardar el estado de la Tienda.
El estado se puede convertir a un formato legible por humanos utilizando la función current de la librería immer.
Actualicemos las importaciones para incluir a la función "current" de la librería immer:
import { current } from '@reduxjs/toolkit'Luego actualicemos el llamado a la función console.log:
console.log(current(state))Ahora lo que imprime la consola es legible para humanos

Redux DevTools
Redux DevTools es una extension de Chrome, que ofrece útiles herramientas de desarrollo para Redux. Se puede usar, por ejemplo, para inspeccionar el estado del store de Redux y enviar acciones (dispatch) a través de la consola del navegador. Cuando el store se crea usando la función configureStore de Redux Toolkit, no se necesita ninguna configuración adicional para que Redux DevTools funcione.
Una vez instalada la extension, al hacer clic en la pestaña de Redux en las herramientas de desarrollo del navegador, Redux DevTools debería abrirse:

Puedes inspeccionar cómo el envío de una determinada acción cambia el estado haciendo clic en la acción:

También es posible enviar acciones (dispatch) a la store utilizando las herramientas de desarrollo:

El código actual de la aplicación se puede encontrar en GitHub, en la rama part6-3.
Configuración de JSON Server
Expandamos la aplicación, de modo que las notas se almacenen en el backend. Usaremos json-server, de la parte 2.
El estado inicial de la base de datos se almacena en el archivo db.json, que se coloca en la raíz del proyecto:
{
"notes": [
{
"content": "the app state is in redux store",
"important": true,
"id": 1
},
{
"content": "state changes are made with actions",
"important": false,
"id": 2
}
]
}Instalaremos json-server en nuestro proyecto...
npm install json-server --save-devy agregaremos la siguiente línea a la parte de scripts del archivo package.json
"scripts": {
"server": "json-server -p 3001 db.json",
// ...
}Ahora iniciemos json-server con el comando npm run server.
Fetch API
En el desarrollo de software, a menudo es necesario considerar si una cierta funcionalidad debe implementarse usando una librería externa o si es mejor utilizar las soluciones nativas proporcionadas por el entorno. Ambos enfoques tienen sus propias ventajas y desafíos.
En las partes anteriores de este curso, usamos la librería Axios para hacer peticiones HTTP. Ahora, exploremos una forma alternativa de hacer peticiones HTTP usando la Fetch API nativa.
Es típico que una librería externa como Axios se implemente usando otras librerías externas. Por ejemplo, si instalas Axios en tu proyecto con el comando npm install axios, la salida de la consola será:
$ npm install axios
added 23 packages, and audited 302 packages in 1s
71 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilitiesPor lo tanto, además de la librería Axios, el comando instalaría más de 20 paquetes npm adicionales que Axios necesita para funcionar.
La Fetch API proporciona una forma similar de hacer peticiones HTTP como Axios, pero usar la Fetch API no requiere instalar ninguna librería externa. El mantenimiento de la aplicación se vuelve más fácil cuando hay menos librerías que actualizar, y la seguridad también mejora porque la superficie de ataque potencial de la aplicación se reduce. La seguridad y el mantenimiento de las aplicaciones se discute más a fondo en la parte 7 del curso.
En la práctica, las peticiones se realizan usando la función fetch(). La sintaxis utilizada difiere algo de Axios. También notaremos pronto que Axios se ha encargado de algunas cosas por nosotros y nos ha facilitado la vida. Sin embargo, ahora usaremos la Fetch API, ya que es una solución nativa ampliamente utilizada que todo desarrollador Full Stack debería conocer.
Obteniendo datos del backend
Creemos un método para obtener datos del backend en el archivo src/services/notes.js:
const baseUrl = 'http://localhost:3001/notes'
const getAll = async () => {
const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
const data = await response.json()
return data
}
export default { getAll }Examinemos más de cerca la implementación del método getAll. Las notas ahora se obtienen del backend llamando a la función fetch(), a la cual se le da la URL del backend como argumento. El tipo de petición no se define explícitamente, por lo que fetch realiza su acción predeterminada, que es una petición GET.
Una vez que la respuesta ha llegado, se verifica el éxito de la petición usando la propiedad response.ok, y se lanza un error si es necesario:
if (!response.ok) {
throw new Error('Failed to fetch notes')
}El atributo response.ok se establece en true si la petición fue exitosa, es decir, el código de estado de la respuesta está entre 200 y 299. Para todos los demás códigos de estado, como 404 o 500, se establece en false.
Ten en cuenta que fetch no lanza automáticamente un error incluso si el código de estado de la respuesta es, por ejemplo, 404. El manejo de errores debe implementarse manualmente, como lo hemos hecho aquí.
Si la petición es exitosa, los datos contenidos en la respuesta se convierten a formato JSON:
const data = await response.json()fetch no convierte automáticamente ningún dato incluido en la respuesta a formato JSON; la conversión debe hacerse manualmente. También es importante notar que response.json() es un método asíncrono, por lo que se requiere la palabra clave await.
Simplifiquemos aún más el código devolviendo directamente los datos devueltos por el método response.json():
const getAll = async () => {
const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
return await response.json()}Inicializando el store con datos obtenidos del servidor
Modifiquemos ahora nuestra aplicación para que el estado de la aplicación se inicialice con las notas obtenidas del servidor.
En el archivo noteReducer.js, cambiemos la inicialización del estado de las notas para que por defecto no haya notas:
const noteSlice = createSlice({
name: 'notes',
initialState: [], // ...
})Agreguemos un action creator llamado setNotes, que nos permita reemplazar directamente el array de notas. Podemos crear el action creator deseado usando la función createSlice de la siguiente manera:
// ...
const noteSlice = createSlice({
name: 'notes',
initialState: [],
reducers: {
createNote(state, action) {
const content = action.payload
state.push({
content,
important: false,
id: generateId()
})
},
toggleImportanceOf(state, action) {
const id = action.payload
const noteToChange = state.find(n => n.id === id)
const changedNote = {
...noteToChange,
important: !noteToChange.important
}
return state.map(note => (note.id !== id ? note : changedNote))
},
setNotes(state, action) { return action.payload } }
})
export const { createNote, toggleImportanceOf, setNotes } = noteSlice.actionsexport default noteSlice.reducerImplementemos la inicialización de las notas en el componente App. Como es habitual al obtener datos de un servidor, usaremos el hook useEffect:
import { useEffect } from 'react'import { useDispatch } from 'react-redux'
import NoteForm from './components/NoteForm'
import Notes from './components/Notes'
import VisibilityFilter from './components/VisibilityFilter'
import { setNotes } from './reducers/noteReducer'import noteService from './services/notes'
const App = () => {
const dispatch = useDispatch()
useEffect(() => { noteService.getAll().then(notes => dispatch(setNotes(notes))) }, [dispatch])
return (
<div>
<NoteForm />
<VisibilityFilter />
<Notes />
</div>
)
}
export default AppEnviando datos al backend
A continuación, implementemos la funcionalidad para enviar una nueva nota al servidor. Esto también nos dará una oportunidad de practicar cómo hacer una petición POST usando el método fetch().
Expandamos el código en src/services/notes.js que maneja la comunicación con el servidor de la siguiente manera:
const baseUrl = 'http://localhost:3001/notes'
const getAll = async () => {
const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
return await response.json()
}
const createNew = async (content) => { const response = await fetch(baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, important: false }), }) if (!response.ok) { throw new Error('Failed to create note') } return await response.json()}
export default { getAll, createNew }Examinemos más de cerca la implementación del método createNew. El primer parámetro de la función fetch() especifica la URL a la que se realiza la petición. El segundo parámetro es un objeto que define otros detalles de la petición, como el tipo de petición, encabezados y los datos enviados con la petición. Podemos aclarar aún más el código almacenando el objeto que define los detalles de la petición en una variable options separada:
const createNew = async (content) => {
const options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, important: false }), } const response = await fetch(baseUrl, options)
if (!response.ok) {
throw new Error('Failed to create note')
}
return await response.json()
}Examinemos más de cerca el objeto options:
- method define el tipo de petición, que en este caso es POST
- headers define los encabezados de la petición. Agregamos el encabezado 'Content-Type': 'application/json' para informar al servidor que los datos enviados con la petición están en formato JSON, para que pueda manejar la petición correctamente
- body contiene los datos enviados con la petición. No puedes asignar directamente un objeto JavaScript a este campo; primero debe convertirse a una cadena JSON llamando a la función JSON.stringify()
Al igual que con una petición GET, el código de estado de la respuesta se verifica para detectar errores:
if (!response.ok) {
throw new Error('Failed to create note')
}Si la petición es exitosa, JSON Server devuelve la nota recién creada, para la cual también ha generado un id único. Sin embargo, los datos contenidos en la respuesta aún deben convertirse a formato JSON usando el método response.json():
return await response.json()Luego modificaremos el componente de nuestra aplicación NoteForm para que una nueva nota se envíe al backend. El método addNote del componente cambiará ligeramente:
import { useDispatch } from 'react-redux'
import { createNote } from '../reducers/noteReducer'
import noteService from '../services/notes'
const NoteForm = (props) => {
const dispatch = useDispatch()
const addNote = async (event) => { event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
const newNote = await noteService.createNew(content) dispatch(createNote(newNote)) }
return (
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
)
}
export default NoteFormCuando se crea una nueva nota en el backend llamando al método createNew(), el valor de retorno es un objeto que representa la nota, al cual el backend ha generado un id único. Por lo tanto, modifiquemos el action creator createNote definido en notesReducer.js de la siguiente manera:
const noteSlice = createSlice({
name: 'notes',
initialState: [],
reducers: {
createNote(state, action) {
state.push(action.payload) },
// ..
},
})El cambio de importancia de las notas podría implementarse utilizando el mismo principio, haciendo una llamada asíncrona al servidor y luego enviando una acción apropiada.
El estado actual del código para la aplicación se puede encontrar en GitHub en la rama part6-4.
Acciones asíncronas y Redux Thunk
Nuestro enfoque es bastante bueno, pero no es muy bueno que la comunicación con el servidor suceda dentro de las funciones de los componentes. Sería mejor si la comunicación pudiera abstraerse de los componentes para que no tengan que hacer nada más que llamar al action creator apropiado. Como ejemplo, App inicializaría el estado de la aplicación de la siguiente manera:
const App = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(initializeNotes())
}, [dispatch])
// ...
}y NoteForm crearía una nueva nota de la siguiente manera:
const NoteForm = () => {
const dispatch = useDispatch()
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
dispatch(createNote(content))
}
// ...
}En esta implementación, ambos componentes enviarían una acción sin necesidad de saber sobre la comunicación con el servidor que sucede detrás de escena. Estos tipos de acciones asíncronas se pueden implementar utilizando la librería Redux Thunk. El uso de la librería no requiere ninguna configuración adicional o incluso instalación cuando el store de Redux se ha creado utilizando la función configureStore del kit de herramientas de Redux (Redux Toolkit).
Con Redux Thunk, es posible implementar action creators que devuelven una función en lugar de un objeto. La función recibe los métodos dispatch y getState del store de Redux como parámetros. Esto permite, por ejemplo, implementaciones de action creators asíncronos, que primero esperan la finalización de una cierta operación asíncrona y luego despachan alguna acción, que cambia el estado del store.
Podemos definir un action creator llamado initializeNotes en el archivo noteReducer.js, que obtiene las notas iniciales del servidor, de la siguiente manera:
import { createSlice } from '@reduxjs/toolkit'
import noteService from '../services/notes'
const noteSlice = createSlice({
name: 'notes',
initialState: [],
reducers: {
createNote(state, action) {
state.push(action.payload)
},
toggleImportanceOf(state, action) {
const id = action.payload
const noteToChange = state.find((n) => n.id === id)
const changedNote = {
...noteToChange,
important: !noteToChange.important,
}
return state.map((note) => (note.id !== id ? note : changedNote))
},
setNotes(state, action) {
return action.payload
},
},
})
const { setNotes } = noteSlice.actions
export const initializeNotes = () => { return async (dispatch) => { const notes = await noteService.getAll() dispatch(setNotes(notes)) }}
export const { createNote, toggleImportanceOf } = noteSlice.actions
export default noteSlice.reducerEn su función interna, es decir, en la acción asíncrona, la operación primero obtiene todas las notas del servidor y luego despacha la acción para agregarlas al store. Es importante destacar que Redux pasa automáticamente una referencia al método dispatch como argumento a la función, por lo que el action creator initializeNotes no requiere ningún parámetro.
El action creator setNotes ya no se exporta fuera del módulo, ya que el estado inicial de las notas ahora se establecerá usando el action creator asíncrono initializeNotes que creamos. Sin embargo, todavía usamos el action creator setNotes dentro del módulo.
El componente App ahora puede definirse de la siguiente manera:
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import NoteForm from './components/NoteForm'
import Notes from './components/Notes'
import VisibilityFilter from './components/VisibilityFilter'
import { initializeNotes } from './reducers/noteReducer'
const App = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(initializeNotes()) }, [dispatch])
return (
<div>
<NoteForm />
<VisibilityFilter />
<Notes />
</div>
)
}
export default AppLa solución es bastante elegante. La lógica de inicialización de las notas se ha separado completamente del componente React.
A continuación, creemos un action creator asíncrono llamado appendNote:
import { createSlice } from '@reduxjs/toolkit'
import noteService from '../services/notes'
const noteSlice = createSlice({
name: 'notes',
initialState: [],
reducers: {
createNote(state, action) {
state.push(action.payload)
},
toggleImportanceOf(state, action) {
const id = action.payload
const noteToChange = state.find((n) => n.id === id)
const changedNote = {
...noteToChange,
important: !noteToChange.important,
}
return state.map((note) => (note.id !== id ? note : changedNote))
},
setNotes(state, action) {
return action.payload
},
},
})
const { createNote, setNotes } = noteSlice.actions
export const initializeNotes = () => {
return async (dispatch) => {
const notes = await noteService.getAll()
dispatch(setNotes(notes))
}
}
export const appendNote = (content) => { return async (dispatch) => { const newNote = await noteService.createNew(content) dispatch(createNote(newNote)) }}
export const { toggleImportanceOf } = noteSlice.actions
export default noteSlice.reducerEl principio es el mismo una vez más. Primero se realiza una operación asíncrona y, una vez completada, se despacha una acción que actualiza el estado del store. El action creator createNote ya no se exporta fuera del archivo; solo se usa internamente en la implementación de la función appendNote.
El componente NoteForm cambia de la siguiente manera:
import { useDispatch } from 'react-redux'
import { appendNote } from '../reducers/noteReducer'
const NoteForm = () => {
const dispatch = useDispatch()
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
dispatch(appendNote(content)) }
return (
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
)
}El estado actual del código de la aplicación se puede encontrar en GitHub en la rama part6-5.
Redux Toolkit ofrece una gran cantidad de herramientas para simplificar la administración de estado asíncrono. Las herramientas adecuadas para este caso de uso son, por ejemplo, la función createAsyncThunk y la API RTK Query.



