Aller au contenu

a

Architecture de flux et Redux

Jusqu'à présent, nous avons suivi les conventions de gestion d'état recommandées par React. Nous avons placé l'état et les fonctions pour le gérer dans un niveau supérieur de la structure des composants de l'application. Assez souvent, la majeure partie de l'état de l'application et les fonctions modifiant l'état résident directement dans le composant racine. L'état et ses méthodes de gestion ont ensuite été passés à d'autres composants avec les props. Cela fonctionne jusqu'à un certain point, mais lorsque les applications grandissent, la gestion de l'état devient un défi.

Architecture Flux

Il y a déjà quelques années, Facebook a développé l'architecture Flux pour faciliter la gestion de l'état des applications React. Dans Flux, l'état est séparé des composants React et placé dans ses propres stores. L'état dans le store n'est pas modifié directement, mais avec différentes actions.

Lorsqu'une action change l'état du store, les vues sont rerendues:

diagramme action->dispatcher->store->vue

Si une action dans l'application, par exemple appuyer sur un bouton, nécessite de changer l'état, le changement est réalisé avec une action. Cela provoque à nouveau le rerendu de la vue:

même diagramme que ci-dessus mais avec l'action bouclant en arrière

Flux offre une manière standard de comment et où l'état de l'application est conservé et comment il est modifié.

Redux

Facebook a une implémentation pour Flux, mais nous utiliserons la bibliothèque Redux. Elle fonctionne sur le même principe mais est un peu plus simple. Facebook utilise maintenant également Redux au lieu de leur Flux original.

Nous allons apprendre à connaître Redux en implémentant à nouveau une application de compteur:

application de compteur dans le navigateur

Créez une nouvelle application Vite et installez redux avec la commande

npm install redux

Comme dans Flux, dans Redux, l'état est également stocké dans un store.

L'ensemble de l'état de l'application est stocké dans un objet JavaScript dans le store. Étant donné que notre application n'a besoin que de la valeur du compteur, nous la sauvegarderons directement dans le store. Si l'état était plus compliqué, différentes choses dans l'état seraient sauvegardées comme champs séparés de l'objet.

L'état du store est modifié avec des actions. Les actions sont des objets, qui ont au moins un champ déterminant le type de l'action. Notre application a besoin par exemple de l'action suivante:

{
  type: 'INCREMENT'
}

Si l'action implique des données, d'autres champs peuvent être déclarés selon les besoins. Cependant, notre application de comptage est si simple que les actions sont suffisantes avec juste le champ de type.

L'impact de l'action sur l'état de l'application est défini à l'aide d'un reducer. En pratique, un reducer est une fonction à laquelle sont donnés l'état actuel et une action comme paramètres. Elle retourne un nouvel état.

Définissons maintenant un reducer pour notre application:

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
}

Le premier paramètre est l'état dans le store. Le reducer retourne un nouvel état basé sur le type de l'action. Ainsi, par exemple, lorsque le type de l'Action est INCREMENT, l'état obtient l'ancienne valeur plus un. Si le type de l'Action est ZERO, la nouvelle valeur de l'état est zéro.

Changeons un peu le code. Nous avons utilisé des instructions if-else pour répondre à une action et changer l'état. Cependant, l'instruction switch est l'approche la plus courante pour écrire un reducer.

Définissons également une valeur par défaut de 0 pour le paramètre état. Maintenant, le reducer fonctionne même si l'état du store n'a pas encore été initialisé.

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

Reducer n'est jamais supposé être appelé directement depuis le code de l'application. Reducer est uniquement donné en paramètre à la fonction createStore qui crée le store:

import { createStore } from 'redux'
const counterReducer = (state = 0, action) => {
  // ...
}

const store = createStore(counterReducer)

Le store utilise maintenant le reducer pour gérer les actions, qui sont dispatchées ou 'envoyées' au store avec sa méthode dispatch.

store.dispatch({ type: 'INCREMENT' })

Vous pouvez connaître l'état du store en utilisant la méthode getState.

Par exemple, le code suivant:

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

