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:
TodoForm.js:
TodoList.js:
TodoListItem.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:
TodoFilters.js:
TodoForm.js:
TodoList.js:
TodoListItem.js:
TodoStats.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:
TodoFilters.js:
TodoForm.js:
TodoList.js:
TodoStats.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.