Saltar al contenido

a

Flux-architecture y Redux

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:

diagrama action->dispatcher->store->view

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:

mismo diagrama que arriba pero con la acción retrocediendo

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:

aplicación de contador en el navegador

Crea una nueva aplicación Vite e instala redux con el comando

npm install redux

Como 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) => {
  // ...
}

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
-1

porque 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
-1

El 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 React from 'react'
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={e => store.dispatch({ type: 'INCREMENT' })}
      >
        plus
      </button>
      <button
        onClick={e => store.dispatch({ type: 'DECREMENT' })}
      >
        minus
      </button>
      <button 
        onClick={e => 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

mensaje de error de vscode: createStore esta obsoleto, usa configureStore en su lugar

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 es la siguiente

const noteReducer = (state = [], action) => {
  if (action.type === 'NEW_NOTE') {
    state.push(action.payload)
    return state
  }

  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>
  )
}

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) => {
  if (action.type === 'NEW_NOTE') {
    state.push(action.payload)
    return state
  }

  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) => {
  if (action.type === 'NEW_NOTE') {
    return state.concat(action.payload)  }

  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). Comencemos creando una prueba para manejar la acción NEW_NOTE.

Tenemos que configurar primero la biblioteca de pruebas Jest para el proyecto. Vamos a instalar las siguientes dependencias:

npm install --save-dev jest @babel/preset-env @babel/preset-react eslint-plugin-jest

A continuación crearemos el archivo .babelrc, con el siguiente contenido:

{
  "presets": [
    "@babel/preset-env",
    ["@babel/preset-react", { "runtime": "automatic" }]
  ]
}

Expandamos package.json con un script para ejecutar las pruebas:

{
  // ...
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "test": "jest"  },
  // ...
}

Y finalmente, .eslint.cjs necesita ser modificado de la siguiente manera:

module.exports = {
  root: true,
  env: { 
    browser: true,
    es2020: true,
    "jest/globals": true  },
  // ...
}

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. 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-freeze

La prueba, que definimos en el archivo src/reducers/noteReducer.test.js, tiene el siguiente contenido:

import noteReducer from './noteReducer'
import deepFreeze from 'deep-freeze'

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)
  })
})

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á

terminal mostrando test fallando y error acerca de no usar array.push

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)     // imprime 1
console.log(second)   // imprime 2
console.log(rest)     // imprime [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.

<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-redux

A continuación movemos el componente App en su propio archivo App.jsx. Veamos cómo afecta esto al resto de los archivos de la aplicación.

main.jsx se convierte en:

import React from 'react'
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.

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) => {
  // ...
}

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 noteReducer

Si 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 App

Hay 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 => state

que 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 la creación de una nueva nota en su propio componente.

import { useDispatch } from 'react-redux'import { createNote } from '../reducers/noteReducer'
const NewNote = () => {
  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 NewNote

A 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 (que se colocarán en el archivo 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 Notes

La lógica para cambiar la importancia de una nota ahora está en el componente que administra la lista de notas.

No queda mucho código en App:

const App = () => {

  return (
    <div>
      <NewNote />
      <Notes />
    </div>
  )
}

Note, 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.