React: Approches de base de la gestion des états





Bonne journée, mes amis!



J'attire votre attention sur une application simple - une liste de tâches. Quelle est sa particularité, demandez-vous. Le fait est que j'ai essayé d'implémenter la même astuce en utilisant quatre approches différentes pour gérer l'état dans les applications React: useState, useContext + useReducer, Redux Toolkit et Recoil.



Commençons par l'état d'une application et pourquoi il est si important de choisir le bon outil pour l'utiliser.



État est un terme collectif désignant toute information relative à une demande. Il peut s'agir à la fois de données utilisées dans l'application, telles que la même liste de tâches ou liste d'utilisateurs, ou d'un état en tant que tel, tel que l'état de chargement ou l'état d'un formulaire.



Conditionnellement, l'État peut être divisé en local et global. Un état local fait généralement référence à l'état d'un composant individuel, par exemple, l'état d'un formulaire est en règle générale l'état local du composant correspondant. À son tour, l'état global est plus correctement appelé distribué ou partagé, ce qui signifie qu'un tel état est utilisé par plusieurs composants. La conditionnalité de la gradation en question s'exprime dans le fait que l'état local peut très bien être utilisé par plusieurs composants (par exemple, l'état défini à l'aide de useState () peut être passé aux composants enfants comme accessoires), et l'état global n'est pas nécessairement utilisé par tous les composants de l'application (par exemple, dans Redux où il y a un magasin pour l'état de l'application entière, généralement,une tranche distincte de l'état est créée pour chaque partie de l'interface utilisateur, plus précisément pour la logique de commande de cette partie).



L'importance de choisir le bon outil pour gérer l'état de votre application découle des problèmes qui surviennent lorsqu'un outil ne correspond pas à la taille de l'application ou à la complexité de la logique qu'il implémente. Nous verrons cela au fur et à mesure que nous développerons la liste des choses à faire.



Je n'entrerai pas dans les détails du fonctionnement de chaque outil, mais je me limiterai à une description générale et à des liens vers des documents pertinents. Pour le prototypage de l'interface utilisateur, react-bootstrap sera utilisé .



Code sur GitHub

Sandbox sur CodeSandbox



Créez un projet à l'aide de Create React App:



yarn create react-app state-management
# 
npm init react-app state-management
# 
npx create-react-app state-management

      
      





Installer les dépendances:



yarn add bootstrap react-bootstrap nanoid
# 
npm i bootstrap react-bootstrap nanoid

      
      





  • bootstrap, react-bootstrap - styles
  • nanoïde - utilitaire pour générer un identifiant unique


Dans src, créez un répertoire "use-state" pour la première version de tudushka.



useState ()



Hooks Cheat Sheet



Le hook useState () sert à gérer l'état local d'un composant. Il renvoie un tableau avec deux éléments: la valeur de l'état actuel et une fonction de définition pour mettre à jour cette valeur. La signature de ce crochet est:



const [state, setState] = useState(initialValue)

      
      





  • state - la valeur actuelle de l'état
  • setState - setter
  • initialValue - valeur initiale ou par défaut


L'un des avantages de la déstructuration des tableaux, par rapport à la déstructuration des objets, est la possibilité d'utiliser des noms de variables arbitraires. Par convention, le nom du setter doit commencer par "set" + le nom du premier élément avec une majuscule ([count, setCount], [text, setText], etc.).



Pour l'instant, nous nous limiterons à quatre opérations de base: ajouter, basculer (exécuter), mettre à jour et supprimer une tâche, mais compliquons notre vie par le fait que notre état initial sera sous forme de données normalisées (cela nous permettra pour pratiquer correctement la mise à jour immuable).



Structure du projet:



|--use-state
  |--components
    |--index.js
    |--TodoForm.js
    |--TodoList.js
    |--TodoListItem.js
  |--App.js

      
      





Je pense que tout est clair ici.



Dans App.js, nous utilisons useState () pour définir l'état initial de l'application, importer et rendre les composants de l'application, en leur passant l'état et le setter comme accessoires:



// 
import { useState } from 'react'
// 
import { TodoForm, TodoList } from './components'
// 
import { Container } from 'react-bootstrap'

//  
//    ,    
const initialState = {
  todos: {
    ids: ['1', '2', '3', '4'],
    entities: {
      1: {
        id: '1',
        text: 'Eat',
        completed: true
      },
      2: {
        id: '2',
        text: 'Code',
        completed: true
      },
      3: {
        id: '3',
        text: 'Sleep',
        completed: false
      },
      4: {
        id: '4',
        text: 'Repeat',
        completed: false
      }
    }
  }
}

export default function App() {
  const [state, setState] = useState(initialState)

  const { length } = state.todos.ids

  return (
    <Container style={{ maxWidth: '480px' }} className='text-center'>
      <h1 className='mt-2'>useState</h1>
      <TodoForm setState={setState} />
      {length ? <TodoList state={state} setState={setState} /> : null}
    </Container>
  )
}

      
      





Dans TodoForm.js, nous implémentons l'ajout d'une nouvelle tâche à la liste:



// 
import { useState } from 'react'
//    ID
import { nanoid } from 'nanoid'
// 
import { Container, Form, Button } from 'react-bootstrap'

//   
export const TodoForm = ({ setState }) => {
  const [text, setText] = useState('')

  const updateText = ({ target: { value } }) => {
    setText(value)
  }

  const addTodo = (e) => {
    e.preventDefault()

    const trimmed = text.trim()

    if (trimmed) {
      const id = nanoid(5)

      const newTodo = { id, text, completed: false }

      //  ,     
      setState((state) => ({
        ...state,
        todos: {
          ...state.todos,
          ids: state.todos.ids.concat(id),
          entities: {
            ...state.todos.entities,
            [id]: newTodo
          }
        }
      }))

      setText('')
    }
  }

  return (
    <Container className='mt-4'>
      <h4>Form</h4>
      <Form className='d-flex' onSubmit={addTodo}>
        <Form.Control
          type='text'
          placeholder='Enter text...'
          value={text}
          onChange={updateText}
        />
        <Button variant='primary' type='submit'>
          Add
        </Button>
      </Form>
    </Container>
  )
}

      
      





Dans TodoList.js, nous rendons simplement la liste des éléments:



// 
import { TodoListItem } from './TodoListItem'
// 
import { Container, ListGroup } from 'react-bootstrap'

