Reaccionar: enfoques básicos para la gestión del estado





¡Buen dia amigos!



Llamo su atención sobre una aplicación sencilla: una lista de tareas. ¿Qué tiene de especial ?, preguntas. El punto es que intenté implementar el mismo truco usando cuatro enfoques diferentes para administrar el estado en las aplicaciones React: useState, useContext + useReducer, Redux Toolkit y Recoil.



Comencemos con cuál es el estado de una aplicación y por qué es tan importante elegir la herramienta adecuada para trabajar con ella.



Estado es un término colectivo para cualquier información relacionada con una aplicación. Estos pueden ser datos utilizados en la aplicación, como la misma lista de tareas o lista de usuarios, o el estado como tal, como el estado de carga o el estado de un formulario.



Condicionalmente, el estado se puede dividir en local y global. Un estado local generalmente se refiere al estado de un componente individual, por ejemplo, el estado de una forma, como regla, es el estado local del componente correspondiente. A su vez, el estado global se llama más correctamente distribuido o compartido, lo que significa que dicho estado es utilizado por más de un componente. La condicionalidad de la gradación en cuestión se expresa en el hecho de que el estado local bien puede ser utilizado por varios componentes (por ejemplo, el estado definido usando useState () puede pasarse a componentes secundarios como accesorios), y el estado global no es necesariamente utilizado por todos los componentes de la aplicación (por ejemplo, en Redux donde hay una tienda para el estado de toda la aplicación, por lo general,se crea una porción separada del estado para cada parte de la interfaz de usuario, más precisamente, para la lógica de control de esta parte).



La importancia de elegir la herramienta adecuada para administrar el estado de la aplicación surge de los problemas que surgen cuando una herramienta no coincide con el tamaño de la aplicación o la complejidad de la lógica que implementa. Veremos esto a medida que desarrollemos la lista de tareas pendientes.



No entraré en los detalles del funcionamiento de cada herramienta, sino que me limitaré a una descripción general y enlaces a materiales relevantes. Para la creación de prototipos de UI, se utilizará react-bootstrap .



Código en GitHub

Sandbox en CodeSandbox



Cree un proyecto usando Create React App:



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

      
      





Instalar dependencias:



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

      
      





  • bootstrap, react-bootstrap - estilos
  • nanoid : utilidad para generar una identificación única


En src, cree un directorio "use-state" para la primera versión de tudushka.



useState ()



Hoja de



referencia de Hooks El hook useState () es para administrar el estado local de un componente. Devuelve una matriz con dos elementos: el valor del estado actual y una función de establecimiento para actualizar este valor. La firma de este gancho es:



const [state, setState] = useState(initialValue)

      
      





  • estado - el valor actual del estado
  • setState - setter
  • initialValue - valor inicial o predeterminado


Una de las ventajas de la desestructuración de matrices, a diferencia de la desestructuración de objetos, es la capacidad de utilizar nombres de variables arbitrarios. Por convención, el nombre del colocador debe comenzar con "set" + el nombre del primer elemento con una letra mayúscula ([count, setCount], [text, setText], etc.).



Por ahora, nos limitaremos a cuatro operaciones básicas: agregar, cambiar (ejecutar), actualizar y eliminar una tarea, pero compliquemos nuestra vida por el hecho de que nuestro estado inicial será en forma de datos normalizados (esto nos permitirá practicar correctamente la actualización inmutable).



Estructura del proyecto:



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

      
      





Creo que aquí todo está claro.



En App.js, usamos useState () para definir el estado inicial de la aplicación, importar y renderizar los componentes de la aplicación, pasándoles el estado y el setter como accesorios:



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

      
      





En TodoForm.js, estamos implementando la adición de una nueva tarea a la lista:



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

      
      





En TodoList.js, simplemente representamos la lista de elementos:



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

      
      





Finalmente, la parte divertida ocurre en TodoListItem.js: aquí implementamos las operaciones restantes: cambiar, actualizar y eliminar una tarea:



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

      
      





En components / index.js, reexportamos los componentes:



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

      
      





El archivo scr / index.js se ve así:



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

      
      





Los principales problemas de este enfoque de la gestión estatal:



  • La necesidad de transferir el estado y / o el colocador en cada nivel de anidación debido a la naturaleza local del estado.
  • La lógica para actualizar el estado de la aplicación se distribuye entre los componentes y se mezcla con la lógica de los propios componentes.
  • Complejidad de la renovación estatal derivada de su inmutabilidad.
  • Flujo de datos unidireccional, la imposibilidad de libre intercambio de datos entre componentes ubicados en el mismo nivel de anidamiento, pero en diferentes subárboles del DOM virtual



Los dos primeros problemas se pueden resolver con la combinación useContext () / useReducer ().



useContext () + useReducer ()



El



contexto de la hoja de referencia de Hooks permite pasar valores a los componentes secundarios directamente, sin pasar por sus antepasados. El gancho useContext () le permite recuperar valores del contexto en cualquier componente envuelto en un proveedor.



Creando un contexto:



const TodoContext = createContext()

      
      





Proporcionar contexto con estado a los componentes secundarios:



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

      
      