imprimerait ce qui suit dans la console


0
3
-1

car au début, l'état du store est de 0. Après trois actions INCREMENT, l'état est de 3. À la fin, après les actions ZERO et DECREMENT, l'état est de -1.

La troisième méthode importante que le store possède est subscribe, qui est utilisée pour créer des fonctions de rappel que le store appelle chaque fois qu'une action est dispatchée au store.

Si, par exemple, nous ajoutions la fonction suivante à subscribe, chaque changement dans le store serait imprimé dans la console.

store.subscribe(() => {
  const storeNow = store.getState()
  console.log(storeNow)
})

donc le code

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

provoquerait l'impression suivante


1
2
3
0
-1

Le code de notre application compteur est le suivant. Tout le code a été écrit dans le même fichier (main.jsx), donc le store est directement disponible pour le code React. Nous découvrirons plus tard de meilleures façons de structurer le code 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)

Il y a quelques points notables dans le code. App rend la valeur du compteur en la demandant au store avec la méthode store.getState(). Les gestionnaires d'action des boutons dispatch les bonnes actions au store.

Lorsque l'état dans le store est changé, React n'est pas capable de rerendre automatiquement l'application. Ainsi, nous avons enregistré une fonction renderApp, qui rend toute l'application, pour écouter les changements dans le store avec la méthode store.subscribe. Notez que nous devons immédiatement appeler la méthode renderApp. Sans cet appel, le premier rendu de l'application ne se produirait jamais.

Une note sur l'utilisation de createStore

Les plus observateurs remarqueront que le nom de la fonction createStore est barré. Si vous déplacez la souris sur le nom, une explication apparaîtra

erreur vscode montrant createStore déprécié et recommandant d'utiliser configureStore

L'explication complète est la suivante

Nous recommandons d'utiliser la méthode configureStore du package @reduxjs/toolkit, qui remplace createStore.

Redux Toolkit est notre approche recommandée pour écrire la logique Redux aujourd'hui, y compris la configuration du store, les reducers, la récupération de données et plus encore.

Pour plus de détails, veuillez lire cette page de documentation Redux: https://redux.js.org/introduction/why-rtk-is-redux-today

configureStore de Redux Toolkit est une version améliorée de createStore qui simplifie la configuration et aide à éviter les bugs courants.

Vous ne devriez pas utiliser le package redux core seul aujourd'hui, sauf à des fins d'apprentissage. La méthode createStore du package redux core ne sera pas retirée, mais nous encourageons tous les utilisateurs à migrer vers l'utilisation de Redux Toolkit pour tout le code Redux.

Donc, au lieu de la fonction createStore, il est recommandé d'utiliser la fonction un peu plus "avancée" configureStore, et nous l'utiliserons également lorsque nous aurons atteint la fonctionnalité de base de Redux.

Note à part: createStore est défini comme "déprécié", ce qui signifie généralement que la fonctionnalité sera retirée dans une version plus récente de la bibliothèque. L'explication ci-dessus et la discussion de celle-ci révèlent que createStore ne sera pas retiré, et il a été donné le statut déprécié, peut-être pour des raisons légèrement incorrectes. Donc, la fonction n'est pas obsolète, mais aujourd'hui, il y a une nouvelle manière plus préférable de faire presque la même chose.

Notes sur Redux

Nous visons à modifier notre application de notes pour utiliser Redux pour la gestion de l'état. Cependant, couvrons d'abord quelques concepts clés à travers une application de notes simplifiée.

La première version de notre application est la suivante

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

Jusqu'à présent, l'application n'a pas la fonctionnalité pour ajouter de nouvelles notes, bien qu'il soit possible de le faire en dispatchant des actions NEW_NOTE.

Maintenant, les actions ont un type et un champ payload, qui contient la note à ajouter:

{
  type: 'NEW_NOTE',
  payload: {
    content: 'state changes are made with actions',
    important: false,
    id: 2
  }
}

Le choix du nom du champ n'est pas aléatoire. La convention générale est que les actions ont exactement deux champs, type indiquant le type et payload contenant les données incluses avec l'Action.