//        ,
//    
//  ,     
export const TodoList = ({ state, setState }) => (
  <Container className='mt-2'>
    <h4>List</h4>
    <ListGroup>
      {state.todos.ids.map((id) => (
        <TodoListItem
          key={id}
          todo={state.todos.entities[id]}
          setState={setState}
        />
      ))}
    </ListGroup>
  </Container>
)

      
      





Enfin, la partie amusante se passe dans TodoListItem.js - nous implémentons ici les opérations restantes: commutation, mise à jour et suppression d'une tâche:



// 
import { ListGroup, Form, Button } from 'react-bootstrap'

//     
export const TodoListItem = ({ todo, setState }) => {
  const { id, text, completed } = todo

  //  
  const toggleTodo = () => {
    setState((state) => {
      //  
      const { todos } = state

      return {
        ...state,
        todos: {
          ...todos,
          entities: {
            ...todos.entities,
            [id]: {
              ...todos.entities[id],
              completed: !todos.entities[id].completed
            }
          }
        }
      }
    })
  }

  //  
  const updateTodo = ({ target: { value } }) => {
    const trimmed = value.trim()

    if (trimmed) {
      setState((state) => {
        const { todos } = state

        return {
          ...state,
          todos: {
            ...todos,
            entities: {
              ...todos.entities,
              [id]: {
                ...todos.entities[id],
                text: trimmed
              }
            }
          }
        }
      })
    }
  }

  //  
  const deleteTodo = () => {
    setState((state) => {
      const { todos } = state

      const newIds = todos.ids.filter((_id) => _id !== id)

      const newTodos = newIds.reduce((obj, id) => {
        if (todos.entities[id]) return { ...obj, [id]: todos.entities[id] }
        else return obj
      }, {})

      return {
        ...state,
        todos: {
          ...todos,
          ids: newIds,
          entities: newTodos
        }
      }
    })
  }

  //      
  const inputStyles = {
    outline: 'none',
    border: 'none',
    background: 'none',
    textAlign: 'center',
    textDecoration: completed ? 'line-through' : '',
    opacity: completed ? '0.8' : '1'
  }

  return (
    <ListGroup.Item className='d-flex align-items-baseline'>
      <Form.Check
        type='checkbox'
        checked={completed}
        onChange={toggleTodo}
      />
      <Form.Control
        style={inputStyles}
        defaultValue={text}
        onChange={updateTodo}
        disabled={completed}
      />
      <Button variant='danger' onClick={deleteTodo}>
        Delete
      </Button>
    </ListGroup.Item>
  )
}

      
      





Dans components / index.js, nous réexportons les composants:



export { TodoForm } from './TodoForm'
export { TodoList } from './TodoList'

      
      





Le fichier scr / index.js ressemble à ceci:



import React from 'react'
import { render } from 'react-dom'

// 
import 'bootstrap/dist/css/bootstrap.min.css'

// 
import App from './use-state/App'

const root$ = document.getElementById('root')
render(<App />, root$)

      
      





Les principaux problèmes de cette approche de la gestion de l'État:



  • La nécessité de transférer l'état et / ou le poseur à chaque niveau d'imbrication en raison de la nature locale de l'état
  • La logique de mise à jour de l'état de l'application est dispersée à travers les composants et mélangée à la logique des composants eux-mêmes
  • Complexité du renouvellement de l'État découlant de son immuabilité
  • Flux de données unidirectionnel, impossibilité d'échange libre de données entre des composants situés au même niveau d'imbrication, mais dans des sous-arbres différents du DOM virtuel



Les deux premiers problèmes peuvent être résolus avec la combinaison useContext () / useReducer ().



useContext () + useReducer ()



Hooks Cheat Sheet



Context permet de transmettre des valeurs directement aux composants enfants, en contournant leurs ancêtres. Le hook useContext () vous permet de récupérer des valeurs du contexte dans n'importe quel composant encapsulé dans un fournisseur.



Créer un contexte:



const TodoContext = createContext()

      
      





Fournir un contexte avec état aux composants enfants:



<TodoContext.Provider value={state}>
  <App />
</TodoContext.Provider>

      
      





Extraction de la valeur d'état du contexte dans un composant:



const state = useContext(TodoContext)

      
      





Le hook useReducer () accepte un réducteur et un état initial. Il renvoie la valeur de l'état actuel et une fonction de répartition des opérations en fonction desquelles l'état est mis à jour. La signature de ce crochet est:



const [state, dispatch] = useReducer(todoReducer, initialState)

      
      





L'algorithme de mise à jour de l'état ressemble à ceci: le composant envoie l'opération au réducteur, et le réducteur, en fonction du type de l'opération (action.type) et de la charge utile optionnelle de l'opération (action.payload), modifie le États d'une certaine manière.



La combinaison de useContext () et useReducer () permet de transmettre l'état et le répartiteur renvoyés par useReducer () à tout composant descendant d'un fournisseur de contexte.



Créez un répertoire "use-reducer" pour la deuxième version de l'astuce. Structure du projet:



|--use-reducer
  |--modules
    |--components
      |--index.js
      |--TodoForm.js
      |--TodoList.js
      |--TodoListItem.js
    |--todoReducer
      |--actions.js
      |--actionTypes.js
      |--todoReducer.js
    |--todoContext.js
  |--App.js

      
      





Commençons par la boîte de vitesses. Dans actionTypes.js, nous définissons simplement les types (noms, constantes) des opérations:



const ADD_TODO = 'ADD_TODO'
const TOGGLE_TODO = 'TOGGLE_TODO'
const UPDATE_TODO = 'UPDATE_TODO'
const DELETE_TODO = 'DELETE_TODO'

export { ADD_TODO, TOGGLE_TODO, UPDATE_TODO, DELETE_TODO }

      
      





Les types d'opération sont définis dans un fichier séparé, car ils sont utilisés à la fois lors de la création d'objets d'opération et lors du choix d'un réducteur de cas dans une instruction switch. Il existe une autre approche où les types, les créateurs de l'opération et le réducteur sont placés dans le même fichier. Cette approche est appelée la structure de fichiers «canard».



Actions.js définit les soi-disant créateurs d'action, qui renvoient des objets d'une certaine forme (pour le réducteur):



import { ADD_TODO, TOGGLE_TODO, UPDATE_TODO, DELETE_TODO } from './actionTypes'

const createAction = (type, payload) => ({ type, payload })

