Reaktion: Grundlegende Ansätze zur Zustandsverwaltung





Guten Tag, Freunde!



Ich mache Sie auf eine einfache Anwendung aufmerksam - eine Liste von Aufgaben. Was ist das Besondere daran? Der Punkt ist, dass ich versucht habe, denselben Trick mit vier verschiedenen Ansätzen zum Verwalten des Status in React-Anwendungen zu implementieren: useState, useContext + useReducer, Redux Toolkit und Recoil.



Beginnen wir mit dem Status einer Anwendung und warum es so wichtig ist, das richtige Tool für die Arbeit mit ihr auszuwählen.



Staat ist ein Sammelbegriff für alle Informationen, die sich auf einen Antrag beziehen. Dies können sowohl Daten sein, die in der Anwendung verwendet werden, z. B. dieselbe Aufgabenliste oder Benutzerliste, als auch der Status als solcher, z. B. der Ladezustand oder der Status eines Formulars.



Bedingt kann der Staat in lokal und global unterteilt werden. Ein lokaler Zustand bezieht sich normalerweise auf den Zustand einer einzelnen Komponente, beispielsweise ist der Zustand eines Formulars in der Regel der lokale Zustand der entsprechenden Komponente. Der globale Status wird wiederum korrekter als verteilt oder gemeinsam genutzt bezeichnet, was bedeutet, dass ein solcher Status von mehr als einer Komponente verwendet wird. Die Konditionalität der fraglichen Abstufung drückt sich darin aus, dass der lokale Status durchaus von mehreren Komponenten verwendet werden kann (z. B. kann der mit useState () definierte Status als Requisiten an untergeordnete Komponenten übergeben werden) und der globale Status nicht Wird notwendigerweise von allen Anwendungskomponenten verwendet (z. B. in Redux, wo normalerweise ein Speicher für den Status der gesamten Anwendung vorhanden ist).Für jeden Teil der Benutzeroberfläche wird ein separater Teil des Status erstellt, genauer gesagt für die Steuerlogik dieses Teils.



Die Wichtigkeit der Auswahl des richtigen Tools für die Verwaltung des Status Ihrer Anwendung ergibt sich aus den Problemen, die auftreten, wenn ein Tool nicht der Größe der Anwendung oder der Komplexität der implementierten Logik entspricht. Wir werden dies sehen, wenn wir die To-Do-Liste entwickeln.



Ich werde nicht auf die Details der Bedienung der einzelnen Werkzeuge eingehen, sondern mich auf eine allgemeine Beschreibung und Links zu relevanten Materialien beschränken. Für das UI-Prototyping wird React-Bootstrap verwendet .



Code auf GitHub

Sandbox auf CodeSandbox



Erstellen Sie ein Projekt mit der Create React App:



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

      
      





Abhängigkeiten installieren:



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

      
      





  • Bootstrap, React-Bootstrap - Stile
  • Nanoid - Dienstprogramm zum Generieren einer eindeutigen ID


Erstellen Sie in src ein "use-state" -Verzeichnis für die erste Version des Tudushka.



useState ()



Hooks Cheat Sheet



Der useState () - Hook dient zum Verwalten des lokalen Status einer Komponente. Es gibt ein Array mit zwei Elementen zurück: dem aktuellen Statuswert und einer Setterfunktion zum Aktualisieren dieses Werts. Die Signatur dieses Hakens lautet:



const [state, setState] = useState(initialValue)

      
      





  • state - der aktuelle Wert des Status
  • setState - Setter
  • initialValue - Anfangs- oder Standardwert


Einer der Vorteile der Array-Destrukturierung im Gegensatz zur Objekt-Destrukturierung ist die Möglichkeit, beliebige Variablennamen zu verwenden. Konventionell muss der Name des Setters mit "set" + dem Namen des ersten Elements mit einem Großbuchstaben ([count, setCount], [text, setText] usw.) beginnen.