Fonctions pures, immuables

La version initiale du reducer est très simple:

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

  return state
}

L'état est maintenant un tableau. Les actions de type NEW_NOTE entraînent l'ajout d'une nouvelle note à l'état avec la méthode push.

L'application semble fonctionner, mais le reducer que nous avons déclaré est mauvais. Il casse l'hypothèse de base de Redux selon laquelle les reducer doivent être des fonctions pures.

Les fonctions pures sont telles qu'elles ne provoquent aucun effet de bord et doivent toujours renvoyer la même réponse lorsqu'elles sont appelées avec les mêmes paramètres.

Nous avons ajouté une nouvelle note à l'état avec la méthode state.push(action.payload) qui change l'état de l'objet état. Cela n'est pas autorisé. Le problème est facilement résolu en utilisant la méthode concat, qui crée un nouveau tableau, contenant tous les éléments de l'ancien tableau et le nouvel élément:

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

  return state
}

Un état de reducer doit être composé d'objets immuables. Si un changement se produit dans l'état, l'ancien objet n'est pas modifié, mais il est remplacé par un nouvel objet modifié. C'est exactement ce que nous avons fait avec le nouveau reducer: l'ancien tableau est remplacé par le nouveau.

Étendons notre reducer pour qu'il puisse gérer le changement d'importance d'une note:

{
  type: 'TOGGLE_IMPORTANCE',
  payload: {
    id: 2
  }
}

Puisque nous n'avons pas encore de code qui utilise cette fonctionnalité, nous étendons le reducer de manière 'test-driven'. Commençons par créer un test pour gérer l'action NEW_NOTE.

Nous devons d'abord configurer la bibliothèque de tests Jest pour le projet. Installons les dépendances suivantes:

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

Ensuite, nous allons créer le fichier .babelrc, avec le contenu suivant:

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

Étendons le fichier package.json avec un script pour exécuter les tests:

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

Et enfin, le fichier .eslintrc.cjs doit être modifié comme suit:

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

Pour faciliter les tests, nous allons d'abord déplacer le code du reducer dans son propre module, dans le fichier src/reducers/noteReducer.js. Nous allons également ajouter la bibliothèque deep-freeze, qui peut être utilisée pour s'assurer que le reducer a été correctement défini comme une fonction immuable. Installons la bibliothèque en tant que dépendance de développement

npm install --save-dev deep-freeze

Le test, que nous définissons dans le fichier src/reducers/noteReducer.test.js, contient le contenu suivant:

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

La commande deepFreeze(state) garantit que le reducer ne modifie pas l'état du store qui lui est donné en paramètre. Si le reducer utilise la commande push pour manipuler l'état, le test ne passera pas

terminal montrant l'échec du test et une erreur concernant l'utilisation de array.push

Maintenant, nous allons créer un test pour l'action 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
  })
})

Donc l'action suivante

{
  type: 'TOGGLE_IMPORTANCE',
  payload: {
    id: 2
  }
}

doit changer l'importance de la note avec l'id 2.

Le reducer est étendu comme suit

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

Nous créons une copie de la note dont l'importance a changé avec la syntaxe familier de la partie 2, et remplaçons l'état par un nouvel état contenant toutes les notes qui n'ont pas changé et la copie de la note modifiée changedNote.

Récapitulons ce qui se passe dans le code. D'abord, nous recherchons un objet note spécifique, dont nous voulons changer l'importance:

const noteToChange = state.find(n => n.id === id)

ensuite, nous créons un nouvel objet, qui est une copie de la note originale, seul la valeur du champ important a été changée pour l'opposé de ce qu'elle était:

const changedNote = { 
  ...noteToChange, 
  important: !noteToChange.important 
}

Un nouvel état est ensuite retourné. Nous le créons en prenant toutes les notes de l'ancien état à l'exception de la note désirée, que nous remplaçons par sa copie légèrement modifiée:

state.map(note =>
  note.id !== id ? note : changedNote 
)

Syntaxe de décomposition des tableaux