const addTodo = (newTodo) => createAction(ADD_TODO, newTodo)
const toggleTodo = (todoId) => createAction(TOGGLE_TODO, todoId)
const updateTodo = (payload) => createAction(UPDATE_TODO, payload)
const deleteTodo = (todoId) => createAction(DELETE_TODO, todoId)

export { addTodo, toggleTodo, updateTodo, deleteTodo }

      
      





Le réducteur lui-même est défini dans todoReducer.js. Encore une fois, le réducteur prend l'état de l'application et l'opération distribuée à partir du composant et, en fonction du type d'opération (et de la charge utile), effectue certaines actions qui entraînent la mise à jour de l'état. La mise à jour de l'état est effectuée de la même manière que dans la version précédente de l'astuce, sauf qu'au lieu de setState (), le réducteur renvoie un nouvel état.



//    ID
import { nanoid } from 'nanoid'
//  
import * as actions from './actionTypes'

export const todoReducer = (state, action) => {
  const { todos } = state

  switch (action.type) {
    case actions.ADD_TODO: {
      const { payload: newTodo } = action

      const id = nanoid(5)

      return {
        ...state,
        todos: {
          ...todos,
          ids: todos.ids.concat(id),
          entities: {
            ...todos.entities,
            [id]: { id, ...newTodo }
          }
        }
      }
    }

    case actions.TOGGLE_TODO: {
      const { payload: id } = action

      return {
        ...state,
        todos: {
          ...todos,
          entities: {
            ...todos.entities,
            [id]: {
              ...todos.entities[id],
              completed: !todos.entities[id].completed
            }
          }
        }
      }
    }

    case actions.UPDATE_TODO: {
      const { payload: id, text } = action

      return {
        ...state,
        todos: {
          ...todos,
          entities: {
            ...todos.entities,
            [id]: {
              ...todos.entities[id],
              text
            }
          }
        }
      }
    }

    case actions.DELETE_TODO: {
      const { payload: id } = action

      const newIds = todos.ids.filter((_id) => _id !== id)

      const newTodos = newIds.reduce((obj, id) => {
        if (todos.entities[id]) return { ...obj, [id]: todos.entities[id] }
        else return obj
      }, {})

      return {
        ...state,
        todos: {
          ...todos,
          ids: newIds,
          entities: newTodos
        }
      }
    }
    //   (     case)      
    default:
      return state
  }
}

      
      





TodoContext.js définit l'état initial de l'application, crée et exporte un fournisseur de contexte avec une valeur d'état et un répartiteur de useReducer ():



// react
import { createContext, useReducer, useContext } from 'react'
// 
import { todoReducer } from './todoReducer/todoReducer'

//  
const TodoContext = createContext()

//  
const initialState = {
  todos: {
    ids: ['1', '2', '3', '4'],
    entities: {
      1: {
        id: '1',
        text: 'Eat',
        completed: true
      },
      2: {
        id: '2',
        text: 'Code',
        completed: true
      },
      3: {
        id: '3',
        text: 'Sleep',
        completed: false
      },
      4: {
        id: '4',
        text: 'Repeat',
        completed: false
      }
    }
  }
}

// 
export const TodoProvider = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, initialState)

  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  )
}

//      
export const useTodoContext = () => useContext(TodoContext)

      
      





Dans ce cas, src / index.js ressemble à ceci:



// React, ReactDOM  

import { TodoProvider } from './use-reducer/modules/TodoContext'

import App from './use-reducer/App'

const root$ = document.getElementById('root')
render(
  <TodoProvider>
    <App />
  </TodoProvider>,
  root$
)

      
      





Nous n'avons plus besoin de transmettre l'état et la fonction pour le mettre à jour à chaque niveau d'imbrication des composants. Le composant récupère l'état et le répartiteur à l'aide de useTodoContext (), par exemple:



import { useTodoContext } from '../TodoContext'

//  
const { state, dispatch } = useTodoContext()

      
      





Les opérations sont distribuées au réducteur à l'aide de dispatch (), auquel le créateur de l'opération est passé, auquel la charge utile peut être transmise:



import * as actions from '../todoReducer/actions'

//  
dispatch(actions.addTodo(newTodo))

      
      





Code composant
App.js:



// components
import { TodoForm, TodoList } from './modules/components'
// styles
import { Container } from 'react-bootstrap'
// context
import { useTodoContext } from './modules/TodoContext'

export default function App() {
  const { state } = useTodoContext()

  const { length } = state.todos.ids

  return (
    <Container style={{ maxWidth: '480px' }} className='text-center'>
      <h1 className='mt-2'>useReducer</h1>
      <TodoForm />
      {length ? <TodoList /> : null}
    </Container>
  )
}

      
      





TodoForm.js:



// react
import { useState } from 'react'
// styles
import { Container, Form, Button } from 'react-bootstrap'
// context
import { useTodoContext } from '../TodoContext'
// actions
import * as actions from '../todoReducer/actions'

export const TodoForm = () => {
  const { dispatch } = useTodoContext()
  const [text, setText] = useState('')

  const updateText = ({ target: { value } }) => {
    setText(value)
  }

  const handleAddTodo = (e) => {
    e.preventDefault()

    const trimmed = text.trim()

    if (trimmed) {
      const newTodo = { text, completed: false }

      dispatch(actions.addTodo(newTodo))

      setText('')
    }
  }

  return (
    <Container className='mt-4'>
      <h4>Form</h4>
      <Form className='d-flex' onSubmit={handleAddTodo}>
        <Form.Control
          type='text'
          placeholder='Enter text...'
          value={text}
          onChange={updateText}
        />
        <Button variant='primary' type='submit'>
          Add
        </Button>
      </Form>
    </Container>
  )
}

      
      





TodoList.js:



// components
import { TodoListItem } from './TodoListItem'
// styles
import { Container, ListGroup } from 'react-bootstrap'
// context
import { useTodoContext } from '../TodoContext'

export const TodoList = () => {
  const {
    state: { todos }
  } = useTodoContext()

  return (
    <Container className='mt-2'>
      <h4>List</h4>
      <ListGroup>
        {todos.ids.map((id) => (
          <TodoListItem key={id} todo={todos.entities[id]} />
        ))}
      </ListGroup>
    </Container>
  )
}

      
      





TodoListItem.js:



// styles
import { ListGroup, Form, Button } from 'react-bootstrap'
// context
import { useTodoContext } from '../TodoContext'
// actions
import * as actions from '../todoReducer/actions'

