React: Abordagens básicas para a gestão do estado





Bom dia amigos!



Chamo sua atenção para um aplicativo simples - uma lista de tarefas. O que há de especial nisso, você pergunta. A questão é que tentei implementar o mesmo truque usando quatro abordagens diferentes para gerenciar o estado em aplicativos React: useState, useContext + useReducer, Redux Toolkit e Recoil.



Vamos começar com qual é o estado de um aplicativo e por que escolher a ferramenta certa para trabalhar com ele é tão importante.



Estado é um termo coletivo para qualquer informação relacionada a um aplicativo. Podem ser dados usados ​​no aplicativo, como a mesma lista de tarefas ou lista de usuários, ou estado como tal, como o estado de carregamento ou o estado de um formulário.



Condicionalmente, o estado pode ser dividido em local e global. Um estado local geralmente se refere ao estado de um componente individual, por exemplo, o estado de um formulário, como regra, é o estado local do componente correspondente. Por sua vez, o estado global é mais corretamente denominado distribuído ou compartilhado, o que significa que tal estado é usado por mais de um componente. A condicionalidade da gradação em questão é expressa no fato de que o estado local pode muito bem ser usado por vários componentes (por exemplo, o estado definido usando useState () pode ser passado para componentes filhos como suportes), e o estado global não é necessariamente usado por todos os componentes do aplicativo (por exemplo, no Redux, onde há um armazenamento para o estado de todo o aplicativo, geralmente,uma fatia separada do estado é criada para cada parte da IU, mais precisamente, para a lógica de controle desta parte).



A importância de escolher a ferramenta certa para gerenciar o estado de seu aplicativo decorre dos problemas que surgem quando uma ferramenta não corresponde ao tamanho do aplicativo ou à complexidade da lógica que implementa. Veremos isso à medida que desenvolvermos a lista de tarefas pendentes.



Não entrarei em detalhes do funcionamento de cada ferramenta, mas me limitarei a uma descrição geral e links para materiais relevantes. Para prototipagem de IU, será usado react-bootstrap .



Código no GitHub

Sandbox no CodeSandbox



Crie um projeto usando Create React App:



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

      
      





Instale dependências:



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

      
      





  • bootstrap, react-bootstrap - estilos
  • nanoid - utilitário para gerar um ID único


Em src, crie um diretório "use-state" para a primeira versão do tudushka.



useState ()



Folha de



dicas de ganchos O gancho useState () serve para gerenciar o estado local de um componente. Ele retorna uma matriz com dois elementos: o valor do estado atual e uma função setter para atualizar esse valor. A assinatura deste gancho é:



const [state, setState] = useState(initialValue)

      
      





  • estado - o valor atual do estado
  • setState - setter
  • initialValue - valor inicial ou padrão


Uma das vantagens da desestruturação de array, em oposição à desestruturação de objeto, é a capacidade de usar nomes de variáveis ​​arbitrárias. Por convenção, o nome do setter deve começar com "set" + o nome do primeiro elemento com uma letra maiúscula ([count, setCount], [text, setText], etc.).



Por enquanto, vamos nos restringir a quatro operações básicas: adicionar, trocar (executar), atualizar e excluir uma tarefa, mas vamos complicar nossa vida pelo fato de que nosso estado inicial será na forma de dados normalizados (isso nos permitirá para praticar a atualização imutável corretamente).



Estrutura do projeto:



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

      
      





Acho que tudo está claro aqui.



No App.js, usamos useState () para definir o estado inicial do aplicativo, importar e renderizar os componentes do aplicativo, passando-lhes o estado e o setter como props:



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

      
      





Em TodoForm.js, estamos implementando a adição de uma nova tarefa à 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>
  )
}

      
      





Em TodoList.js, apenas renderizamos a lista de itens:



// 
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, a parte divertida acontece em TodoListItem.js - aqui implementamos as operações restantes: alternar, atualizar e excluir uma tarefa:



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

      
      





Em components / index.js, reexportamos os componentes:



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

      
      





O arquivo scr / index.js tem a seguinte aparência:



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

      
      





Os principais problemas desta abordagem para a gestão do estado:



  • A necessidade de transferir estado e / ou incubadora em cada nível de aninhamento devido à natureza local do estado
  • A lógica para atualizar o estado do aplicativo está espalhada pelos componentes e misturada com a lógica dos próprios componentes
  • Complexidade da renovação do estado decorrente de sua imutabilidade
  • Fluxo de dados unidirecional, a impossibilidade de troca livre de dados entre componentes localizados no mesmo nível de aninhamento, mas em diferentes subárvores do DOM virtual



Os primeiros dois problemas podem ser resolvidos com a combinação useContext () / useReducer ().



useContext () + useReducer ()



O



contexto da folha de dicas do Hooks permite passar valores para componentes filhos diretamente, ignorando seus ancestrais. O gancho useContext () permite que você recupere valores do contexto em qualquer componente empacotado em um provedor.



Criando um contexto:



const TodoContext = createContext()

      
      





