Bereaksi: Pendekatan Dasar untuk Manajemen Negara





Selamat siang teman!



Saya beri perhatian Anda aplikasi sederhana - daftar tugas. Apa yang istimewa tentang itu, Anda bertanya. Intinya adalah saya mencoba menerapkan trik yang sama menggunakan empat pendekatan berbeda untuk mengelola status di aplikasi React: useState, useContext + useReducer, Redux Toolkit, dan Recoil.



Mari kita mulai dengan apa status aplikasi dan mengapa memilih alat yang tepat untuk bekerja dengannya sangat penting.



Negara adalah istilah kolektif untuk informasi apa pun yang terkait dengan aplikasi. Ini bisa berupa data yang digunakan dalam aplikasi, seperti daftar tugas atau daftar pengguna yang sama, atau status seperti itu, seperti status pemuatan atau status formulir.



Secara kondisional, negara dapat dibagi menjadi lokal dan global. Status lokal biasanya mengacu pada status komponen individu, misalnya, status formulir, sebagai aturan, adalah status lokal dari komponen terkait. Pada gilirannya, status global lebih tepat disebut terdistribusi atau dibagi, yang berarti bahwa status seperti itu digunakan oleh lebih dari satu komponen. Kondisionalitas gradasi yang dimaksud diekspresikan dalam fakta bahwa status lokal dapat digunakan oleh beberapa komponen (misalnya, status yang ditentukan menggunakan useState () dapat diteruskan ke komponen turunan sebagai properti), dan status global tidak harus digunakan oleh semua komponen aplikasi (misalnya, di Redux di mana terdapat satu penyimpanan untuk status seluruh aplikasi, biasanya,potongan status yang terpisah dibuat untuk setiap bagian UI, lebih tepatnya, untuk logika kontrol bagian ini).



Pentingnya memilih alat yang tepat untuk mengelola keadaan aplikasi Anda berasal dari masalah yang muncul saat alat tidak cocok dengan ukuran aplikasi atau kompleksitas logika yang diimplementasikannya. Kami akan melihat ini saat kami mengembangkan daftar tugas.



Saya tidak akan membahas detail pengoperasian setiap alat, tetapi akan membatasi diri pada deskripsi umum dan tautan ke materi yang relevan. Untuk pembuatan prototipe UI, react-bootstrap akan digunakan .



Kode di GitHub

Sandbox di CodeSandbox



Buat proyek menggunakan Aplikasi Buat React:



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

      
      





Instal dependensi:



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

      
      





  • bootstrap, react-bootstrap - styles
  • nanoid - utilitas untuk menghasilkan ID unik


Di src, buat direktori "use-state" untuk versi tudushka yang pertama.



useState ()



Hooks Cheat Sheet



Hook useState () adalah untuk mengelola status lokal suatu komponen. Ini mengembalikan array dengan dua elemen: nilai status saat ini dan fungsi penyetel untuk memperbarui nilai ini. Tanda tangan dari hook ini adalah:



const [state, setState] = useState(initialValue)

      
      





  • negara - nilai negara saat ini
  • setState - penyetel
  • initialValue - nilai awal atau default


Salah satu keuntungan dari perusakan array, dibandingkan dengan perusakan objek, adalah kemampuan untuk menggunakan nama variabel yang berubah-ubah. Menurut ketentuan, nama penyetel harus dimulai dengan "set" + nama elemen pertama dengan huruf kapital ([count, setCount], [text, setText], dll.).



Untuk saat ini, kita akan membatasi diri pada empat operasi dasar: menambah, mengganti (mengeksekusi), memperbarui dan menghapus tugas, tetapi mari kita mempersulit hidup kita dengan fakta bahwa keadaan awal kita akan dalam bentuk data yang dinormalisasi (ini akan memungkinkan kita untuk mempraktikkan pembaruan yang tidak dapat diubah dengan benar).



Struktur proyek:



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

      
      





Saya pikir semuanya jelas di sini.



Di App.js, kami menggunakan useState () untuk menentukan status awal aplikasi, mengimpor dan merender komponen aplikasi, meneruskan status dan penyetel sebagai 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>
  )
}

      
      





Di TodoForm.js, kami menerapkan penambahan tugas baru ke daftar:



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

      
      





Di TodoList.js, kami hanya merender daftar item:



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

      
      