export const TodoListItem = ({ todo }) => {
  const { dispatch } = useTodoContext()

  const { id, text, completed } = todo

  const handleUpdateTodo = ({ target: { value } }) => {
    const trimmed = value.trim()

    if (trimmed) {
      dispatch(actions.updateTodo({ id, trimmed }))
    }
  }

  const inputStyles = {
    outline: 'none',
    border: 'none',
    background: 'none',
    textAlign: 'center',
    textDecoration: completed ? 'line-through' : '',
    opacity: completed ? '0.8' : '1'
  }

  return (
    <ListGroup.Item className='d-flex align-items-baseline'>
      <Form.Check
        type='checkbox'
        checked={completed}
        onChange={() => dispatch(actions.toggleTodo(id))}
      />
      <Form.Control
        style={inputStyles}
        defaultValue={text}
        onChange={handleUpdateTodo}
        disabled={completed}
      />
      <Button variant='danger' onClick={() => dispatch(actions.deleteTodo(id))}>
        Delete
      </Button>
    </ListGroup.Item>
  )
}

      
      







Ainsi, nous avons résolu les deux premiers problèmes associés à l'utilisation de useState () comme outil de gestion de l'état. En fait, avec l'aide d'une bibliothèque intéressante, nous pouvons résoudre le troisième problème - la complexité de la mise à jour de l'état. immer vous permet de muter en toute sécurité des valeurs immuables (oui, je sais comment cela sonne), il suffit d'envelopper le réducteur dans une fonction "produire ()". Créons un fichier "todoReducer / todoProducer.js":



// ,  immer
import produce from 'immer'
import { nanoid } from 'nanoid'
//  
import * as actions from './actionTypes'

//   ""  
//     draft -   
export const todoProducer = produce((draft, action) => {
  const {
    todos: { ids, entities }
  } = draft

  switch (action.type) {
    case actions.ADD_TODO: {
      const { payload: newTodo } = action

      const id = nanoid(5)

      ids.push(id)
      entities[id] = { id, ...newTodo }
      break
    }
    case actions.TOGGLE_TODO: {
      const { payload: id } = action

      entities[id].completed = !entities[id].completed
      break
    }
    case actions.UPDATE_TODO: {
      const { payload: id, text } = action

      entities[id].text = text
      break
    }
    case actions.DELETE_TODO: {
      const { payload: id } = action

      ids.splice(ids.indexOf(id), 1)
      delete entities[id]
      break
    }
    default:
      return draft
  }
})

      
      





La principale limitation imposée par immer est que nous devons soit faire muter l'état directement, soit retourner un état qui a été mis à jour de manière immuable. Vous ne pouvez pas faire les deux en même temps.



Nous apportons des modifications à todoContext.js:



// import { todoReducer } from './todoReducer/todoReducer'
import { todoProducer } from './todoReducer/todoProducer'

//  
// const [state, dispatch] = useReducer(todoReducer, initialState)
const [state, dispatch] = useReducer(todoProducer, initialState)

      
      





Tout fonctionne comme avant, mais le code du réducteur est désormais plus facile à lire et à analyser.



Passer à autre chose.



Boîte à outils Redux



Le Guide de la



boîte à outils Redux La boîte à outils Redux est un ensemble d'outils qui facilitent le travail avec Redux. Redux lui-même est très similaire à ce que nous avons implémenté avec useContext () + useReducer ():



  • L'état de l'ensemble de l'application se trouve dans un seul magasin
  • Les composants enfants sont encapsulés dans un fournisseur de react-redux , auquel le magasin est passé en tant que prop "store"
  • Les réducteurs de chaque partie de l'état sont combinés à l'aide de combineReducers () en un seul réducteur racine, qui est passé à createStore () lorsque le magasin est créé.
  • Les composants sont connectés au magasin à l'aide de connect () (+ mapStateToProps (), mapDispatchToProps ()), etc.


Pour implémenter les opérations de base, nous utiliserons les utilitaires suivants de Redux Toolkit:



  • configureStore () - pour créer et configurer le magasin
  • createSlice () - pour créer des parties de l'état
  • createEntityAdapter () - pour créer un adaptateur d'entité


Un peu plus tard, nous étendrons les fonctionnalités de la liste des tâches à l'aide des utilitaires suivants:



  • createSelector () - pour créer des sélecteurs
  • createAsyncThunk () - pour créer un thunk


Aussi dans les composants, nous utiliserons les hooks suivants de react-redux: "useDispatch ()" - pour accéder au répartiteur et "useSelector ()" - pour accéder aux sélecteurs.



Créez un répertoire "redux-toolkit" pour la troisième version du twist. Installez Redux Toolkit:



yarn add @reduxjs/toolkit
# 
npm i @reduxjs/toolkit

      
      





Structure du projet:



|--redux-toolkit
  |--modules
    |--components
      |--index.js
      |--TodoForm.js
      |--TodoList.js
      |--TodoListItem.js
  |--slices
    |--todosSlice.js
  |--App.js
  |--store.js

      
      





Commençons par le référentiel. store.js:



//    
import { configureStore } from '@reduxjs/toolkit'
// 
import todosReducer from './modules/slices/todosSlice'

//  
const preloadedState = {
  todos: {
    ids: ['1', '2', '3', '4'],
    entities: {
      1: {
        id: '1',
        text: 'Eat',
        completed: true
      },
      2: {
        id: '2',
        text: 'Code',
        completed: true
      },
      3: {
        id: '3',
        text: 'Sleep',
        completed: false
      },
      4: {
        id: '4',
        text: 'Repeat',
        completed: false
      }
    }
  }
}

// 
const store = configureStore({
  reducer: {
    todos: todosReducer
  },
  preloadedState
})

export default store

      
      





Dans ce cas, src / index.js ressemble à ceci:



// React, ReactDOM & 

// 
import { Provider } from 'react-redux'

//  
import App from './redux-toolkit/App'
// 
import store from './redux-toolkit/store'

const root$ = document.getElementById('root')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  root$
)

      
      





Nous passons à la boîte de vitesses. slices / todosSlice.js:



//        
import {
  createSlice,
  createEntityAdapter
} from '@reduxjs/toolkit'

//  
const todosAdapter = createEntityAdapter()

//   
//  { ids: [], entities: {} }
const initialState = todosAdapter.getInitialState()

