
è¯ãäžæ¥ãåéïŒ
ç°¡åãªã¢ããªã±ãŒã·ã§ã³ãã€ãŸãã¿ã¹ã¯ã®ãªã¹ãã«æ³šç®ããŸããããã®äœãç¹å¥ãªã®ããããªãã¯å°ããŸããéèŠãªã®ã¯ã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ã¯ããŸã æã«å ¥ããŠããŸããã
ãæž èŽããããšãããããŸãããè¯ãäžæ¥ãã