Puisque nous avons maintenant de bons tests pour le reducer, nous pouvons refactoriser le code en toute sécurité.

L'ajout d'une nouvelle note crée l'état qu'il retourne avec la fonction concat de Array. Examinons comment nous pouvons obtenir le même résultat en utilisant la syntaxe de décomposition des tableaux de JavaScript:

const noteReducer = (state = [], action) => {
  switch(action.type) {
    case 'NEW_NOTE':
      return [...state, action.payload]    case 'TOGGLE_IMPORTANCE':
      // ...
    default:
    return state
  }
}

La syntaxe de décomposition fonctionne comme suit. Si nous déclarons

const numbers = [1, 2, 3]

...numbers décompose le tableau en éléments individuels, qui peuvent être placés dans un autre tableau.

[...numbers, 4, 5]

et le résultat est un tableau [1, 2, 3, 4, 5].

Si nous avions placé le tableau dans un autre tableau sans la décomposition

[numbers, 4, 5]

le résultat aurait été [ [1, 2, 3], 4, 5].

Lorsque nous prenons des éléments d'un tableau par déstructuration, une syntaxe d'apparence similaire est utilisée pour rassembler le reste des éléments:

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]

Formulaire non contrôlé

Ajoutons la fonctionnalité pour ajouter de nouvelles notes et changer leur importance:

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

L'implémentation des deux fonctionnalités est simple. Il est notable que nous n'avons pas lié l'état des champs du formulaire à l'état du composant App comme nous l'avons précédemment fait. React appelle ce type de formulaire non contrôlé.

Les formulaires non contrôlés ont certaines limitations (par exemple, les messages d'erreur dynamiques ou la désactivation du bouton d'envoi en fonction de l'entrée ne sont pas possibles). Cependant, ils conviennent à nos besoins actuels.

Vous pouvez en savoir plus sur les formulaires non contrôlés ici.

La méthode de gestion pour ajouter de nouvelles notes est simple, elle dispatche juste l'action pour ajouter des notes:

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

Nous pouvons obtenir le contenu de la nouvelle note directement à partir du champ du formulaire. Comme le champ a un nom, nous pouvons accéder au contenu via l'objet événement event.target.note.value.

<form onSubmit={addNote}>
  <input name="note" />  <button type="submit">add</button>
</form>

L'importance d'une note peut être changée en cliquant sur son nom. Le gestionnaire d'événement est très simple:

toggleImportance = (id) => {
  store.dispatch({
    type: 'TOGGLE_IMPORTANCE',
    payload: { id }
  })
}

Créateurs d'actions

Nous commençons à remarquer que, même dans des applications aussi simples que la nôtre, l'utilisation de Redux peut simplifier le code frontend. Cependant, nous pouvons faire beaucoup mieux.

Les composants React n'ont pas besoin de connaître les types d'actions et les formulaires Redux. Séparons la création d'actions en fonctions distinctes:

const createNote = (content) => {
  return {
    type: 'NEW_NOTE',
    payload: {
      content,
      important: false,
      id: generateId()
    }
  }
}

const toggleImportanceOf = (id) => {
  return {
    type: 'TOGGLE_IMPORTANCE',
    payload: { id }
  }
}

Les fonctions qui créent des actions sont appelées créateurs d'actions.

Le composant App n'a plus besoin de connaître quoi que ce soit sur la représentation interne des actions, il obtient simplement la bonne action en appelant la fonction créatrice:

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

  // ...
}

Transmettre le Store Redux à divers composants

À part le reducer, notre application est dans un seul fichier. Cela n'est bien sûr pas sensé, et nous devrions séparer App dans son propre module.

La question est maintenant, comment App peut-il accéder au store après le déménagement? Et plus largement, lorsqu'un composant est composé de nombreux petits composants, il doit y avoir un moyen pour tous les composants d'accéder au store. Il existe plusieurs façons de partager le store Redux avec les composants. D'abord, nous examinerons la manière la plus récente, et peut-être la plus facile, qui est d'utiliser l'API hooks de la bibliothèque react-redux.