//   
const todosSlice = createSlice({
  //  ,        
  name: 'todos',
  //  
  initialState,
  // 
  reducers: {
    //        { type: 'todos/addTodo', payload: newTodo }
    addTodo: todosAdapter.addOne,
    // Redux Toolkit  immer   
    toggleTodo(state, action) {
      const { payload: id } = action
      const todo = state.entities[id]
      todo.completed = !todo.completed
    },
    updateTodo(state, action) {
      const { id, text } = action.payload
      const todo = state.entities[id]
      todo.text = text
    },
    deleteTodo: todosAdapter.removeOne
  }
})

//      entities   
export const { selectAll: selectAllTodos } = todosAdapter.getSelectors(
  (state) => state.todos
)

//   
export const {
  addTodo,
  toggleTodo,
  updateTodo,
  deleteTodo
} = todosSlice.actions

//  
export default todosSlice.reducer

      
      





Dans le composant, useDispatch () est utilisé pour accéder au répartiteur, et le créateur d'activité importé de todosSlice.js est utilisé pour distribuer une opération spécifique:



import { useDispatch } from 'react-redux'
import { addTodo } from '../slices/todosSlice'

//  
const dispatch = useDispatch()

dispatch(addTodo(newTodo))

      
      





Développons un peu les fonctionnalités de notre tudushka, à savoir: ajouter la possibilité de filtrer les tâches, des boutons pour terminer toutes les tâches et supprimer les tâches terminées, ainsi que des statistiques utiles. Implémentons également l'obtention d'une liste de tâches à partir du serveur.



Commençons par le serveur.



Nous utiliserons JSON Server comme "fausse API" . Voici une feuille de triche pour travailler avec . Installez json-server et simultanément - un utilitaire pour exécuter deux commandes ou plus:



yarn add json-server concurrently
# 
npm i json-server concurrently

      
      





Nous apportons des modifications à la section "scripts" de package.json:



"server": "concurrently \"json-server -w db.json -p 5000 -d 1000\" \"yarn start\""

      
      





  • -w - signifie surveiller les modifications apportées au fichier "db.json"
  • -p - signifie port, par défaut les demandes de l'application sont envoyées au port 3000
  • -d - retarder la réponse du serveur


Créez un fichier "db.json" dans le répertoire racine du projet (state-management):



{
  "todos": [
    {
      "id": "1",
      "text": "Eat",
      "completed": true,
      "visible": true
    },
    {
      "id": "2",
      "text": "Code",
      "completed": true,
      "visible": true
    },
    {
      "id": "3",
      "text": "Sleep",
      "completed": false,
      "visible": true
    },
    {
      "id": "4",
      "text": "Repeat",
      "completed": false,
      "visible": true
    }
  ]
}

      
      





Par défaut, toutes les demandes de l'application sont envoyées au port 3000 (le port sur lequel le serveur de développement est en cours d'exécution). Pour que les requêtes soient envoyées au port 5000 (le port sur lequel le serveur json fonctionnera), elles doivent être envoyées par proxy. Ajoutez la ligne suivante à package.json:



"proxy": "http://localhost:5000"

      
      





Nous démarrons le serveur en utilisant la commande "yarn server".



Nous créons une autre partie de l'État. slices / filterSlice.js:



import { createSlice } from '@reduxjs/toolkit'

// 
export const Filters = {
  All: 'all',
  Active: 'active',
  Completed: 'completed'
}

//   -   
const initialState = {
  status: Filters.All
}

//  
const filterSlice = createSlice({
  name: 'filter',
  initialState,
  reducers: {
    setFilter(state, action) {
      state.status = action.payload
    }
  }
})

export const { setFilter } = filterSlice.actions

export default filterSlice.reducer

      
      





Nous apportons des modifications à store.js:



//     preloadedState
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './modules/slices/todosSlice'
import filterReducer from './modules/slices/filterSlice'

const store = configureStore({
  reducer: {
    todos: todosReducer,
    filter: filterReducer
  }
})

export default store

      
      





Nous apportons des modifications à todosSlice.js:



import {
  createSlice,
  createEntityAdapter,
  //    
  createSelector,
  //    
  createAsyncThunk
} from '@reduxjs/toolkit'
//    HTTP-
import axios from 'axios'

// 
import { Filters } from './filterSlice'

const todosAdapter = createEntityAdapter()

const initialState = todosAdapter.getInitialState({
  //      
  status: 'idle'
})

//  
const SERVER_URL = 'http://localhost:5000/todos'
// 
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  try {
    const response = await axios(SERVER_URL)
    return response.data
  } catch (err) {
    console.error(err.toJSON())
  }
})

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo: todosAdapter.addOne,
    toggleTodo(state, action) {
      const { payload: id } = action
      const todo = state.entities[id]
      todo.completed = !todo.completed
    },
    updateTodo(state, action) {
      const { id, text } = action.payload
      const todo = state.entities[id]
      todo.text = text
    },
    deleteTodo: todosAdapter.removeOne,
    //      
    completeAllTodos(state) {
      Object.values(state.entities).forEach((todo) => {
        todo.completed = true
      })
    },
    //      
    clearCompletedTodos(state) {
      const completedIds = Object.values(state.entities)
        .filter((todo) => todo.completed)
        .map((todo) => todo.id)
      todosAdapter.removeMany(state, completedIds)
    }
  },
  //  
  extraReducers: (builder) => {
    builder
      //       
      //     loading
      //       App.js
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading'
      })
      //     
      //    
      //    
      .addCase(fetchTodos.fulfilled, (state, action) => {
        todosAdapter.setAll(state, action.payload)
        state.status = 'idle'
      })
  }
})

export const { selectAll: selectAllTodos } = todosAdapter.getSelectors(
  (state) => state.todos
)

//         
export const selectFilteredTodos = createSelector(
  selectAllTodos,
  (state) => state.filter,
  (todos, filter) => {
    const { status } = filter
    if (status === Filters.All) return todos
    return status === Filters.Active
      ? todos.filter((todo) => !todo.completed)
      : todos.filter((todo) => todo.completed)
  }
)

export const {
  addTodo,
  toggleTodo,
  updateTodo,
  deleteTodo,
  completeAllTodos,
  clearCompletedTodos
} = todosSlice.actions

export default todosSlice.reducer

      
      





Nous apportons des modifications à src / index.js:



//    "App"
import { fetchTodos } from './redux-toolkit/modules/slices/todosSlice'