Fornecendo contexto com estado para componentes filhos:



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

      
      





Extraindo o valor do estado do contexto em um componente:



const state = useContext(TodoContext)

      
      





O gancho useReducer () aceita um redutor e um estado inicial. Ele retorna o valor do estado atual e uma função para despachar operações com base nas quais o estado é atualizado. A assinatura deste gancho é:



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

      
      





O algoritmo para atualizar o estado se parece com o seguinte: o componente envia a operação para o redutor, e o redutor, com base no tipo de operação (action.type) e a carga útil opcional da operação (action.payload), altera o afirma de uma certa maneira.



A combinação de useContext () e useReducer () resulta na capacidade de passar o estado e o despachante retornado por useReducer () para qualquer componente que seja descendente de um provedor de contexto.



Crie um diretório "redutor de uso" para a segunda versão do truque. Estrutura do projeto:



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

      
      





Vamos começar com a caixa de câmbio. Em actionTypes.js, simplesmente definimos os tipos (nomes, constantes) das operações:



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 }

      
      





Os tipos de operação são definidos em um arquivo separado, uma vez que são usados ​​ao criar objetos de operação e ao escolher um redutor de caso em uma instrução switch. Existe outra abordagem onde os tipos, os criadores da operação e o redutor são colocados no mesmo arquivo. Essa abordagem é chamada de estrutura de arquivo "pato".



Actions.js define os chamados criadores de ação, que retornam objetos de uma determinada forma (para o redutor):



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 }

      
      





O próprio redutor é definido em todoReducer.js. Mais uma vez, o redutor obtém o estado do aplicativo e a operação despachada do componente e, com base no tipo de operação (e carga útil), executa certas ações que resultam na atualização do estado. A atualização do estado é realizada da mesma forma que na versão anterior do truque, exceto que em vez de setState (), o redutor retorna um novo 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 o estado inicial do aplicativo, cria e exporta um provedor de contexto com um valor de estado e um despachante 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)

      
      





Nesse caso, src / index.js se parece com isto:



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

      
      





Agora não precisamos passar o estado e a função para atualizá-lo em cada nível de aninhamento de componente. O componente recupera o estado e o expedidor usando useTodoContext (), por exemplo:



import { useTodoContext } from '../TodoContext'

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

      
      





As operações são despachadas para o redutor usando dispatch (), para o qual o criador da operação é passado, para o qual a carga útil pode ser passada:



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

      
      







Assim, resolvemos os primeiros dois problemas associados ao uso de useState () como uma ferramenta para gerenciar o estado. Na verdade, com a ajuda de uma biblioteca interessante, podemos resolver o terceiro problema - a complexidade de atualizar o estado. O immer permite que você modifique valores imutáveis ​​com segurança (sim, eu sei como isso soa), basta envolver o redutor em uma função "produzir ()". Vamos criar um arquivo "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
  }
})

      
      





A principal limitação que o immer impõe é que devemos alterar o estado diretamente ou retornar um estado que foi atualizado de forma imutável. Você não pode fazer as duas coisas ao mesmo tempo.



Fazemos alterações em todoContext.js:



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

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

      
      





Tudo funciona como antes, mas o código do redutor agora é mais fácil de ler e analisar.



Se movendo.



Redux Toolkit



The Redux Toolkit Guide O



Redux Toolkit é uma coleção de ferramentas que facilitam o trabalho com o Redux. O próprio Redux é muito semelhante ao que implementamos com useContext () + useReducer ():



  • O estado de todo o aplicativo está em uma loja
  • Os componentes filhos são empacotados em um provedor de react-redux , para o qual a loja é passada como uma "loja" prop
  • Os redutores de cada parte do estado são combinados usando combineReducers () em um único redutor raiz, que é passado para createStore () quando o armazenamento é criado.
  • Os componentes são conectados à loja usando connect () (+ mapStateToProps (), mapDispatchToProps ()), etc.


Para implementar as operações básicas, usaremos os seguintes utilitários do Redux Toolkit:



  • configureStore () - para criar e configurar a loja
  • createSlice () - para criar partes do estado
  • createEntityAdapter () - para criar um adaptador de entidade


Um pouco mais tarde, iremos expandir a funcionalidade da lista de tarefas usando os seguintes utilitários:



  • createSelector () - para criar seletores
  • createAsyncThunk () - para criar thunk


Também nos componentes, usaremos os seguintes ganchos do react-redux: "useDispatch ()" - para obter acesso ao dispatcher e "useSelector ()" - para obter acesso aos seletores.



Crie um diretório "redux-toolkit" para a terceira versão do twist. Instale o Redux Toolkit:



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

      
      





Estrutura do projeto:



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

      
      





Vamos começar com o repositório. 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

      
      





Nesse caso, src / index.js se parece com isto:



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

      
      





Passamos para a caixa de câmbio. fatias / 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

      
      





No componente, useDispatch () é usado para acessar o despachante e o criador de atividade importado de todosSlice.js é usado para despachar uma operação específica:



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

//  
const dispatch = useDispatch()

dispatch(addTodo(newTodo))

      
      