Extraer el valor de estado del contexto en un componente:



const state = useContext(TodoContext)

      
      





El gancho useReducer () acepta un reductor y un estado inicial. Devuelve el valor del estado actual y una función para despachar operaciones en función de las cuales se actualiza el estado. La firma de este gancho es:



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

      
      





El algoritmo de actualización de estado se ve así: el componente envía la operación al reductor, y el reductor, según el tipo de operación (action.type) y la carga útil de operación opcional (action.payload), cambia los estados de cierta manera.



La combinación de useContext () y useReducer () da como resultado la capacidad de pasar el estado y el despachador devueltos por useReducer () a cualquier componente que sea descendiente de un proveedor de contexto.



Crea el directorio "use-reducer" para la segunda versión del truco. Estructura del proyecto:



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

      
      





Empecemos por la caja de cambios. En actionTypes.js, simplemente definimos los tipos (nombres, constantes) de las operaciones:



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 }

      
      





Los tipos de operación se definen en un archivo separado, ya que se utilizan tanto al crear objetos de operación como al elegir un reductor de casos en una declaración de cambio. Hay otro enfoque donde los tipos, los creadores de la operación y el reductor se colocan en el mismo archivo. Este enfoque se denomina estructura de archivo "pato".



Actions.js define los llamados creadores de acciones, que devuelven objetos de una forma determinada (para el reductor):



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 }

      
      





El reductor en sí está definido en todoReducer.js. Una vez más, el reductor toma el estado de la aplicación y la operación despachada desde el componente y, según el tipo de operación (y la carga útil), realiza ciertas acciones que dan como resultado la actualización del estado. La actualización del estado se realiza de la misma manera que en la versión anterior del truco, excepto que en lugar de setState (), el reductor devuelve un nuevo estado.



//    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 define el estado inicial de la aplicación, crea y exporta un proveedor de contexto con un valor de estado y un despachador 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)

      
      





En este caso, src / index.js se ve así:



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

      
      





Ahora no necesitamos pasar el estado y la función para actualizarlo en cada nivel de anidación de componentes. El componente recupera el estado y el despachador usando useTodoContext (), por ejemplo:



import { useTodoContext } from '../TodoContext'

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

      
      





Las operaciones se envían al reductor mediante dispatch (), al que se le pasa el creador de la operación, al que se le puede pasar la carga útil:



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

//  
dispatch(actions.addTodo(newTodo))

      
      





Código de componente
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>
  )
}

      
      







Por lo tanto, hemos resuelto los dos primeros problemas asociados con el uso de useState () como herramienta para administrar el estado. De hecho, con la ayuda de una biblioteca interesante, podemos resolver el tercer problema: la complejidad de actualizar el estado. immer te permite mutar de forma segura valores inmutables (sí, sé cómo suena eso), simplemente envuelve el reductor en una función "produce ()". Creemos un archivo "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 principal limitación que impone immer es que debemos mutar el estado directamente o devolver un estado que se ha actualizado de manera inmutable. No puedes hacer ambas cosas al mismo tiempo.



Realizamos cambios en todoContext.js:



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

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

      
      





Todo funciona como antes, pero el código reductor ahora es más fácil de leer y analizar.



Hacia adelante.



Kit de herramientas de Redux



La guía del kit de herramientas de Redux El kit de



herramientas de Redux es una colección de herramientas que facilitan el trabajo con Redux. Redux en sí es muy similar a lo que implementamos con useContext () + useReducer ():



  • El estado de toda la aplicación está en una tienda.
  • Los componentes secundarios se envuelven en un proveedor de react-redux , al que se pasa la tienda como un accesorio de "tienda"
  • Los reductores de cada parte del estado se combinan usando combineReducers () en un reductor raíz único, que se pasa a createStore () cuando se crea la tienda.
  • Los componentes se conectan a la tienda mediante connect () (+ mapStateToProps (), mapDispatchToProps ()), etc.


Para implementar las operaciones básicas, usaremos las siguientes utilidades del Redux Toolkit:



  • configureStore (): para crear y configurar la tienda
  • createSlice () - para crear partes del estado
  • createEntityAdapter () - para crear un adaptador de entidad


Un poco más tarde, ampliaremos la funcionalidad de la lista de tareas usando las siguientes utilidades:



  • createSelector () - para crear selectores
  • createAsyncThunk () - para crear procesador


También en los componentes usaremos los siguientes ganchos de react-redux: "useDispatch ()" - para obtener acceso al despachador y "useSelector ()" - para obtener acceso a los selectores.



Cree un directorio "redux-toolkit" para la tercera versión del giro. Instale el kit de herramientas de Redux:



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

      
      





Estructura del proyecto:



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

      
      





Comencemos con el repositorio. 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

      
      





En este caso, src / index.js se ve así:



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

      
      





Pasamos a la caja de cambios. rebanadas / 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

      
      





En el componente, useDispatch () se usa para acceder al despachador, y el creador de actividad importado de todosSlice.js se usa para despachar una operación específica:



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

//  
const dispatch = useDispatch()

dispatch(addTodo(newTodo))

      
      