store.dispatch(fetchTodos())

      
      





App.js ressemble à ceci:



//     
import { useSelector } from 'react-redux'
//   - 
import Loader from 'react-loader-spinner'
// 
import {
  TodoForm,
  TodoList,
  TodoFilters,
  TodoControls,
  TodoStats
} from './modules/components'
// 
import { Container } from 'react-bootstrap'
//     entitites   
import { selectAllTodos } from './modules/slices/todosSlice'

export default function App() {
  //    
  const { length } = useSelector(selectAllTodos)
  //   
  const loadingStatus = useSelector((state) => state.todos.status)

  //    
  const loaderStyles = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)'
  }

  if (loadingStatus === 'loading')
    return (
      <Loader
        type='Oval'
        color='#00bfff'
        height={80}
        width={80}
        style={loaderStyles}
      />
    )

  return (
    <Container style={{ maxWidth: '480px' }} className='text-center'>
      <h1 className='mt-2'>Redux Toolkit</h1>
      <TodoForm />
      {length ? (
        <>
          <TodoStats />
          <TodoFilters />
          <TodoList />
          <TodoControls />
        </>
      ) : null}
    </Container>
  )
}

      
      





Code des autres composants
TodoControls.js:



// redux
import { useDispatch } from 'react-redux'
// styles
import { Container, ButtonGroup, Button } from 'react-bootstrap'
// action creators
import { completeAllTodos, clearCompletedTodos } from '../slices/todosSlice'

export const TodoControls = () => {
  const dispatch = useDispatch()

  return (
    <Container className='mt-2'>
      <h4>Controls</h4>
      <ButtonGroup>
        <Button
          variant='outline-secondary'
          onClick={() => dispatch(completeAllTodos())}
        >
          Complete all
        </Button>
        <Button
          variant='outline-secondary'
          onClick={() => dispatch(clearCompletedTodos())}
        >
          Clear completed
        </Button>
      </ButtonGroup>
    </Container>
  )
}

      
      





TodoFilters.js:



// redux
import { useDispatch, useSelector } from 'react-redux'
// styles
import { Container, Form } from 'react-bootstrap'
// filters & action creator
import { Filters, setFilter } from '../slices/filterSlice'

export const TodoFilters = () => {
  const dispatch = useDispatch()
  const { status } = useSelector((state) => state.filter)

  const changeFilter = (filter) => {
    dispatch(setFilter(filter))
  }

  return (
    <Container className='mt-2'>
      <h4>Filters</h4>
      {Object.keys(Filters).map((key) => {
        const value = Filters[key]
        const checked = value === status

        return (
          <Form.Check
            key={value}
            inline
            label={value.toUpperCase()}
            type='radio'
            name='filter'
            onChange={() => changeFilter(value)}
            checked={checked}
          />
        )
      })}
    </Container>
  )
}

      
      





TodoForm.js:



// react
import { useState } from 'react'
// redux
import { useDispatch } from 'react-redux'
// libs
import { nanoid } from 'nanoid'
// styles
import { Container, Form, Button } from 'react-bootstrap'
// action creator
import { addTodo } from '../slices/todosSlice'

export const TodoForm = () => {
  const dispatch = useDispatch()
  const [text, setText] = useState('')

  const updateText = ({ target: { value } }) => {
    setText(value)
  }

  const handleAddTodo = (e) => {
    e.preventDefault()

    const trimmed = text.trim()

    if (trimmed) {
      const newTodo = { id: nanoid(5), text, completed: false }

      dispatch(addTodo(newTodo))

      setText('')
    }
  }

  return (
    <Container className='mt-4'>
      <h4>Form</h4>
      <Form className='d-flex' onSubmit={handleAddTodo}>
        <Form.Control
          type='text'
          placeholder='Enter text...'
          value={text}
          onChange={updateText}
        />
        <Button variant='primary' type='submit'>
          Add
        </Button>
      </Form>
    </Container>
  )
}

      
      





TodoList.js:



// redux
import { useSelector } from 'react-redux'
// component
import { TodoListItem } from './TodoListItem'
// styles
import { Container, ListGroup } from 'react-bootstrap'
// selector
import { selectFilteredTodos } from '../slices/todosSlice'

export const TodoList = () => {
  const filteredTodos = useSelector(selectFilteredTodos)

  return (
    <Container className='mt-2'>
      <h4>List</h4>
      <ListGroup>
        {filteredTodos.map((todo) => (
          <TodoListItem key={todo.id} todo={todo} />
        ))}
      </ListGroup>
    </Container>
  )
}

      
      





TodoListItem.js:



// redux
import { useDispatch } from 'react-redux'
// styles
import { ListGroup, Form, Button } from 'react-bootstrap'
// action creators
import { toggleTodo, updateTodo, deleteTodo } from '../slices/todosSlice'

export const TodoListItem = ({ todo }) => {
  const dispatch = useDispatch()

  const { id, text, completed } = todo

  const handleUpdateTodo = ({ target: { value } }) => {
    const trimmed = value.trim()

    if (trimmed) {
      dispatch(updateTodo({ id, trimmed }))
    }
  }

  const inputStyles = {
    outline: 'none',
    border: 'none',
    background: 'none',
    textAlign: 'center',
    textDecoration: completed ? 'line-through' : '',
    opacity: completed ? '0.8' : '1'
  }

  return (
    <ListGroup.Item className='d-flex align-items-baseline'>
      <Form.Check
        type='checkbox'
        checked={completed}
        onChange={() => dispatch(toggleTodo(id))}
      />
      <Form.Control
        style={inputStyles}
        defaultValue={text}
        onChange={handleUpdateTodo}
        disabled={completed}
      />
      <Button variant='danger' onClick={() => dispatch(deleteTodo(id))}>
        Delete
      </Button>
    </ListGroup.Item>
  )
}

      
      





TodoStats.js:



// react
import { useState, useEffect } from 'react'
// redux
import { useSelector } from 'react-redux'
// styles
import { Container, ListGroup } from 'react-bootstrap'
// selector
import { selectAllTodos } from '../slices/todosSlice'