Akhirnya, bagian yang menyenangkan terjadi di TodoListItem.js - di sini kami menerapkan operasi yang tersisa: mengalihkan, memperbarui, dan menghapus tugas:



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

      
      





Di components / index.js, kami mengekspor ulang komponen:



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

      
      





File scr / index.js terlihat seperti ini:



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

      
      





Masalah utama dari pendekatan manajemen negara ini:



  • Kebutuhan untuk mentransfer status dan / atau penyetel di setiap tingkat bersarang karena sifat lokal negara bagian tersebut
  • Logika untuk memperbarui status aplikasi tersebar di seluruh komponen dan dicampur dengan logika komponen itu sendiri
  • Kompleksitas pembaruan negara yang timbul dari kekekalannya
  • Aliran data searah, ketidakmungkinan pertukaran data bebas antara komponen yang terletak di tingkat bersarang yang sama, tetapi di subpohon yang berbeda dari DOM virtual



Dua masalah pertama dapat diselesaikan dengan kombinasi useContext () / useReducer ().



useContext () + useReducer ()



Hooks Cheat Sheet



Context memungkinkan meneruskan nilai ke komponen anak secara langsung, melewati leluhurnya. Hook useContext () memungkinkan Anda mengambil nilai dari konteks dalam komponen apa pun yang dibungkus dalam penyedia.



Membuat konteks:



const TodoContext = createContext()

      
      





Memberikan konteks stateful ke komponen anak:



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

      
      





Mengekstrak nilai status dari konteks dalam sebuah komponen:



const state = useContext(TodoContext)

      
      





Hook useReducer () menerima reducer dan status awal. Ini mengembalikan nilai status saat ini dan fungsi untuk pengiriman operasi berdasarkan status diperbarui. Tanda tangan dari hook ini adalah:



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

      
      





Algoritme untuk memperbarui status terlihat seperti ini: komponen mengirim operasi ke reducer, dan reducer, berdasarkan jenis operasi (action.type) dan payload opsional operasi (action.payload), mengubah menyatakan dengan cara tertentu.



Kombinasi useContext () dan useReducer () menghasilkan kemampuan untuk meneruskan status dan dispatcher yang dikembalikan oleh useReducer () ke komponen apa pun yang merupakan turunan dari penyedia konteks.



Buat direktori "use-reducer" untuk trik versi kedua. Struktur proyek:



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

      
      





Mari kita mulai dengan gearbox. Di actionTypes.js, kita cukup mendefinisikan tipe (nama, konstanta) dari operasi:



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 }

      
      





Jenis operasi ditentukan dalam file terpisah, karena mereka digunakan baik saat membuat objek operasi maupun saat memilih peredam huruf dalam pernyataan sakelar. Ada pendekatan lain di mana tipe, pembuat operasi dan peredam ditempatkan dalam file yang sama. Pendekatan ini disebut struktur file "bebek".



Actions.js mendefinisikan apa yang disebut pembuat tindakan, yang mengembalikan objek dengan bentuk tertentu (untuk peredam):



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 }

      
      





Peredam itu sendiri didefinisikan dalam todoReducer.js. Sekali lagi, peredam mengambil status aplikasi dan operasi yang dikirim dari komponen dan, berdasarkan jenis operasi (dan payload), melakukan tindakan tertentu yang mengakibatkan status diperbarui. Memperbarui status dilakukan dengan cara yang sama seperti versi trik sebelumnya, kecuali bahwa alih-alih setState (), peredam mengembalikan status baru.



//    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 mendefinisikan status awal aplikasi, membuat dan mengekspor penyedia konteks dengan nilai status dan dispatcher dari 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)

      
      





Dalam kasus ini, src / index.js terlihat seperti ini:



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

      
      





Sekarang kita tidak perlu meneruskan status dan fungsi untuk mengupdatenya di setiap tingkat penyarangan komponen. Komponen mengambil status dan petugas operator menggunakan useTodoContext (), misalnya:



import { useTodoContext } from '../TodoContext'

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

      
      





Operasi dikirim ke reducer menggunakan dispatch (), tempat pembuat operasi diteruskan, ke mana payload dapat diteruskan:



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

//  
dispatch(actions.addTodo(newTodo))

      
      





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

      
      