Im Moment beschränken wir uns auf vier grundlegende Operationen: Hinzufügen, Wechseln (Ausführen), Aktualisieren und Löschen einer Aufgabe, aber erschweren wir unser Leben durch die Tatsache, dass unser Ausgangszustand in Form normalisierter Daten vorliegt (dies ermöglicht uns dies) unveränderliche Aktualisierung richtig zu üben).



Projektstruktur:



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

      
      





Ich denke hier ist alles klar.



In App.js verwenden wir useState (), um den Anfangszustand der Anwendung zu definieren, Anwendungskomponenten zu importieren und zu rendern und ihnen den Status und den Setter als Requisiten zu übergeben:



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

      
      





In TodoForm.js implementieren wir das Hinzufügen einer neuen Aufgabe zur 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>
  )
}

      
      





In TodoList.js rendern wir nur die Liste der Elemente:



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

      
      





Schließlich passiert der lustige Teil in TodoListItem.js - hier implementieren wir die verbleibenden Operationen: Umschalten, Aktualisieren und Löschen einer Aufgabe:



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

      
      





In components / index.js exportieren wir die Komponenten erneut:



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

      
      





Die Datei scr / index.js sieht folgendermaßen aus:



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

      
      





Die Hauptprobleme dieses Ansatzes zur staatlichen Verwaltung:



  • Die Notwendigkeit, den Status und / oder den Setter auf jeder Verschachtelungsebene aufgrund der lokalen Natur des Status zu übertragen
  • Die Logik zum Aktualisieren des Status der Anwendung ist über die Komponenten verteilt und mit der Logik der Komponenten selbst gemischt
  • Komplexität der staatlichen Erneuerung aufgrund ihrer Unveränderlichkeit
  • Unidirektionaler Datenfluss, die Unmöglichkeit des freien Datenaustauschs zwischen Komponenten, die sich auf derselben Verschachtelungsebene befinden, jedoch in verschiedenen Teilbäumen des virtuellen DOM



Die ersten beiden Probleme können mit der Kombination useContext () / useReducer () gelöst werden.



useContext () + useReducer ()



Hooks Cheat Sheet



Context ermöglicht die direkte Übergabe von Werten an untergeordnete Komponenten unter Umgehung ihrer Vorfahren. Mit dem useContext () -Hook können Sie Werte aus dem Kontext in jeder Komponente abrufen, die in einen Anbieter eingeschlossen ist.



Kontext erstellen:



const TodoContext = createContext()

      
      





Bereitstellen eines zustandsbehafteten Kontexts für untergeordnete Komponenten:



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

      
      





Extrahieren des Statuswerts aus dem Kontext in einer Komponente:



const state = useContext(TodoContext)

      
      





Der useReducer () - Hook akzeptiert einen Reduzierer und einen Anfangszustand. Es gibt den Wert des aktuellen Status und eine Funktion zum Versenden von Vorgängen zurück, auf deren Grundlage der Status aktualisiert wird. Die Signatur dieses Hakens lautet:



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

      
      





Der Algorithmus zum Aktualisieren des Status sieht folgendermaßen aus: Die Komponente sendet die Operation an den Reduzierer, und der Reduzierer ändert basierend auf dem Operationstyp (action.type) und der optionalen Nutzlast der Operation (action.payload) die Staaten in gewisser Weise.



Die Kombination von useContext () und useReducer () führt dazu, dass der von useReducer () zurückgegebene Status und Dispatcher an jede Komponente übergeben werden kann, die ein Nachkomme eines Kontextanbieters ist.



Erstellen Sie ein "use-reducer" -Verzeichnis für die zweite Version des Tricks. Projektstruktur:



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

      
      





Beginnen wir mit dem Getriebe. In actionTypes.js definieren wir einfach die Typen (Namen, Konstanten) der Operationen:



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 }

      
      