export const TodoStats = () => {
  const allTodos = useSelector(selectAllTodos)

  const [stats, setStats] = useState({
    total: 0,
    active: 0,
    completed: 0,
    percent: 0
  })

  useEffect(() => {
    if (allTodos.length) {
      const total = allTodos.length
      const completed = allTodos.filter((todo) => todo.completed).length
      const active = total - completed
      const percent = total === 0 ? 0 : Math.round((active / total) * 100) + '%'

      setStats({
        total,
        active,
        completed,
        percent
      })
    }
  }, [allTodos])

  return (
    <Container className='mt-2'>
      <h4>Stats</h4>
      <ListGroup horizontal>
        {Object.entries(stats).map(([[first, ...rest], count], index) => (
          <ListGroup.Item key={index}>
            {first.toUpperCase() + rest.join('')}: {count}
          </ListGroup.Item>
        ))}
      </ListGroup>
    </Container>
  )
}

      
      







Comme nous pouvons le voir, avec l'avènement de Redux Toolkit, utiliser Redux pour gérer l'état de l'application est devenu plus facile que d'utiliser la combinaison useContext () + useReducer () (incroyable, mais vrai), outre le fait que Redux fournit plus d'options pour de tels la gestion. Cependant, Redux est toujours conçu pour les applications avec état complexes et volumineuses. Existe-t-il une alternative pour gérer l'état des applications de taille petite à moyenne autre que useContext () / useReducer (). La réponse est oui. C'est Recoil .



Recul



Recoil Guide



Recoil est un nouvel outil de gestion de l'état dans les applications React. Que signifie nouveau? Cela signifie que certaines de ses API sont encore en cours de développement et peuvent changer à l'avenir. Cependant, les opportunités que nous utiliserons pour créer la tudushka sont stables.



Les atomes et les sélecteurs sont au cœur de Recoil. L'atome fait partie de l'état et le sélecteur fait partie de l'état dérivé. Les atomes sont créés en utilisant la fonction "atom ()", et les sélecteurs sont créés en utilisant la fonction "selector ()". Pour récupérer les valeurs des atomes et des sélecteurs, utilisez les hooks useRecoilState () (lecture et écriture), useRecoilValue () (lecture seule), useSetRecoilState () (écriture seule) et autres. Les composants qui utilisent l'état Recoil doivent être encapsulés dans RecoilRoot . On dirait que Recoil est intermédiaire entre useState () et Redux.



Créez un répertoire "recoil" pour la dernière tudushka et installez Recoil:



yarn add recoil
# 
npm i recoil

      
      





Structure du projet:



|--recoil
  |--modules
    |--atoms
      |--filterAtom.js
      |--todosAtom.js
    |--components
      |--index.js
      |--TodoControls.js
      |--TodoFilters.js
      |--TodoForm.js
      |--TodoList.js
      |--TodoListItem.js
      |--TodoStats.js
  |--App.js

      
      





Voici à quoi ressemble l'atome de la liste de tâches:



// todosAtom.js
//      
import { atom, selector } from 'recoil'
//    HTTP-
import axios from 'axios'

//  
const SERVER_URL = 'http://localhost:5000/todos'

//      
export const todosState = atom({
  key: 'todosState',
  default: selector({
    key: 'todosState/default',
    get: async () => {
      try {
        const response = await axios(SERVER_URL)
        return response.data
      } catch (err) {
        console.log(err.toJSON())
      }
    }
  })
})

      
      





L'une des choses intéressantes à propos de Recoil est que nous pouvons mélanger la logique synchrone et asynchrone lors de la création d'atomes et de sélecteurs. Il est conçu de manière à ce que nous ayons la possibilité d'utiliser React Suspense pour restituer le contenu de secours avant de recevoir des données. Nous avons également la possibilité d'utiliser un fusible (ErrorBoundary) pour détecter les erreurs qui se produisent lors de la création d'atomes et de sélecteurs, y compris de manière asynchrone.



Dans ce cas, src / index.js ressemble à ceci:



import React, { Component, Suspense } from 'react'
import { render } from 'react-dom'
// recoil
import { RecoilRoot } from 'recoil'

//  
import Loader from 'react-loader-spinner'

import App from './recoil/App'

//     React
class ErrorBoundary extends Component {
  constructor(props) {
    super(props)
    this.state = { error: null, errorInfo: null }
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo
    })
  }

  render() {
    if (this.state.errorInfo) {
      return (
        <div>
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
        </div>
      )
    }
    return this.props.children
  }
}

const loaderStyles = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)'
}

const root$ = document.getElementById('root')
//        Suspense,   ErrorBoundary
render(
  <RecoilRoot>
    <Suspense
      fallback={
        <Loader
          type='Oval'
          color='#00bfff'
          height={80}
          width={80}
          style={loaderStyles}
        />
      }
    >
      <ErrorBoundary>
        <App />
      </ErrorBoundary>
    </Suspense>
  </RecoilRoot>,
  root$
)

      
      





L'atome de filtre ressemble à ceci:



// filterAtom.js
// recoil
import { atom, selector } from 'recoil'
// 
import { todosState } from './todosAtom'

export const Filters = {
  All: 'all',
  Active: 'active',
  Completed: 'completed'
}

export const todoListFilterState = atom({
  key: 'todoListFilterState',
  default: Filters.All
})

//     :      
export const filteredTodosState = selector({
  key: 'filteredTodosState',
  get: ({ get }) => {
    const filter = get(todoListFilterState)
    const todos = get(todosState)

    if (filter === Filters.All) return todos

    return filter === Filters.Completed
      ? todos.filter((todo) => todo.completed)
      : todos.filter((todo) => !todo.completed)
  }
})

      
      





Les composants extraient les valeurs des atomes et des sélecteurs en utilisant les crochets ci-dessus. Par exemple, le code du composant "TodoListItem" ressemble à ceci:



// 
import { useRecoilState } from 'recoil'
// 
import { ListGroup, Form, Button } from 'react-bootstrap'
// 
import { todosState } from '../atoms/todosAtom'

export const TodoListItem = ({ todo }) => {
  //   -   useState()   Recoil
  const [todos, setTodos] = useRecoilState(todosState)
  const { id, text, completed } = todo

  const toggleTodo = () => {
    const newTodos = todos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )

    setTodos(newTodos)
  }

  const updateTodo = ({ target: { value } }) => {
    const trimmed = value.trim()

    if (!trimmed) return

    const newTodos = todos.map((todo) =>
      todo.id === id ? { ...todo, text: value } : todo
    )

    setTodos(newTodos)
  }

  const deleteTodo = () => {
    const newTodos = todos.filter((todo) => todo.id !== id)

    setTodos(newTodos)
  }

  const inputStyles = {
    outline: 'none',
    border: 'none',
    background: 'none',
    textAlign: 'center',
    textDecoration: completed ? 'line-through' : '',
    opacity: completed ? '0.8' : '1'
  }

  return (
    <ListGroup.Item className='d-flex align-items-baseline'>
      <Form.Check type='checkbox' checked={completed} onChange={toggleTodo} />
      <Form.Control
        style={inputStyles}
        defaultValue={text}
        onChange={updateTodo}
        disabled={completed}
      />
      <Button variant='danger' onClick={deleteTodo}>
        Delete
      </Button>
    </ListGroup.Item>
  )
}

      
      