Vamos expandir um pouco a funcionalidade do nosso tudushka, a saber: adicionar a capacidade de filtrar tarefas, botões para concluir todas as tarefas e excluir tarefas concluídas, bem como algumas estatísticas úteis. Vamos também implementar a obtenção de uma lista de tarefas do servidor.



Vamos começar com o servidor.



Usaremos o servidor JSON como a "API falsa" . Aqui está uma folha de dicas para trabalhar com isso . Instale json-server e simultaneamente - um utilitário para executar dois ou mais comandos:



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

      
      





Fazemos alterações na seção "scripts" de package.json:



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

      
      





  • -w - significa monitorar mudanças no arquivo "db.json"
  • -p - significa porta, por padrão, as solicitações do aplicativo são enviadas para a porta 3000
  • -d - demora na resposta do servidor


Crie um arquivo "db.json" no diretório raiz do projeto (gerenciamento 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
    }
  ]
}

      
      





Por padrão, todas as solicitações do aplicativo são enviadas para a porta 3000 (a porta na qual o servidor de desenvolvimento está sendo executado). Para que as solicitações sejam enviadas à porta 5000 (a porta na qual o json-server será executado), elas devem ser enviadas por proxy. Adicione a seguinte linha ao package.json:



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

      
      





Iniciamos o servidor usando o comando "servidor yarn".



Criamos outra parte do estado. fatias / 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

      
      





Fazemos alterações em 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

      
      





Fazemos alterações em 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

      
      





Fazemos alterações em src / index.js:



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

store.dispatch(fetchTodos())

      
      





App.js tem esta aparência:



//     
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 outros 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, com o advento do Redux Toolkit, usar Redux para gerenciar o estado do aplicativo tornou-se mais fácil do que usar a combinação useContext () + useReducer () (inacreditável, mas verdadeiro), além do fato de que Redux oferece mais opções para tal gestão. No entanto, o Redux ainda é projetado para aplicativos grandes e complexos com estado. Existe alguma alternativa para gerenciar o estado de aplicativos de pequeno a médio porte além de useContext () / useReducer (). A resposta é sim. Este é o Recoil .



Recuo



Recoil Guide



Recoil é uma nova ferramenta para gerenciar o estado em aplicativos React. O que significa novo? Isso significa que algumas de suas APIs ainda estão em desenvolvimento e podem mudar no futuro. No entanto, as oportunidades que usaremos para criar o tudushka são estáveis.



Átomos e seletores estão no centro do Recoil. O átomo faz parte do estado e o seletor faz parte do estado derivado. Os átomos são criados usando a função "atom ()" e os seletores são criados usando a função "selector ()". Para recuperar valores de átomos e seletores, useRecoilState () (leitura e gravação), useRecoilValue () (somente leitura), useSetRecoilState () (somente gravação) ganchos e outros. Os componentes que usam o estado Recoil devem ser agrupados em RecoilRoot . Parece que o Recoil é intermediário entre useState () e Redux.



Crie um diretório "recoil" para o tudushka mais recente e instale o Recoil:



yarn add recoil
# 
npm i recoil

      
      





Estrutura do projeto:



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

      
      





Esta é a aparência do átomo da lista de tarefas:



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

      
      





Uma das coisas interessantes sobre o Recoil é que podemos misturar lógica síncrona e assíncrona ao criar átomos e seletores. Ele foi projetado de forma que possamos usar o React Suspense para renderizar o conteúdo de fallback antes de receber os dados. Também temos a capacidade de usar um fusível (ErrorBoundary) para detectar erros que ocorrem ao criar átomos e seletores, inclusive de forma assíncrona.



Neste caso, src / index.js se parece com isto:



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

      
      





O átomo do filtro tem a seguinte aparência:



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

      
      





Os componentes extraem valores de átomos e seletores usando os ganchos acima. Por exemplo, o código para o componente "TodoListItem" se parece com este:



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

      
      







Conclusão



Então, você e eu implementamos uma lista de tarefas usando quatro abordagens diferentes para gerenciar o estado. Que conclusões podem ser tiradas de tudo isso?



Vou expressar minha opinião, não pretende ser a verdade última. Claro, escolher a ferramenta de gerenciamento de estado certa depende das tarefas do aplicativo:



  • Para gerenciar o estado local (o estado de um ou dois componentes; presumindo que os dois estejam intimamente relacionados), use useState ()
  • Use Recoil ou useContext () / useReducer () para gerenciar o estado distribuído (o estado de dois ou mais componentes autônomos) ou o estado de aplicativos de pequeno a médio porte.
  • Observe que se você só precisa passar valores para componentes profundamente aninhados, useContext () (useContext () em si não é uma ferramenta para gerenciar estado)
  • Finalmente, para gerenciar o estado global (o estado de todos ou a maioria dos componentes) ou o estado de um aplicativo complexo, use o Redux Toolkit


MobX, sobre o qual ouvi muitas coisas boas, ainda não chegou.



Obrigado pela atenção e tenha um bom dia.



All Articles