Ampliemos un poco la funcionalidad de nuestro tudushka, a saber: agreguemos la capacidad de filtrar tareas, botones para completar todas las tareas y eliminar tareas completadas, así como algunas estadísticas útiles. También implementemos la obtención de una lista de tareas del servidor.



Empecemos por el servidor.



Usaremos JSON Server como la "API falsa" . Aquí hay una hoja de trucos para trabajar con él . Instale json-server y, al mismo tiempo , una utilidad para ejecutar dos o más comandos:



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

      
      





Realizamos cambios en la sección "scripts" de package.json:



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

      
      





  • -w - significa monitorear cambios en el archivo "db.json"
  • -p - significa puerto, por defecto las solicitudes de la aplicación se envían al puerto 3000
  • -d - demora la respuesta del servidor


Cree un archivo "db.json" en el directorio raíz del proyecto (gestión de estado):



{
  "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
    }
  ]
}

      
      





De forma predeterminada, todas las solicitudes de la aplicación se envían al puerto 3000 (el puerto en el que se ejecuta el servidor de desarrollo). Para que las solicitudes se envíen al puerto 5000 (el puerto en el que se ejecutará el servidor json), deben tener un proxy. Agregue la siguiente línea a package.json:



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

      
      





Iniciamos el servidor usando el comando "yarn server".



Creamos otra parte del estado. rebanadas / 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

      
      





Realizamos cambios en 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

      
      





Realizamos cambios en 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

      
      





Realizamos cambios en src / index.js:



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

store.dispatch(fetchTodos())

      
      





App.js tiene este aspecto:



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

      
      





Código de otros componentes
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>
  )
}

      
      







Como podemos ver, con la llegada de Redux Toolkit, usar Redux para administrar el estado de la aplicación se ha vuelto más fácil que usar la combinación useContext () + useReducer () (increíble, pero cierto), además del hecho de que Redux proporciona más opciones para tal administración. Sin embargo, Redux todavía está diseñado para aplicaciones con estado grandes y complejas. ¿Existe alguna alternativa para administrar el estado de las aplicaciones pequeñas y medianas que no sea useContext () / useReducer ()? La respuesta es sí. Este es Recoil .



Retroceso



Recoil Guide



Recoil es una nueva herramienta para administrar el estado en las aplicaciones React. ¿Qué significa nuevo? Esto significa que algunas de sus API aún están en desarrollo y pueden cambiar en el futuro. Sin embargo, las oportunidades que usaremos para crear el tudushka son estables.



Los átomos y los selectores están en el corazón de Recoil. El átomo es parte del estado y el selector es parte del estado derivado. Los átomos se crean usando la función "atom ()" y los selectores se crean usando la función "selector ()". Para recuperar valores de átomos y selectores, useRecoilState () (lectura y escritura), useRecoilValue () (solo lectura), useSetRecoilState () (solo escritura) ganchos y otros. Los componentes que usan el estado Recoil deben estar empaquetados en RecoilRoot . Parece que Recoil es intermedio entre useState () y Redux.



Cree un directorio de "retroceso" para el último tudushka e instale Recoil:



yarn add recoil
# 
npm i recoil

      
      





Estructura del proyecto:



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

      
      





Así es como se ve el átomo de la lista de tareas:



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

      
      





Una de las cosas interesantes de Recoil es que podemos mezclar lógica sincrónica y asincrónica al crear átomos y selectores. Está diseñado de tal manera que tenemos la capacidad de usar React Suspense para renderizar contenido de respaldo antes de recibir datos. También tenemos la capacidad de utilizar un fusible (ErrorBoundary) para captar los errores que se producen al crear átomos y selectores, incluso de forma asincrónica.



En este caso, src / index.js se ve así:



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

      
      





El átomo de filtro se ve así:



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

      
      





Los componentes extraen valores de átomos y selectores utilizando los ganchos anteriores. Por ejemplo, el código del componente "TodoListItem" se ve así:



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

      
      





Código de otros componentes
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>
  )
}

      
      







Conclusión



Entonces, usted y yo hemos implementado una lista de tareas utilizando cuatro enfoques diferentes para administrar el estado. ¿Qué conclusiones se pueden sacar de todo esto?



Expresaré mi opinión, no pretende ser la verdad última. Por supuesto, elegir la herramienta de administración de estado adecuada depende de las tareas de la aplicación:



  • Para administrar el estado local (el estado de uno o dos componentes; asumiendo que los dos están estrechamente relacionados) use useState ()
  • Use Recoil o useContext () / useReducer () para administrar el estado distribuido (el estado de dos o más componentes autónomos) o el estado de aplicaciones pequeñas y medianas
  • Tenga en cuenta que si solo necesita pasar valores a componentes profundamente anidados, useContext () está bien (useContext () en sí mismo no es una herramienta para administrar el estado)
  • Finalmente, para administrar el estado global (el estado de todos o la mayoría de los componentes) o el estado de una aplicación compleja, use Redux Toolkit


MobX, del que he escuchado muchas cosas buenas, aún no se ha adaptado.



Gracias por su atención y que tenga un buen día.



All Articles