D'abord, nous installons react-redux

npm install react-redux

Ensuite, nous déplaçons le composant App dans son propre fichier App.jsx. Voyons comment cela affecte le reste des fichiers de l'application.

main.jsx devient:

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

Notez que l'application est maintenant définie comme un enfant d'un composant Provider fourni par la bibliothèque react-redux. Le store de l'application est donné au Provider en tant qu'attribut store.

La définition des créateurs d'actions a été déplacée dans le fichier reducers/noteReducer.js où le reducer est défini. Ce fichier ressemble à ceci:

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 l'application a de nombreux composants qui ont besoin du store, le composant App doit passer store comme props à tous ces composants.

Le module a maintenant plusieurs commandes export.

La fonction reducer est toujours retournée avec la commande export default, donc le reducer peut être importé de la manière habituelle:

import noteReducer from './reducers/noteReducer'

Un module peut avoir seulement un export par défaut, mais plusieurs exports "normaux"

export const createNote = (content) => {
  // ...
}

export const toggleImportanceOf = (id) => { 
  // ...
}

Les fonctions exportées normalement (pas par défaut) peuvent être importées avec la syntaxe des accolades:

import { createNote } from './../reducers/noteReducer'

Code pour le composant 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

Il y a quelques points à noter dans le code. Auparavant, le code dispatchait des actions en appelant la méthode dispatch du store Redux:

store.dispatch({
  type: 'TOGGLE_IMPORTANCE',
  payload: { id }
})

Maintenant, cela se fait avec la fonction dispatch provenant du hook useDispatch.

import { useSelector, useDispatch } from 'react-redux'
const App = () => {
  const dispatch = useDispatch()  // ...

  const toggleImportance = (id) => {
    dispatch(toggleImportanceOf(id))  }

  // ...
}

Le hook useDispatch donne à n'importe quel composant React l'accès à la fonction dispatch du store Redux défini dans main.jsx. Cela permet à tous les composants de modifier l'état du store Redux.

Le composant peut accéder aux notes stockées dans le store avec le hook useSelector de la bibliothèque react-redux.

import { useSelector, useDispatch } from 'react-redux'
const App = () => {
  // ...
  const notes = useSelector(state => state)  // ...
}

useSelector reçoit une fonction en paramètre. La fonction recherche ou sélectionne des données dans le store Redux. Ici, nous avons besoin de toutes les notes, donc notre fonction sélecteur retourne l'ensemble de l'état:

state => state

which is a shorthand for:

(state) => {
  return state
}

Habituellement, les fonctions sélecteurs sont un peu plus intéressantes et ne retournent que des parties sélectionnées du contenu du store Redux. Nous pourrions par exemple ne retourner que les notes marquées comme importantes:

const importantNotes = useSelector(state => state.filter(note => note.important))  

La version actuelle de l'application peut être trouvée sur GitHub, branche part6-0.

Plus de composants

Séparons la création d'une nouvelle note en un composant.

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

Contrairement au code React que nous avons fait sans Redux, le gestionnaire d'événement pour changer l'état de l'application (qui vit maintenant dans Redux) a été déplacé de App vers un composant enfant. La logique de changement d'état dans Redux est toujours soigneusement séparée de toute la partie React de l'application.

Nous allons également séparer la liste des notes et l'affichage d'une seule note en leurs propres composants (qui seront tous deux placés dans le fichier 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 logique de changement de l'importance d'une note se trouve maintenant dans le composant qui gère la liste des notes.

Il ne reste pas beaucoup de code dans App:

const App = () => {

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

Note, responsable du rendu d'une note unique, est très simple et n'est pas conscient que le gestionnaire d'événement qu'il reçoit en props dispatche une action. Ce type de composants est appelé présentationnel dans la terminologie React.

Notes, d'autre part, est un composant conteneur, car il contient une certaine logique d'application : il définit ce que font les gestionnaires d'événements des composants Note et coordonne la configuration des composants présentationnels, c'est-à-dire, les Notes.

Le code de l'application Redux peut être trouvé sur GitHub, branche part6-1.