Operationstypen werden in einer separaten Datei definiert, da sie sowohl beim Erstellen von Operationsobjekten als auch bei der Auswahl eines Fallreduzierers in einer switch-Anweisung verwendet werden. Es gibt einen anderen Ansatz, bei dem die Typen, die Ersteller der Operation und der Reduzierer in derselben Datei abgelegt werden. Dieser Ansatz wird als "Enten" -Dateistruktur bezeichnet.



Actions.js definiert die sogenannten Aktionsersteller, die Objekte einer bestimmten Form (für den Reduzierer) zurückgeben:



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 }

      
      





Der Reduzierer selbst ist in todoReducer.js definiert. Der Reduzierer übernimmt erneut den Anwendungsstatus und die von der Komponente ausgelöste Operation und führt basierend auf dem Operationstyp (und der Nutzlast) bestimmte Aktionen aus, die dazu führen, dass der Status aktualisiert wird. Das Aktualisieren des Status erfolgt auf die gleiche Weise wie in der vorherigen Version des Tricks, außer dass der Reduzierer anstelle von setState () einen neuen Status zurückgibt.



//    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 definiert den Anfangszustand der Anwendung, erstellt und exportiert einen Kontextanbieter mit einem Statuswert und einem Dispatcher von 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)

      
      





In diesem Fall sieht src / index.js folgendermaßen aus:



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

      
      





Jetzt müssen wir den Status und die Funktion nicht mehr übergeben, um sie auf jeder Ebene der Komponentenverschachtelung zu aktualisieren. Die Komponente ruft den Status und den Dispatcher mit useTodoContext () ab, zum Beispiel:



import { useTodoContext } from '../TodoContext'

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

      
      





Operationen werden mit dispatch () an den Reduzierer gesendet, an den der Ersteller der Operation übergeben wird, an den die Nutzlast übergeben werden kann:



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

//  
dispatch(actions.addTodo(newTodo))

      
      





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

      
      







Damit haben wir die ersten beiden Probleme gelöst, die mit der Verwendung von useState () als Tool zum Verwalten des Status verbunden sind. Tatsächlich können wir mit Hilfe einer interessanten Bibliothek das dritte Problem lösen - die Komplexität der Aktualisierung des Zustands. Immer ermöglicht es Ihnen, unveränderliche Werte sicher zu mutieren (ja, ich weiß, wie das klingt), wickeln Sie den Reduzierer einfach in eine "produzieren ()" - Funktion. Erstellen wir eine Datei "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
  }
})

      
      





Die Hauptbeschränkung, die immer auferlegt wird, besteht darin, dass wir entweder den Zustand direkt mutieren oder einen Zustand zurückgeben müssen, der unveränderlich aktualisiert wurde. Sie können nicht beide gleichzeitig ausführen.



Wir nehmen Änderungen an todoContext.js vor:



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

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

      
      





Alles funktioniert wie zuvor, aber der Reduzierungscode ist jetzt einfacher zu lesen und zu analysieren.



Weitermachen.



Redux Toolkit



Das Redux Toolkit-Handbuch Das



Redux Toolkit ist eine Sammlung von Tools, mit denen Sie problemlos mit Redux arbeiten können. Redux selbst ist sehr ähnlich zu dem, was wir mit useContext () + useReducer () implementiert haben:



  • Der Status der gesamten Anwendung befindet sich in einem Geschäft
  • Untergeordnete Komponenten werden in einen Provider von React-Redux eingeschlossen , an den der Store als "Store" -Stütze übergeben wird
  • Die Reduzierer jedes Teils des Status werden mithilfe von combinReducers () zu einem einzigen Root-Reduzierer kombiniert, der beim Erstellen des Speichers an createStore () übergeben wird.
  • Komponenten werden über connect () (+ mapStateToProps (), mapDispatchToProps ()) usw. mit dem Geschäft verbunden.


