つくるもの
ほぼチュートリアル通りのあいつです。
要件は以下のような感じです。
・タスクを登録することができる
・左チェックボックスで完了にすることができる
・右の削除ボタンで削除ができる
・チェックしたタスクが完了したタスクに表示される
・チェックしていないタスクが現在のタスクに表示される
・削除したタスクが削除済みのタスクに表示される
・削除したタスクを復元することができる
・削除済みのリストで「ゴミ箱を空にする」を押すと削除済みタスクが抹消される
管理されるStateとStateを更新する関数
それぞれ以下を管理する方法が違う。
管理されるState
- セレクトボックスの入力値
- タスク作成フォームの入力値
- タスク一覧
- タスクの中身
- タスクのvalue
- タスクのcheckboxの真偽値
- 削除済みのタスクかどうかの真偽値
Stateを更新する関数
- セレクトボックスの入力値を更新し、値によってフィルタしたタスクのリストを返す関数
- タスク作成フォームの入力値を更新する関数
- タスクの一覧を更新する関数
- タスクのチェックボックスの値を更新する関数
- タスクの削除フラグを更新する関数
- 削除済みのタスクを一括削除する関数
useStateを使った実装
Todoのリスト / セレクトボックスの入力値 / フォームの入力値 をuseStateで管理している。
const [state, setState] = useState<型>('初期値');
setState('新たな値');
import React, { useState } from 'react';
import './App.css';
interface Todo {
value: string,
id: number,
checked: boolean,
removed: boolean;
}
type Filter = 'all' | 'checked' | 'unchecked' | 'removed';
const App: React.VFC = () => {
const [text, setText] = useState<string>('');
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<Filter>('all');
const handleOnClick = () => {
if(!text) return;
const newTodo: Todo = {
value: text,
id: new Date().getTime(),
checked: false,
removed: false,
};
setTodos([newTodo, ...todos]);
setText('');
};
const handleOnEdit = (id: number, value: string) => {
const newTodos = todos.map((todo) => {
if (todo.id === id) {
todo.value = value;
}
return todo;
});
setTodos(newTodos);
};
const handleOnCheck = (id: number, checked: boolean) => {
const newTodos = todos.map((todo) => {
if (todo.id === id) {
todo.checked = !checked;
}
return todo;
});
setTodos(newTodos);
};
const handleOnRemove = (id: number, removed: boolean) => {
console.log('removed: ', removed);
console.log('id: ', id);
const newTodos = todos.map((todo) => {
if (todo.id === id) {
todo.removed = !removed;
}
return todo;
});
setTodos(newTodos);
};
const filteredTodos = todos.filter((todo) => {
switch (filter) {
case 'all':
return !todo.removed;
case 'checked':
return todo.checked && !todo.removed;
case 'unchecked':
return !todo.checked && !todo.removed;
case 'removed':
return todo.removed;
default:
return todo;
}
});
const handleOnEmpty = () => {
const newTodos = todos.filter((todo) => !todo.removed);
setTodos(newTodos);
};
return (
<div>
<select
defaultValue="all"
onChange={(e) => setFilter(e.target.value as Filter)}>
<option value="all">すべてのタスク</option>
<option value="checked">完了したタスク</option>
<option value="unchecked">未完了のタスク</option>
<option value="removed">削除済みのタスク</option>
</select>
{
filter === 'removed' ? (
<button onClick={() => handleOnEmpty()}>ゴミ箱を空にする</button>
) : (
<form onSubmit={(e) => e.preventDefault()}>
<input
type="text"
value={text}
disabled={filter === 'checked'}
onChange={(e) => setText(e.target.value)}
/>
<button
onClick={() => handleOnClick()}
disabled={filter === 'checked'}
>追加</button>
</form>
)
}
<div className="align-lists">
<ul>
{filteredTodos.map((todo) => {
return (
<li key={todo.id}>
<input
type="checkbox"
disabled={todo.removed}
checked={todo.checked}
onChange={() => handleOnCheck(todo.id, todo.checked)}
/>
<input
type="text"
disabled={todo.checked || todo.removed}
value={todo.value}
onChange={(e) => handleOnEdit(todo.id, e.target.value)}
/>
<button onClick={() => handleOnRemove(todo.id, todo.removed)}>
{todo.removed ? '復元' : '削除'}
</button>
</li>
)
})}
</ul>
</div>
</div>
);
};
export default App;
useReducerを使った実装
Todoのリスト / セレクトボックスの入力値 / フォームの入力値 を state にまとめて管理している。
更新はreducerメソッドをdispatchで呼び出して行う。
アクションをdispatchすると、reducer に現在のstateとアクションが渡って処理される。
stateを更新するロジックは reducer に集約される。
const [state, dispatch] = useReducer(reducer, '初期値');
dispatch({ type: 'アクションのタイプ', ...その他更新に使用する値})
const reducer = (state, action) => {
switch (action.type) {
case 'hoge':
...
}
}
import React, { useReducer, memo, Dispatch } from 'react';
import './App.css';
interface Todo {
value: string,
id: number,
checked: boolean,
removed: boolean;
}
type Filter = 'all' | 'checked' | 'unchecked' | 'removed';
interface State {
text: string;
todos: Todo[];
filter: Filter;
}
const initialState: State = {
text: '',
todos: [],
filter: 'all',
};
type Action =
{ type: 'change', value: string }
| { type: 'filter', value: Filter }
| { type: 'submit' }
| { type: 'empty' }
| { type: 'edit', id: number, value: string }
| { type: 'check', id: number, checked: boolean }
| { type: 'remove', id: number, removed: boolean }
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'change': {
return { ...state, text: action.value };
}
case 'submit': {
if (!state.text) return state;
const newTodo: Todo = {
value: state.text,
id: new Date().getTime(),
checked: false,
removed: false,
};
return { ...state, todos: [newTodo, ...state.todos], text: '' };
}
case 'filter':
return { ...state, filter: action.value };
case 'edit': {
const newTodos = state.todos.map((todo) => {
if (todo.id === action.id) {
todo.value = action.value;
}
return todo;
});
return { ...state, todos: newTodos };
}
case 'check': {
const newTodos = state.todos.map((todo) => {
if (todo.id === action.id) {
todo.checked = !action.checked;
}
return todo;
});
return { ...state, todos: newTodos };
}
case 'remove': {
const newTodos = state.todos.map((todo) => {
if (todo.id === action.id) {
todo.removed = !action.removed;
}
return todo;
});
return { ...state, todos: newTodos };
}
case 'empty': {
const newTodos = state.todos.filter((todo) => !todo.removed);
return { ...state, todos: newTodos };
}
default:
return state;
}
};
const Selector: React.VFC<{ dispatch: Dispatch<Action> }> = memo(
({ dispatch }) => {
const handleOnFilter = (e: React.ChangeEvent<HTMLSelectElement>) => {
dispatch({ type: 'filter', value: e.target.value as Filter });
};
return (
<select className="select" defaultValue="all" onChange={handleOnFilter}>
<option value="all">すべてのタスク</option>
<option value="checked">完了したタスク</option>
<option value="unchecked">現在のタスク</option>
<option value="removed">削除済みのタスク</option>
</select>
);
}
);
Selector.displayName = 'Selector';
const EmptyButton: React.VFC<{ dispatch: Dispatch<Action> }> = memo(
({ dispatch }) => {
const handleOnEmpty = () => {
dispatch({ type: 'empty' });
};
return (
<button className="empty" onClick={handleOnEmpty}>
ごみ箱を空にする
</button>
);
}
);
EmptyButton.displayName = 'EmptyButton';
const Form: React.VFC<{ state: State; dispatch: Dispatch<Action> }> = memo(
({ state, dispatch }) => {
const handleOnClick = () => {
dispatch({ type: 'submit' });
};
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({ type: 'change', value: e.target.value });
};
return (
<form className="form" onSubmit={handleOnSubmit}>
<input
className="text"
type="text"
disabled={state.filter === 'checked'}
value={state.text}
onChange={handleOnChange}
/>
<button
className="button"
disabled={state.filter === 'checked'}
value="追加"
onClick={handleOnClick}
/>
</form>
);
}
);
Form.displayName = 'Form';
const FilteredTodos: React.VFC<{
state: State;
dispatch: Dispatch<Action>;
}> = memo(({ state, dispatch }) => {
const handleOnEdit = (id: number, value: string) => {
dispatch({ type: 'edit', id, value });
};
const handleOnCheck = (id: number, checked: boolean) => {
dispatch({ type: 'check', id, checked });
};
const handleOnRemove = (id: number, removed: boolean) => {
dispatch({ type: 'remove', id, removed });
};
const filteredTodos = state.todos.filter((todo) => {
switch (state.filter) {
case 'all':
return !todo.removed;
case 'checked':
return !todo.removed && todo.checked;
case 'unchecked':
return !todo.removed && !todo.checked;
case 'removed':
return todo.removed;
default:
return todo;
}
});
return (
<div className="align-lists">
<ul>
{filteredTodos.map((todo) => {
return (
<li key={todo.id}>
<input
type="checkbox"
disabled={todo.removed}
checked={todo.checked}
onChange={() => handleOnCheck(todo.id, todo.checked)}
/>
<input
className="text"
type="text"
disabled={todo.checked || todo.removed}
value={todo.value}
onChange={(e) => handleOnEdit(todo.id, e.target.value)}
/>
<button
className="button"
onClick={() => handleOnRemove(todo.id, todo.removed)}>
{todo.removed ? '復元' : '削除'}
</button>
</li>
);
})}
</ul>
</div>
);
});
FilteredTodos.displayName = 'FilteredTodos';
const App: React.VFC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div className="container">
<Selector dispatch={dispatch} />
{state.filter === 'removed' ? (
<EmptyButton dispatch={dispatch} />
) : (
<Form state={state} dispatch={dispatch} />
)}
<FilteredTodos state={state} dispatch={dispatch} />
</div>
);
};
export default App;
useContextを使った実装
更新方法については useReducer と同じく、アクションをdispatchすることでreducerで処理される。
createContextによって作成された Context.Provider から state と dispatch メソッドを提供することで、propsで渡す必要がなくなる。
const Context = React.createContext('初期値');
const [state, dispatch] = useReducer(reducer, initialState);
<Context.Provider value={{ state, dispatch }}>
// ここで呼び出されるコンポーネントは、Context.Providerの提供する state と dispatch メソッドを使用することができる。
</Context.Provider>
// これで使用できる
const { state, dispatch } = useContext(Context);
import React, {
useReducer,
memo,
Dispatch,
createContext,
useContext
} from 'react';
import './App.css';
interface Todo {
value: string,
id: number,
checked: boolean,
removed: boolean;
}
type Filter = 'all' | 'checked' | 'unchecked' | 'removed';
interface State {
text: string;
todos: Todo[];
filter: Filter;
}
const initialState: State = {
text: '',
todos: [],
filter: 'all',
};
type Action =
{ type: 'change', value: string }
| { type: 'filter', value: Filter }
| { type: 'submit' }
| { type: 'empty' }
| { type: 'edit', id: number, value: string }
| { type: 'check', id: number, checked: boolean }
| { type: 'remove', id: number, removed: boolean }
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'change': {
return { ...state, text: action.value };
}
case 'submit': {
if (!state.text) return state;
const newTodo: Todo = {
value: state.text,
id: new Date().getTime(),
checked: false,
removed: false,
};
return { ...state, todos: [newTodo, ...state.todos], text: '' };
}
case 'filter':
return { ...state, filter: action.value };
case 'edit': {
const newTodos = state.todos.map((todo) => {
if (todo.id === action.id) {
todo.value = action.value;
}
return todo;
});
return { ...state, todos: newTodos };
}
case 'check': {
const newTodos = state.todos.map((todo) => {
if (todo.id === action.id) {
todo.checked = !action.checked;
}
return todo;
});
return { ...state, todos: newTodos };
}
case 'remove': {
const newTodos = state.todos.map((todo) => {
if (todo.id === action.id) {
todo.removed = !action.removed;
}
return todo;
});
return { ...state, todos: newTodos };
}
case 'empty': {
const newTodos = state.todos.filter((todo) => !todo.removed);
return { ...state, todos: newTodos };
}
default:
return state;
}
};
const AppContext = createContext(
{} as {
state: State;
dispatch: Dispatch<Action>;
}
);
const Selector: React.VFC= memo(() => {
const { state, dispatch } = useContext(AppContext);
const handleOnFilter = (e: React.ChangeEvent<HTMLSelectElement>) => {
dispatch({ type: 'filter', value: e.target.value as Filter });
};
return (
<select className="select" defaultValue="all" onChange={handleOnFilter}>
<option value="all">すべてのタスク</option>
<option value="checked">完了したタスク</option>
<option value="unchecked">現在のタスク</option>
<option value="removed">削除済みのタスク</option>
</select>
);
});
Selector.displayName = 'Selector';
const EmptyButton: React.VFC = memo(() => {
const { state, dispatch } = useContext(AppContext);
const handleOnEmpty = () => {
dispatch({ type: 'empty' });
};
return (
<button className="empty" onClick={handleOnEmpty}>
ごみ箱を空にする
</button>
);
});
EmptyButton.displayName = 'EmptyButton';
const Form: React.VFC = memo(() => {
const { state, dispatch } = useContext(AppContext);
const handleOnClick = () => {
dispatch({ type: 'submit' });
};
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({ type: 'change', value: e.target.value });
};
return (
<form className="form" onSubmit={(e) => e.preventDefault()}>
<input
className="text"
type="text"
disabled={state.filter === 'checked'}
value={state.text}
onChange={handleOnChange}
/>
<button
disabled={state.filter === 'checked'}
onClick={handleOnClick}
>追加</button>
</form>
);
});
Form.displayName = 'Form';
const FilteredTodos: React.VFC = memo(() => {
const { state, dispatch } = useContext(AppContext);
const handleOnEdit = (id: number, value: string) => {
dispatch({ type: 'edit', id, value });
};
const handleOnCheck = (id: number, checked: boolean) => {
dispatch({ type: 'check', id, checked });
};
const handleOnRemove = (id: number, removed: boolean) => {
dispatch({ type: 'remove', id, removed });
};
const filteredTodos = state.todos.filter((todo) => {
switch (state.filter) {
case 'all':
return !todo.removed;
case 'checked':
return !todo.removed && todo.checked;
case 'unchecked':
return !todo.removed && !todo.checked;
case 'removed':
return todo.removed;
default:
return todo;
}
});
return (
<div className="align-lists">
<ul>
{filteredTodos.map((todo) => {
return (
<li key={todo.id}>
<input
type="checkbox"
disabled={todo.removed}
checked={todo.checked}
onChange={() => handleOnCheck(todo.id, todo.checked)}
/>
<input
className="text"
type="text"
disabled={todo.checked || todo.removed}
value={todo.value}
onChange={(e) => handleOnEdit(todo.id, e.target.value)}
/>
<button
className="button"
onClick={() => handleOnRemove(todo.id, todo.removed)}>
{todo.removed ? '復元' : '削除'}
</button>
</li>
);
})}
</ul>
</div>
);
});
FilteredTodos.displayName = 'FilteredTodos';
const App: React.VFC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
<div className="container">
<Selector />
{state.filter === 'removed' ? (
<EmptyButton />
) : (
<Form />
)}
<FilteredTodos />
</div>
</AppContext.Provider>
);
};
export default App;
Redux(toolkit)を使った実装
1つのStoreを分割して、nameで区切られたSliceを作成。それぞれのSliceごとにStore / Reducer / Actionsが管理される。
下記のtodoSliceでreducersを定義すると、todos/addTodoのActionCreatorが自動生成されるため、Actiontypeでswitch文を書いたりしなくても良い。
// Sliceの例
const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo(state, action) {
const {id, text} = action.payload;
state.push({id, text, removed: true})
}
......
}
})
// configureStore で、Sliceを1つにまとめたstoreを作成
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
todoReducer: todoReducer,
},
});
// コンポーネントにstoreを渡すことで、どこからでもStoreにアクセスできる
import { store } from './app/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
document.getElementById('root')
);
// useSelectorを使用して呼び出し
import { useSelector, useDispatch } from "react-redux";
const generation = useSelector(todo);
// useDispatchを使用して更新
dispatch(addTodo(todo))
型を定義
export type Todo = {
value: string,
id: number,
checked: boolean,
removed: boolean;
};
export type State = {
text: string;
todos: Todo[];
filter: string;
};
Slice
Stateを管理するSliceを作成する。
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {Todo, State} from '../types/Types';
const initialState: State = {
text: '',
todos: [],
filter: 'all',
};
interface updateParams {
id: number,
value: string,
};
const taskModule = createSlice({
name: 'todos',
initialState,
reducers: {
addTask(state: State, action: PayloadAction<string>) {
const newTodo: Todo = {
value: action.payload,
id: new Date().getTime(),
checked: false,
removed: false,
};
return { ...state, todos: [newTodo, ...state.todos], text: ''};
},
editTask(state: State, action: PayloadAction<updateParams>) {
const newTodos = state.todos.map((todo: Todo) => {
if(todo.id === action.payload.id) {
todo.value = action.payload.value;
};
return todo;
});
return { ...state, todos: newTodos }
},
checkTask(state: State, action: PayloadAction<Todo>) {
state.todos.map((todo: Todo) => {
if (todo.id === action.payload.id) {
todo.checked = !action.payload.checked;
}
return todo;
});
},
removeTask(state: State, action: PayloadAction<Todo>) {
state.todos.map((todo) => {
if (todo.id === action.payload.id) {
todo.removed = !action.payload.removed;
}
return todo;
});
},
emptyTask(state: State) {
const newTodos: Todo[] = state.todos.filter((todo) => !todo.removed);
return { ...state, todos: newTodos };
},
changeFilter(state: State, action: PayloadAction<string>) {
state.filter = action.payload
return state;
},
changeText(state: State, action: PayloadAction<string>) {
state.text = action.payload;
return state;
},
}
});
export const {
addTask, editTask, checkTask, removeTask, emptyTask, changeFilter, changeText,
} = taskModule.actions;
export default taskModule;
ストア
作成したSliceを一つにまとめてstoreにする。
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
const store = configureStore({
reducer: rootReducer,
});
export type AppDispatch = typeof store.dispatch;
export default store;
ルート
storeをAppコンポーネント以下に適用する
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
メイン
import React from 'react';
import { useSelector } from 'react-redux';
import { RootState } from './rootReducer';
import './App.css';
import Selector from './components/Selector';
import Form from './components/Form';
import EmptyButton from './components/EmptyButton';
import FilteredTodos from './components/FilteredTodos';
const App: React.VFC = () => {
const { filter } = useSelector((state: RootState) => state.filter);
return (
<div className="container">
<Selector />
{filter === 'removed' ? (
<EmptyButton />
) : (
<Form />
)}
<FilteredTodos />
</div>
);
};
export default App;
コンポーネント
各コンポーネントでは、useSelector
を用いてstoreの値を取得し、useDispatch
を用いてstoreの値を更新する。
import React, {useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {addTask} from '../modules/taskModules';
import { RootState } from '../rootReducer';
function Form() {
const dispatch = useDispatch();
const { filter } = useSelector((state: RootState) => state.filter);
const [inputText, setInputText] = useState<string>('');
const handleSubmit = () => {
dispatch(addTask(inputText));
setInputText('');
}
return (
<div className="form">
<input
className="text"
type="text"
disabled={filter === 'checked'}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
<button
className="button"
disabled={filter === 'checked'}
onClick={handleSubmit}
>追加</button>
</div>
);
};
export default Form;
import React from 'react';
import {useDispatch} from 'react-redux';
import {changeFilter} from '../modules/taskModules';
function Selector() {
const dispatch = useDispatch();
return (
<select className="select" defaultValue="all" onChange={(e) => dispatch(changeFilter(e.target.value))}>
<option value='all'>すべてのタスク</option>
<option value='checked'>完了したタスク</option>
<option value='unchecked'>現在のタスク</option>
<option value='removed'>削除済みのタスク</option>
</select>
);
};
export default Selector;
import React from 'react';
import TodoItem from './TodoItem';
import {useSelector} from 'react-redux';
import { RootState } from '../rootReducer';
function FilteredTodos() {
const { todos } = useSelector((state: RootState) => state.todos);
const { filter } = useSelector((state: RootState) => state.filter);
const filteredTodos = todos.filter((todo: any) => {
switch (filter) {
case 'all':
return !todo.removed;
case 'checked':
return !todo.removed && todo.checked;
case 'unchecked':
return !todo.removed && !todo.checked;
case 'removed':
return todo.removed;
default:
return todo;
}
});
return (
<div className="align-lists">
<ul>
{filteredTodos.map((todo: any) => {
return (
<TodoItem todo={todo} />
);
})}
</ul>
</div>
);
};
export default FilteredTodos;
import React, {useState} from 'react';
import { useDispatch } from 'react-redux';
import { editTask, checkTask, removeTask } from '../modules/taskModules';
import {Todo} from '../types/Types';
type Props = {
todo: Todo
}
function TodoItem(props: Props) {
const todo: Todo = props.todo;
const dispatch = useDispatch();
const [inputText, setInputText] = useState(todo.value);
const handleUpdateTodo = (e: any) => {
setInputText(e.target.value);
const params = {
id: todo.id,
value: inputText,
}
dispatch(editTask(params));
};
return (
<li key={todo.id}>
<input
type="checkbox"
disabled={todo.removed}
checked={todo.checked}
onChange={() => dispatch(checkTask(todo))}
/>
<input
className="text"
type="text"
disabled={todo.checked || todo.removed}
value={inputText}
onChange={handleUpdateTodo}
/>
<button
className="button"
onClick={() => dispatch(removeTask(todo))}>
{todo.removed ? '復元' : '削除'}
</button>
</li>
);
}
export default TodoItem;
import React from 'react';
import {useDispatch} from 'react-redux';
import {emptyTask} from '../modules/taskModules';
function EmptyButton() {
const dispatch = useDispatch();
return (
<button className="empty" onClick={() => dispatch(emptyTask())}>
ごみ箱を空にする
</button>
);
};
export default EmptyButton;
Recoilを使った実装
stateは atom というストアで管理される。
RecoilRootで囲ったコンポーネント群は atom を共有でき、値を取得・更新することができる。
import { RecoilRoot } from "recoil";
<RecoilRoot>
<App />
</RecoilRoot>
Todoの型を定義
type Todo = {
value: string,
id: number,
checked: boolean,
removed: boolean,
}
export default Todo;
メイン
import React from 'react';
import './App.css';
import Selector from './components/Selector';
import Form from './components/Form';
import FilteredTodos from './components/TodoList';
import EmptyButton from './components/EmptyButton';
import { searchTextFormState } from './atoms/SearchToDoFormAtom';
import { useRecoilValue } from "recoil";
const App: React.FC = () => {
const filter: string = useRecoilValue(searchTextFormState);
return (
<div className="container">
<Selector />
{filter === 'removed' ? (
<EmptyButton />
) : (
<Form />
)}
<FilteredTodos />
</div>
);
}
export default App;
コンポーネント
各コンポーネントで atom を利用して画面を動的に変更する。
atom に格納されている state は、useRecoilValueメソッドによって取得できる。
atom に格納されている state を更新したい場合は、useSetRecoilStateメソッドによってセッターを定義し、セッターに更新したい値を渡す。
import atom from 'atom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
const atom = useRecoilValue(atom);
const setAtom = useSetRecoilState(atom);
setAtom('更新したい値')
import React, { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from 'recoil';
import { todoTitleFormState } from '../atoms/AddToDoFormAtom';
import { todoListState } from '../atoms/ToDoListAtom';
import { searchTextFormState } from '../atoms/SearchToDoFormAtom';
import Todo from "../types/Todo";
const Form: React.FC = () => {
const todoList: Todo[] = useRecoilValue(todoListState);
const todoTitleFormValue: string = useRecoilValue(todoTitleFormState);
const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);
const setTodoTitleFormValue: SetterOrUpdater<string> = useSetRecoilState(
todoTitleFormState
);
const filter: string = useRecoilValue(searchTextFormState);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setTodoTitleFormValue(event.target.value);
},
[setTodoTitleFormValue]
);
const onClick = useCallback(() => {
const newTodo = {
value: todoTitleFormValue,
id: new Date().getTime(),
checked: false,
removed: false,
};
setTodoList([...todoList, newTodo]);
setTodoTitleFormValue('');
}, [todoList, todoTitleFormValue, setTodoList, setTodoTitleFormValue]);
return (
<form className="form" onSubmit={(e) => e.preventDefault()}>
<input
className="text"
type="text"
disabled={filter === 'checked'}
value={todoTitleFormValue}
onChange={onChange}
/>
<button
disabled={filter === 'checked'}
onClick={onClick}
>追加</button>
</form>
)
}
export default Form;
import React, { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from 'recoil';
import { searchTextFormState } from '../atoms/SearchToDoFormAtom';
const Selector: React.FC = () => {
const searchTextFormValue: string = useRecoilValue(searchTextFormState);
const setSearchTextFormValue: SetterOrUpdater<string> = useSetRecoilState(
searchTextFormState
);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
setSearchTextFormValue(event.target.value);
},
[setSearchTextFormValue]
);
return (
<select className="select" value={searchTextFormValue} defaultValue="all" onChange={onChange}>
<option value="all">すべてのタスク</option>
<option value="checked">完了したタスク</option>
<option value="unchecked">現在のタスク</option>
<option value="removed">削除済みのタスク</option>
</select>
)
}
export default Selector;
import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from 'recoil';
import { searchedTodoListSelector } from '../selectors/SearchedTodoListSelector';
import { todoListState } from '../atoms/ToDoListAtom';
import Todo from '../types/Todo';
const FilteredTodos: React.FC = () => {
const list: Todo[] = useRecoilValue(searchedTodoListSelector);
const todoList: Todo[] = useRecoilValue(todoListState);
const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);
const onChange = useCallback((id: number, checked: boolean) => {
const newTodoList = todoList.map((todo) => {
const newTodo = {...todo};
if(todo.id === id) {
newTodo.checked = !checked
}
return newTodo;
})
setTodoList(newTodoList);
}, [todoList, setTodoList]);
const onClick = useCallback((id: number, removed: boolean) => {
const newTodoList = todoList.map((todo) => {
const newTodo = {...todo}
if(todo.id === id) {
newTodo.removed = !removed
}
return newTodo;
})
setTodoList(newTodoList);
}, [todoList, setTodoList]);
return (
<div className="align-lists">
<ul>
{
list.map((todo: Todo) => {
return (
<li key={todo.id}>
<input
type="checkbox"
disabled={todo.removed}
checked={todo.checked}
onChange={() => onChange(todo.id, todo.checked)}
/>
<input
className="text"
type="text"
disabled={todo.checked || todo.removed}
value={todo.value}
/>
<button
className="button"
onClick={() => onClick(todo.id, todo.removed)}
>
{todo.removed ? '復元' : '削除'}
</button>
</li>
);
})
}
</ul>
</div>
)
}
export default FilteredTodos;
import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { todoListState } from "../atoms/ToDoListAtom";
import Todo from "../types/Todo";
const EmptyButton: React.FC = () => {
const todoList: Todo[] = useRecoilValue(todoListState);
const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);
const onClick = useCallback(() => {
const newTodos = todoList.filter((todo) => !todo.removed);
setTodoList(newTodos);
}, [setTodoList]);
return (
<button className="empty" onClick={onClick}>
ごみ箱を空にする
</button>
)
}
export default EmptyButton;
atom
ここでstateを管理する
atom は key とデフォルトの値を持つ
import { atom } from "recoil";
export const todoTitleFormState = atom<string>({
key: "todoTitleForm",
default: '',
});
import { atom } from "recoil";
export const searchTextFormState = atom<string>({
key: "searchTextForm",
default: '',
});
import { atom } from "recoil";
import Todo from '../types/Todo';
export const todoListState = atom<Todo[]>({
key: "todos",
default: [],
});
selector
atomの値を加工して返す時に使う。
ここでは、selector の値によって、タスクのリストをフィルタして返している。
atom と同じく、useRecoilValue を利用して値を取得できる。
import selector from 'selector';
import { useRecoilValue } from 'recoil';
const list = useRecoilValue(selector);
ここでは、selectorの値をatomから取得して、これを元にタスクのリストをフィルタして返している。
import { selector } from 'recoil';
import { todoListState } from '../atoms/ToDoListAtom';
import { searchTextFormState } from '../atoms/SearchToDoFormAtom';
import Todo from '../types/Todo';
export const searchedTodoListSelector = selector<Todo[]>({
key: "searchedTodoListSelector",
get: ({ get }) => {
const todoList: Todo[] = get(todoListState);
const filter: string = get(searchTextFormState);
return todoList.filter((todo) => {
switch (filter) {
case 'all':
return !todo.removed;
case 'checked':
return !todo.removed && todo.checked;
case 'unchecked':
return !todo.removed && !todo.checked;
case 'removed':
return todo.removed;
default:
return todo;
}
})
},
});
それぞれの使い所
useState vs useReducer
本質的には同等。コンポーネントにステートを持たせるのが役割。
useReducerは、複数の関連するステート(オブジェクト)を扱うときが使い所。
useStateではステートを更新する際に更新するための関数を呼び出すのに対して、useReducerでは定義したreducerにメッセージを送るだけなのでシンプルになりやすい。
ユーザーデータをAPIから取得してきて一覧表示する例
export function App() {
const [{loading, users}, setState] = useState({loading: false, users: []})
return (
{loading? (
<p>ロード中</p>
):(
<ul>
{users.map((user) => {
<li key={user.id}>{user.name}</li>>}
)}
</ul>
)}
<button onClick={async() => {
setState((state) => ({...state, loading: true}))
const {users} = await fakeApi()
setState((state) => ({...state, loading: false, users})
}}>
データを取得
</button>
)
}
export function App() {
const reducer = (state, action) => {
switch(action.type) {
case '':
return {...state, loading: true}
case '':
const {users} = action.payload;
return {...state, loading: false, users}
default:
return state;
};
};
const [{loading, users}, dispatch] = useReducer(reducer, {loading: false, users: []}
return (
<div>
{loading? (
<p>ロード中</p>
):(
<ul>
{users.map((user) => {
<li key={user.id}>{user.name}</li>}
)}
</ul>
)}
<button onClick={async() => {
dispatch({type: 'pending'})
const {users} = await fakeAPI()
dispatch({type: 'done', payload: {users}})
}}>
データを取得
</button>
</div>
)
}