Code des autres composants
TodoControls.js:



// recoil
import { useRecoilState } from 'recoil'
// styles
import { Container, ButtonGroup, Button } from 'react-bootstrap'
// atom
import { todosState } from '../atoms/todosAtom'

export const TodoControls = () => {
  const [todos, setTodos] = useRecoilState(todosState)

  const completeAllTodos = () => {
    const newTodos = todos.map((todo) => (todo.completed = true))

    setTodos(newTodos)
  }

  const clearCompletedTodos = () => {
    const newTodos = todos.filter((todo) => !todo.completed)

    setTodos(newTodos)
  }

  return (
    <Container className='mt-2'>
      <h4>Controls</h4>
      <ButtonGroup>
        <Button variant='outline-secondary' onClick={completeAllTodos}>
          Complete all
        </Button>
        <Button variant='outline-secondary' onClick={clearCompletedTodos}>
          Clear completed
        </Button>
      </ButtonGroup>
    </Container>
  )
}

      
      





TodoFilters.js:



// recoil
import { useRecoilState } from 'recoil'
// styles
import { Container, Form } from 'react-bootstrap'
// filters & atom
import { Filters, todoListFilterState } from '../atoms/filterAtom'

export const TodoFilters = () => {
  const [filter, setFilter] = useRecoilState(todoListFilterState)

  return (
    <Container className='mt-2'>
      <h4>Filters</h4>
      {Object.keys(Filters).map((key) => {
        const value = Filters[key]
        const checked = value === filter

        return (
          <Form.Check
            key={value}
            inline
            label={value.toUpperCase()}
            type='radio'
            name='filter'
            onChange={() => setFilter(value)}
            checked={checked}
          />
        )
      })}
    </Container>
  )
}

      
      





TodoForm.js:



// react
import { useState } from 'react'
// recoil
import { useSetRecoilState } from 'recoil'
// libs
import { nanoid } from 'nanoid'
// styles
import { Container, Form, Button } from 'react-bootstrap'
// atom
import { todosState } from '../atoms/todosAtom'

export const TodoForm = () => {
  const [text, setText] = useState('')
  const setTodos = useSetRecoilState(todosState)

  const updateText = ({ target: { value } }) => {
    setText(value)
  }

  const addTodo = (e) => {
    e.preventDefault()

    const trimmed = text.trim()

    if (trimmed) {
      const newTodo = { id: nanoid(5), text, completed: false }

      setTodos((oldTodos) => oldTodos.concat(newTodo))

      setText('')
    }
  }

  return (
    <Container className='mt-4'>
      <h4>Form</h4>
      <Form className='d-flex' onSubmit={addTodo}>
        <Form.Control
          type='text'
          placeholder='Enter text...'
          value={text}
          onChange={updateText}
        />
        <Button variant='primary' type='submit'>
          Add
        </Button>
      </Form>
    </Container>
  )
}

      
      





TodoList.js:



// recoil
import { useRecoilValue } from 'recoil'
// components
import { TodoListItem } from './TodoListItem'
// styles
import { Container, ListGroup } from 'react-bootstrap'
// atom
import { filteredTodosState } from '../atoms/filterAtom'

export const TodoList = () => {
  const filteredTodos = useRecoilValue(filteredTodosState)

  return (
    <Container className='mt-2'>
      <h4>List</h4>
      <ListGroup>
        {filteredTodos.map((todo) => (
          <TodoListItem key={todo.id} todo={todo} />
        ))}
      </ListGroup>
    </Container>
  )
}

      
      





TodoStats.js:



// react
import { useState, useEffect } from 'react'
// recoil
import { useRecoilValue } from 'recoil'
// styles
import { Container, ListGroup } from 'react-bootstrap'
// atom
import { todosState } from '../atoms/todosAtom'

export const TodoStats = () => {
  const todos = useRecoilValue(todosState)

  const [stats, setStats] = useState({
    total: 0,
    active: 0,
    completed: 0,
    percent: 0
  })

  useEffect(() => {
    if (todos.length) {
      const total = todos.length
      const completed = todos.filter((todo) => todo.completed).length
      const active = total - completed
      const percent = total === 0 ? 0 : Math.round((active / total) * 100) + '%'

      setStats({
        total,
        active,
        completed,
        percent
      })
    }
  }, [todos])

  return (
    <Container className='mt-2'>
      <h4>Stats</h4>
      <ListGroup horizontal>
        {Object.entries(stats).map(([[first, ...rest], count], index) => (
          <ListGroup.Item key={index}>
            {first.toUpperCase() + rest.join('')}: {count}
          </ListGroup.Item>
        ))}
      </ListGroup>
    </Container>
  )
}

      
      







Conclusion



Ainsi, vous et moi avons implémenté une liste de tâches en utilisant quatre approches différentes pour gérer l'état. Quelles conclusions peut-on tirer de tout cela?



J'exprimerai mon opinion, elle ne prétend pas être la vérité ultime. Bien entendu, le choix du bon outil de gestion d'état dépend des tâches de l'application:



  • Pour gérer l'état local (l'état d'un ou deux composants; en supposant que les deux sont étroitement liés), utilisez useState ()
  • Utilisez Recoil ou useContext () / useReducer () pour gérer l'état distribué (l'état de deux ou plusieurs composants autonomes) ou l'état des applications de petite à moyenne taille.
  • Notez que si vous avez juste besoin de transmettre des valeurs à des composants profondément imbriqués, alors useContext () convient (useContext () lui-même n'est pas un outil de gestion de l'état)
  • Enfin, pour gérer l'état global (l'état de tous ou de la plupart des composants) ou l'état d'une application complexe, utilisez le Redux Toolkit


MobX, dont j'ai entendu beaucoup de bonnes choses, n'a pas encore réussi.



Merci pour votre attention et bonne journée.



All Articles