Jadi, kami telah memecahkan dua masalah pertama yang terkait dengan penggunaan useState () sebagai alat untuk mengelola status. Faktanya, dengan bantuan perpustakaan yang menarik, kita dapat menyelesaikan masalah ketiga - kerumitan memperbarui status. immer memungkinkan Anda untuk mengubah nilai yang tidak dapat diubah dengan aman (ya, saya tahu bagaimana kedengarannya), cukup bungkus peredam dalam fungsi "produksi ()". Mari buat file "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
  }
})

      
      





Batasan utama yang dibebankan oleh immer adalah bahwa kita harus mengubah status secara langsung atau mengembalikan status yang telah diperbarui secara permanen. Anda tidak dapat melakukan keduanya pada saat bersamaan.



Kami membuat perubahan pada todoContext.js:



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

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

      
      





Semuanya berfungsi seperti sebelumnya, tetapi kode peredam sekarang lebih mudah dibaca dan diurai.



Bergerak.



Perangkat Redux



Panduan



Redux Toolkit Redux Toolkit adalah kumpulan alat yang memudahkan untuk bekerja dengan Redux. Redux sendiri sangat mirip dengan apa yang kami implementasikan dengan useContext () + useReducer ():



  • Status seluruh aplikasi ada di satu toko
  • Komponen turunan dibungkus dalam Penyedia dari react-redux , tempat penyimpanan diteruskan sebagai prop "penyimpanan"
  • Pengurang dari setiap bagian status digabungkan menggunakan gabunganReducers () menjadi peredam root tunggal, yang diteruskan ke createStore () saat penyimpanan dibuat.
  • Komponen terhubung ke toko menggunakan connect () (+ mapStateToProps (), mapDispatchToProps ()), dll.


Untuk mengimplementasikan operasi dasar, kami akan menggunakan utilitas berikut dari Redux Toolkit:



  • configureStore () - untuk membuat dan mengkonfigurasi penyimpanan
  • createSlice () - untuk membuat bagian-bagian negara
  • createEntityAdapter () - untuk membuat adaptor entitas


Beberapa saat kemudian, kami akan memperluas fungsionalitas daftar tugas menggunakan utilitas berikut:



  • createSelector () - untuk membuat pemilih
  • createAsyncThunk () - untuk membuat thunk


Juga dalam komponen kita akan menggunakan hook berikut dari react-redux: "useDispatch ()" - untuk mendapatkan akses ke dispatcher dan "useSelector ()" - untuk mendapatkan akses ke penyeleksi.



Buat direktori "redux-toolkit" untuk versi ketiga twist. Instal Redux Toolkit:



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

      
      





Struktur proyek:



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

      
      





Mari kita mulai dengan repositori. 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

      
      





Dalam kasus ini, src / index.js terlihat seperti ini:



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

      
      





Kami lolos ke gearbox. irisan / 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

      
      





Dalam komponen, useDispatch () digunakan untuk mengakses dispatcher, dan pembuat aktivitas yang diimpor dari todosSlice.js digunakan untuk mengirimkan operasi tertentu:



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

//  
const dispatch = useDispatch()

dispatch(addTodo(newTodo))

      
      





Mari kita sedikit memperluas fungsionalitas tudushka kita, yaitu: menambahkan kemampuan untuk memfilter tugas, tombol untuk menyelesaikan semua tugas dan menghapus tugas yang sudah selesai, serta beberapa statistik yang berguna. Mari kita juga menerapkan mendapatkan daftar tugas dari server.



Mari kita mulai dengan server.



Kami akan menggunakan JSON Server sebagai "API palsu" . Berikut adalah lembar contekan untuk mengatasinya . Instal json-server dan secara bersamaan - utilitas untuk menjalankan dua atau lebih perintah:



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

      
      





Kami membuat perubahan pada bagian "skrip" package.json:



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

      
      





  • -w - berarti memantau perubahan pada file "db.json"
  • -p - artinya port, secara default permintaan dari aplikasi dikirim ke port 3000
  • -d - menunda respon dari server


Buat file "db.json" di direktori root proyek (manajemen-negara):



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

      
      





Secara default, semua permintaan dari aplikasi dikirim ke port 3000 (port tempat server pengembangan berjalan). Agar permintaan dikirim ke port 5000 (port tempat server json akan dijalankan), permintaan tersebut harus di-proxy. Tambahkan baris berikut ke package.json:



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

      
      





Kami memulai server menggunakan perintah "server benang".



