良い一日、友達!
簡単なアプリケーション、つまりタスクのリストに注目します。それの何が特別なのか、あなたは尋ねます。重要なのは、Reactアプリケーションで状態を管理するための4つの異なるアプローチ(useState、useContext + useReducer、Redux Toolkit、およびRecoil)を使用して、同じトリックを実装しようとしたことです。
アプリケーションの状態と、それを操作するための適切なツールを選択することが非常に重要である理由から始めましょう。
状態は、アプリケーションに関連する情報の総称です。これは、同じタスクリストやユーザーリストなどのアプリケーションで使用されるデータと、読み込み状態やフォームの状態などの状態の両方にすることができます。
条件付きで、状態はローカルとグローバルに分けることができます。ローカル状態は通常、個々のコンポーネントの状態を指します。たとえば、フォームの状態は、原則として、対応するコンポーネントのローカル状態です。同様に、グローバル状態はより正確に分散または共有と呼ばれます。つまり、このような状態は複数のコンポーネントによって使用されます。問題のグラデーションの条件性は、ローカル状態が複数のコンポーネントによって使用される可能性があり(たとえば、useState()を使用して定義された状態を小道具として子コンポーネントに渡すことができる)、グローバル状態はそうではないという事実で表されます。必然的にすべてのアプリケーションコンポーネントで使用されます(たとえば、アプリケーション全体の状態に対して1つのストアがあるReduxでは、通常、UIの各部分、より正確には、この部分の制御ロジックに対して、状態の個別のスライスが作成されます。
アプリケーションの状態を管理するための適切なツールを選択することの重要性は、ツールがアプリケーションのサイズまたは実装するロジックの複雑さに一致しない場合に発生する問題に起因します。これは、やることリストを作成するときにわかります。
各ツールの操作の詳細については説明しませんが、一般的な説明と関連資料へのリンクに限定します。UIプロトタイピングには、react-bootstrapが使用され ます。 CodeSandboxの
GitHub
サンドボックスの コードCreateReactApp
を使用してプロジェクトを作成します。
yarn create react-app state-management
#
npm init react-app state-management
#
npx create-react-app state-management
インストールの依存関係:
yarn add bootstrap react-bootstrap nanoid
#
npm i bootstrap react-bootstrap nanoid
- bootstrap、react-bootstrap-スタイル
- nanoid-一意のIDを生成するためのユーティリティ
srcで、tudushkaの最初のバージョンの「use-state」ディレクトリを作成します。
useState()
フックに関するチートシート
useState()フックは、コンポーネントのローカル状態を管理するためのものです。現在の状態値と、この値を更新するためのセッター関数の2つの要素を持つ配列を返します。このフックの署名は次のとおりです。
const [state, setState] = useState(initialValue)
- state-状態の現在の値
- setState-セッター
- initialValue-初期値またはデフォルト値
オブジェクトの破棄とは対照的に、配列の破棄の利点の1つは、任意の変数名を使用できることです。慣例により、セッターの名前は「set」+大文字の最初の要素の名前([count、setCount]、[text、setText]など)で始まる必要があります。
今のところ、タスクの追加、切り替え(実行)、更新、削除の4つの基本操作に制限しますが、初期状態が正規化されたデータの形式になるという事実によって、私たちの生活を複雑にしましょう(これにより、不変の更新を適切に練習するため)。
プロジェクト構造:
|--use-state |--components |--index.js |--TodoForm.js |--TodoList.js |--TodoListItem.js |--App.js
ここではすべてが明確だと思います。
App.jsでは、useState()を使用してアプリケーションの初期状態を定義し、アプリケーションコンポーネントをインポートしてレンダリングし、それらに状態とセッターを小道具として渡します。
//
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>
)
}
TodoForm.jsでは、リストに新しいタスクを追加することを実装しています。
//
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>
)
}
TodoList.jsでは、アイテムのリストをレンダリングするだけです。
//
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>
)
最後に、楽しい部分はTodoListItem.jsで発生します。ここでは、残りの操作(タスクの切り替え、更新、削除)を実装します。
//
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>
)
}
components / index.jsで、コンポーネントを再エクスポートします。
export { TodoForm } from './TodoForm'
export { TodoList } from './TodoList'
scr /index.jsファイルは次のようになります。
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$)
状態管理へのこのアプローチの主な問題:
- 州の地域的な性質により、各ネスティングレベルで州および/またはセッターを転送する必要性
- アプリケーションの状態を更新するためのロジックは、コンポーネント全体に分散しており、コンポーネント自体のロジックと混合されています。
- その不変性から生じる状態更新の複雑さ
- 一方向のデータフロー、同じネストレベルにあるが、仮想DOMの異なるサブツリーにあるコンポーネント間でデータを自由に交換することは不可能
最初の2つの問題は、useContext()/ useReducer()の組み合わせで解決できます。
useContext()+ useReducer()
Hooks Cheat Sheet
Contextを使用すると、祖先をバイパスして、子コンポーネントに直接値を渡すことができます。useContext()フックを使用すると、プロバイダーにラップされた任意のコンポーネントのコンテキストから値を取得できます。
コンテキストの作成:
const TodoContext = createContext()
子コンポーネントにステートフルコンテキストを提供する:
<TodoContext.Provider value={state}>
<App />
</TodoContext.Provider>
コンポーネントのコンテキストから状態値を抽出する:
const state = useContext(TodoContext)
useReducer()フックは、レデューサーと初期状態を受け入れます。現在の状態の値と、状態の更新に基づいて操作をディスパッチするための関数を返します。このフックの署名は次のとおりです。
const [state, dispatch] = useReducer(todoReducer, initialState)
状態を更新するためのアルゴリズムは次のようになります。コンポーネントは操作をレデューサーに送信し、レデューサーは操作のタイプ(action.type)と操作のオプションのペイロード(action.payload)に基づいて、特定の方法で述べています。
useContext()とuseReducer()の組み合わせにより、useReducer()によって返された状態とディスパッチャーをコンテキストプロバイダーの子孫である任意のコンポーネントに渡すことができます。
トリックの2番目のバージョン用に「use-reducer」ディレクトリを作成します。プロジェクト構造:
|--use-reducer |--modules |--components |--index.js |--TodoForm.js |--TodoList.js |--TodoListItem.js |--todoReducer |--actions.js |--actionTypes.js |--todoReducer.js |--todoContext.js |--App.js
ギアボックスから始めましょう。actionTypes.jsでは、操作のタイプ(名前、定数)を定義するだけです。
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 }
操作タイプは、操作オブジェクトを作成するときと、switchステートメントでcaseレデューサーを選択するときの両方で使用されるため、別のファイルで定義されます。タイプ、操作の作成者、およびレデューサーを同じファイルに配置する別のアプローチがあります。このアプローチは「ダック」ファイル構造と呼ばれます。
Actions.jsは、特定の形状のオブジェクトを返す、いわゆるアクションクリエーターを定義します(レデューサー用)。
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 }
レデューサー自体はtodoReducer.jsで定義されています。この場合も、レデューサーはアプリケーションの状態とコンポーネントからディスパッチされた操作を取得し、操作の種類(およびペイロード)に基づいて特定のアクションを実行し、その結果、状態が更新されます。状態の更新は、setState()の代わりにレデューサーが新しい状態を返すことを除いて、以前のバージョンのトリックと同じ方法で実行されます。
// 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は、アプリケーションの初期状態を定義し、状態値を使用してコンテキストプロバイダーを作成し、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)
この場合、src /index.jsは次のようになります。
// 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$
)
これで、コンポーネントのネストの各レベルで状態と関数を渡して更新する必要がなくなりました。コンポーネントは、useTodoContext()を使用して状態とディスパッチャーを取得します。次に例を示します。
import { useTodoContext } from '../TodoContext'
//
const { state, dispatch } = useTodoContext()
オペレーションは、ディスパッチ()を使用してレデューサーにディスパッチされます。ディスパッチ()には、オペレーションの作成者が渡され、ペイロードを渡すことができます。
import * as actions from '../todoReducer/actions'
//
dispatch(actions.addTodo(newTodo))
コンポーネントコード
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>
)
}
したがって、状態を管理するためのツールとしてuseState()を使用することに関連する最初の2つの問題を解決しました。実際、興味深いライブラリの助けを借りて、3番目の問題である状態の更新の複雑さを解決できます。 immerを使用すると、不変の値を安全に変更できます(はい、それがどのように聞こえるかはわかります)。レデューサーを「produce()」関数でラップするだけです。「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
}
})
イマーが課す主な制限は、状態を直接変更するか、不変に更新された状態を返す必要があることです。両方を同時に行うことはできません。
todoContext.jsに変更を加えます。
// import { todoReducer } from './todoReducer/todoReducer'
import { todoProducer } from './todoReducer/todoProducer'
//
// const [state, dispatch] = useReducer(todoReducer, initialState)
const [state, dispatch] = useReducer(todoProducer, initialState)
すべてが以前と同じように機能しますが、レデューサーコードの読み取りと解析が簡単になりました。
先に進みます。
Reduxツールキット
Reduxツールキットガイド
Reduxツールキットは、Reduxの操作を簡単にするツールのコレクションです。Redux自体は、useContext()+ useReducer()で実装したものと非常によく似ています。
- アプリケーション全体の状態は1つのストアにあります
- 子コンポーネントはreact-reduxからプロバイダーにラップされ、ストアは「ストア」プロップとして渡されます
- 状態の各部分のレデューサーは、combineReducers()を使用して単一のルートレデューサーに結合され、ストアの作成時にcreateStore()に渡されます。
- コンポーネントは、connect()(+ mapStateToProps()、mapDispatchToProps())などを使用してストアに接続されます。
基本的な操作を実装するために、ReduxToolkitの次のユーティリティを使用します。
- configureStore()-ストアを作成および構成するため
- createSlice()-状態の一部を作成します
- createEntityAdapter()-エンティティアダプタを作成します
少し後で、次のユーティリティを使用してタスクリストの機能を拡張します。
- createSelector()-セレクターを作成するため
- createAsyncThunk()-サンクを作成します
また、コンポーネントでは、react-reduxから次のフックを使用します: "useDispatch()"-ディスパッチャーへのアクセスを取得し、 "useSelector()"-セレクターへのアクセスを取得します。
ツイストの3番目のバージョン用のディレクトリ「redux-toolkit」を作成します。Reduxツールキットをインストールします。
yarn add @reduxjs/toolkit
#
npm i @reduxjs/toolkit
プロジェクト構造:
|--redux-toolkit |--modules |--components |--index.js |--TodoForm.js |--TodoList.js |--TodoListItem.js |--slices |--todosSlice.js |--App.js |--store.js
リポジトリから始めましょう。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
この場合、src /index.jsは次のようになります。
// 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$
)
ギアボックスに渡します。スライス/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
コンポーネントでは、useDispatch()を使用してディスパッチャーにアクセスし、todosSlice.jsからインポートされたアクティビティクリエーターを使用して特定の操作をディスパッチします。
import { useDispatch } from 'react-redux'
import { addTodo } from '../slices/todosSlice'
//
const dispatch = useDispatch()
dispatch(addTodo(newTodo))
tudushkaの機能を少し拡張してみましょう。つまり、タスクをフィルタリングする機能、すべてのタスクを完了して完了したタスクを削除するボタン、およびいくつかの有用な統計を追加します。サーバーからタスクのリストを取得することも実装しましょう。
サーバーから始めましょう。 JSONサーバー
を「偽のAPI」として使用し ます。ここだ 、それを操作するためのチートシート。json-serverを同時にインストールします-2 つ以上のコマンドを実行するためのユーティリティ:
yarn add json-server concurrently # npm i json-server concurrently
package.jsonの「scripts」セクションに変更を加えます。
"server": "concurrently \"json-server -w db.json -p 5000 -d 1000\" \"yarn start\""
- -w-「db.json」ファイルへの変更を監視することを意味します
- -p-ポートを意味します。デフォルトでは、アプリケーションからの要求はポート3000に送信されます。
- -d-サーバーからの応答を遅らせる
プロジェクトのルートディレクトリにファイル「db.json」を作成します(状態管理)。
{
"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
}
]
}
デフォルトでは、アプリケーションからのすべての要求はポート3000(開発サーバーが実行されているポート)に送信されます。リクエストをポート5000(json-serverが実行されるポート)に送信するには、リクエストをプロキシする必要があります。package.jsonに次の行を追加します。
"proxy": "http://localhost:5000"
「yarnserver」コマンドを使用してサーバーを起動します。
状態の別の部分を作成します。スライス/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
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
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
src /index.jsに変更を加えます。
// "App"
import { fetchTodos } from './redux-toolkit/modules/slices/todosSlice'
store.dispatch(fetchTodos())
App.jsは次のようになります。
//
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>
)
}
その他のコンポーネントコード
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>
)
}
ご覧のとおり、Redux Toolkitの登場により、Reduxがアプリケーションの状態を管理するためにReduxを使用することは、useContext()+ useReducer()の組み合わせを使用するよりも簡単になりました(信じられないが本当です)。管理。ただし、Reduxは依然として大規模で複雑なステートフルアプリケーション向けに設計されています。useContext()/ useReducer()以外に、中小規模のアプリケーションの状態を管理するための代替手段はありますか。答えはイエスです。これは リコイルです。
反動
Recoilガイド
Recoilは、Reactアプリケーションの状態を管理するための新しいツールです。 newとはどういう意味ですか?これは、一部のAPIがまだ開発中であり、将来変更される可能性があることを意味します。ただし、tudushkaを作成するために使用する機会は安定しています。
アトムとセレクターはリコイルの中心です。アトムは状態の一部であり、セレクターは派生状態の一部です。アトムは「atom()」関数を使用して作成され、セレクターは「selector()」関数を使用して作成されます。アトムとセレクターから値を取得するには、useRecoilState()(読み取りと書き込み)、useRecoilValue()(読み取り専用)、useSetRecoilState()(書き込み専用)フックなどを使用します。Recoil状態を使用するコンポーネントは、RecoilRootでラップする必要があります。 。RecoilはuseState()とReduxの中間にあるように感じます。
最新のtudushkaの「recoil」ディレクトリを作成し、Recoilをインストールします。
yarn add recoil
#
npm i recoil
プロジェクト構造:
|--recoil |--modules |--atoms |--filterAtom.js |--todosAtom.js |--components |--index.js |--TodoControls.js |--TodoFilters.js |--TodoForm.js |--TodoList.js |--TodoListItem.js |--TodoStats.js |--App.js
タスクリストアトムは次のようになります。
// 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())
}
}
})
})
Recoilの興味深い点の1つは、アトムとセレクターを作成するときに同期ロジックと非同期ロジックを混在させることができることです。これは、React Suspenseを使用して、データを受信する前にフォールバックコンテンツをレンダリングできるように設計されています。また、ヒューズ(ErrorBoundary)を使用して、非同期の方法を含め、アトムとセレクターの作成時に発生するエラーをキャッチする機能もあります。
この場合、src /index.jsは次のようになります。
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$
)
フィルタアトムは次のようになります。
// 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)
}
})
コンポーネントは、上記のフックを使用してアトムとセレクターから値を抽出します。たとえば、「TodoListItem」コンポーネントのコードは次のようになります。
//
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>
)
}
その他のコンポーネントコード
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>
)
}
結論
したがって、あなたと私は、状態を管理するための4つの異なるアプローチを使用してタスクのリストを実装しました。これらすべてからどのような結論を引き出すことができますか?
私は私の意見を表明します、それは究極の真実であるとは主張しません。もちろん、適切な状態管理ツールの選択は、アプリケーションのタスクによって異なります。
- ローカル状態(1つまたは2つのコンポーネントの状態。2つが密接に関連していると仮定)を管理するには、useState()を使用します。
- RecoilまたはuseContext()/ useReducer()を使用して、分散状態(2つ以上の自律コンポーネントの状態)または中小規模のアプリケーションの状態を管理します。
- 深くネストされたコンポーネントに値を渡す必要がある場合は、useContext()で問題ありません(useContext()自体は状態を管理するためのツールではありません)
- 最後に、グローバル状態(すべてまたはほとんどのコンポーネントの状態)または複雑なアプリケーションの状態を管理するには、ReduxToolkitを使用します
良いことをたくさん聞いたMobXは、まだ手に入れていません。
ご清聴ありがとうございました。良い一日を。