Um die grundlegenden Operationen zu implementieren, verwenden wir die folgenden Dienstprogramme aus dem Redux Toolkit:



  • configureStore () - zum Erstellen und Konfigurieren des Stores
  • createSlice () - um Teile des Status zu erstellen
  • createEntityAdapter () - um einen Entitätsadapter zu erstellen


Wenig später werden wir die Funktionalität der Aufgabenliste mit den folgenden Dienstprogrammen erweitern:



  • createSelector () - zum Erstellen von Selektoren
  • createAsyncThunk () - um Thunk zu erstellen


Auch in den Komponenten werden wir die folgenden Hooks von react-redux verwenden: "useDispatch ()" - um Zugriff auf den Dispatcher zu erhalten und "useSelector ()" - um Zugriff auf die Selektoren zu erhalten.



Erstellen Sie ein Verzeichnis "Redux-Toolkit" für die dritte Version des Twists. Installieren Sie das Redux Toolkit:



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

      
      





Projektstruktur:



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

      
      





Beginnen wir mit dem Repository. 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

      
      





In diesem Fall sieht src / index.js folgendermaßen aus:



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

      
      





Wir gehen zum Getriebe. 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

      
      





In der Komponente wird useDispatch () verwendet, um auf den Dispatcher zuzugreifen, und der aus todosSlice.js importierte Aktivitätsersteller wird verwendet, um eine bestimmte Operation zu versenden:



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

//  
const dispatch = useDispatch()

dispatch(addTodo(newTodo))

      
      





Lassen Sie uns die Funktionalität unseres Tudushka ein wenig erweitern, nämlich: Fügen Sie die Möglichkeit hinzu, Aufgaben zu filtern, Schaltflächen, um alle Aufgaben zu erledigen und erledigte Aufgaben zu löschen, sowie einige nützliche Statistiken. Lassen Sie uns auch implementieren, wie eine Liste von Aufgaben vom Server abgerufen wird.



Beginnen wir mit dem Server.



Wir werden JSON Server als "gefälschte API" verwenden . Hier ist ein Spickzettel für die Arbeit damit . Installieren Sie json-server und gleichzeitig - ein Dienstprogramm zum Ausführen von zwei oder mehr Befehlen:



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

      
      





Wir nehmen Änderungen am Abschnitt "Skripte" von package.json vor:



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

      
      





  • -w - bedeutet, dass Änderungen an der Datei "db.json" überwacht werden
  • -p - bedeutet Port. Standardmäßig werden Anforderungen von der Anwendung an Port 3000 gesendet
  • -d - Antwort vom Server verzögern


Erstellen Sie eine Datei "db.json" im Stammverzeichnis des Projekts (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
    }
  ]
}

      
      





Standardmäßig werden alle Anforderungen von der Anwendung an Port 3000 (den Port, auf dem der Entwicklungsserver ausgeführt wird) gesendet. Damit Anforderungen an Port 5000 (den Port, auf dem der JSON-Server ausgeführt wird) gesendet werden können, müssen sie per Proxy übertragen werden. Fügen Sie package.json die folgende Zeile hinzu:



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

      
      





Wir starten den Server mit dem Befehl "Garnserver".



Wir schaffen einen anderen Teil des Staates. 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

      
      





Wir nehmen Änderungen an store.js vor:



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

      
      





Wir nehmen Änderungen an todosSlice.js vor:



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

      
      





Wir nehmen Änderungen an src / index.js vor:



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

store.dispatch(fetchTodos())

      
      





App.js sieht folgendermaßen aus:



//     
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 für andere Komponenten
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>
  )
}

      
      







Wie wir sehen können, ist die Verwendung von Redux zum Verwalten des Anwendungsstatus mit dem Aufkommen des Redux-Toolkits einfacher geworden als die Verwendung der Kombination useContext () + useReducer () (unglaublich, aber wahr), abgesehen von der Tatsache, dass Redux mehr Optionen für solche bietet Management. Redux ist jedoch weiterhin für große, komplexe Stateful-Anwendungen konzipiert. Gibt es eine andere Alternative zum Verwalten des Status kleiner bis mittlerer Anwendungen als useContext () / useReducer ()? Die Antwort lautet: Ja. Das ist Rückstoß .