Kami membuat bagian lain dari negara bagian. irisan / 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

      
      





Kami membuat perubahan pada 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

      
      





Kami membuat perubahan pada 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

      
      





Kami membuat perubahan ke src / index.js:



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

store.dispatch(fetchTodos())

      
      





App.js terlihat seperti ini:



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

      
      





Kode komponen lainnya
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>
  )
}

      
      







Seperti yang bisa kita lihat, dengan munculnya Redux Toolkit, menggunakan Redux untuk mengelola status aplikasi menjadi lebih mudah daripada menggunakan kombinasi useContext () + useReducer () (luar biasa, tetapi benar), selain fakta bahwa Redux menyediakan lebih banyak opsi untuk itu. pengelolaan. Namun, Redux masih didesain untuk aplikasi stateful yang besar dan kompleks. Apakah ada alternatif lain untuk mengelola status aplikasi berukuran kecil hingga menengah selain useContext () / useReducer (). Jawabannya adalah ya. Ini Recoil .



Mundur



Recoil Guide



Recoil adalah alat baru untuk mengelola status di aplikasi React. Apa arti baru? Artinya, beberapa API-nya masih dalam pengembangan dan dapat berubah di masa mendatang. Namun, peluang yang akan kami gunakan untuk membuat tudushka stabil.



Atom dan penyeleksi berada di jantung Recoil. Atom adalah bagian dari keadaan, dan pemilih adalah bagian dari keadaan turunan. Atom dibuat menggunakan fungsi "atom ()", dan penyeleksi dibuat menggunakan fungsi "selector ()". Untuk mengambil nilai dari atom dan pemilih, useRecoilState () (baca dan tulis), useRecoilValue () (hanya baca), kait useSetRecoilState () (hanya tulis), dan lainnya. Komponen yang menggunakan status Recoil harus dibungkus dalam RecoilRoot . Terasa seperti Recoil adalah perantara antara useState () dan Redux.



Buat direktori "mundur" untuk tudushka terbaru dan instal Recoil:



yarn add recoil
# 
npm i recoil

      
      





Struktur proyek:



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

      
      





Seperti inilah tampilan atom daftar tugas:



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

      
      





Salah satu hal menarik tentang Recoil adalah kita dapat mencampur logika sinkron dan asinkron saat membuat atom dan penyeleksi. Ini dirancang sedemikian rupa sehingga kami memiliki kemampuan untuk menggunakan React Suspense untuk membuat konten fallback sebelum menerima data. Kami juga memiliki kemampuan untuk menggunakan sekering (ErrorBoundary) untuk menangkap kesalahan yang terjadi saat membuat atom dan selektor, termasuk secara asinkron.



Dalam kasus ini, src / index.js terlihat seperti ini:



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

      
      





Atom filter terlihat seperti ini:



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

      
      





Komponen mengekstrak nilai dari atom dan penyeleksi menggunakan kait di atas. Misalnya, kode untuk komponen "TodoListItem" terlihat seperti ini:



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

      
      





Kode komponen lainnya
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>
  )
}

      
      







Kesimpulan



Jadi, Anda dan saya telah menerapkan daftar tugas menggunakan empat pendekatan berbeda untuk mengelola negara. Kesimpulan apa yang bisa ditarik dari semua ini?



Saya akan mengungkapkan pendapat saya, itu tidak mengklaim sebagai kebenaran tertinggi. Tentu saja, memilih alat manajemen keadaan yang tepat bergantung pada tugas aplikasi:



  • Untuk mengelola keadaan lokal (keadaan satu atau dua komponen; dengan asumsi keduanya terkait erat) gunakan useState ()
  • Gunakan Recoil atau useContext () / useReducer () untuk mengelola status terdistribusi (status dua atau lebih komponen otonom) atau status aplikasi kecil hingga menengah.
  • Perhatikan bahwa jika Anda hanya perlu meneruskan nilai ke komponen bersarang dalam, maka useContext () baik-baik saja (useContext () itu sendiri bukan alat untuk mengelola status)
  • Terakhir, untuk mengelola status global (status semua atau sebagian besar komponen) atau status aplikasi yang kompleks, gunakan Redux Toolkit


MobX, yang saya dengar banyak hal baik, belum sempat beredar.



Terima kasih atas perhatiannya dan semoga harimu menyenangkan.



All Articles