Rückstoß



Recoil Guide



Recoil ist ein neues Tool zum Verwalten des Status in React-Anwendungen. Was bedeutet neu? Dies bedeutet, dass sich einige seiner APIs noch in der Entwicklung befinden und sich in Zukunft möglicherweise ändern werden. Die Möglichkeiten, die wir zur Herstellung der Tudushka nutzen werden, sind jedoch stabil.



Atome und Selektoren sind das Herzstück von Recoil. Das Atom ist Teil des Zustands, und der Selektor ist Teil des abgeleiteten Zustands. Atome werden mit der Funktion "atom ()" erstellt, und Selektoren werden mit der Funktion "selector ()" erstellt. Verwenden Sie zum Abrufen von Werten von Atomen und SelektorenRecoilState () (lesen und schreiben), useRecoilValue () (schreibgeschützt), useSetRecoilState () (schreibgeschützt) und andere. Komponenten, die den Recoil-Status verwenden, müssen in RecoilRoot eingeschlossen werden . Es fühlt sich an, als würde Recoil zwischen useState () und Redux liegen.



Erstellen Sie ein "Rückstoß" -Verzeichnis für die neueste Tudushka und installieren Sie Recoil:



yarn add recoil
# 
npm i recoil

      
      





Projektstruktur:



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

      
      





So sieht das Atom der Aufgabenliste aus:



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

      
      





Eines der interessanten Dinge an Recoil ist, dass wir beim Erstellen von Atomen und Selektoren synchrone und asynchrone Logik mischen können. Es ist so konzipiert, dass wir React Suspense verwenden können, um Fallback-Inhalte vor dem Empfang von Daten zu rendern. Wir haben auch die Möglichkeit, eine Sicherung (ErrorBoundary) zu verwenden, um Fehler abzufangen, die beim Erstellen von Atomen und Selektoren auftreten, auch auf asynchrone Weise.



In diesem Fall sieht src / index.js folgendermaßen aus:



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

      
      





Das Filteratom sieht folgendermaßen aus:



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

      
      





Komponenten extrahieren Werte aus Atomen und Selektoren mit den obigen Hooks. Der Code für die Komponente "TodoListItem" sieht beispielsweise folgendermaßen aus:



// 
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 für andere Komponenten
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>
  )
}

      
      







Fazit



Sie und ich haben also eine Liste von Aufgaben implementiert, die vier verschiedene Ansätze zur Verwaltung des Zustands verwenden. Welche Schlussfolgerungen können daraus gezogen werden?



Ich werde meine Meinung äußern, es behauptet nicht, die ultimative Wahrheit zu sein. Die Auswahl des richtigen Statusverwaltungstools hängt natürlich von den Aufgaben der Anwendung ab:



  • Verwenden Sie useState (), um den lokalen Status (den Status einer oder zweier Komponenten; vorausgesetzt, die beiden sind eng miteinander verbunden) zu verwalten.
  • Verwenden Sie Recoil oder useContext () / useReducer (), um den verteilten Status (den Status von zwei oder mehr autonomen Komponenten) oder den Status kleiner bis mittlerer Anwendungen zu verwalten.
  • Beachten Sie, dass useContext () in Ordnung ist, wenn Sie nur Werte an tief verschachtelte Komponenten übergeben müssen (useContext () selbst ist kein Tool zum Verwalten des Status).
  • Verwenden Sie das Redux Toolkit, um den globalen Status (den Status aller oder der meisten Komponenten) oder den Status einer komplexen Anwendung zu verwalten


MobX, von dem ich viele gute Dinge gehört habe, ist noch nicht dazu gekommen.



Vielen Dank für Ihre Aufmerksamkeit und einen schönen Tag.



All Articles