つくるもの
ほぼチュートリアル通りのあいつです。
要件は以下のような感じです。
・タスクを登録することができる
・左チェックボックスで完了にすることができる
・右の削除ボタンで削除ができる
・チェックしたタスクが完了したタスクに表示される
・チェックしていないタスクが現在のタスクに表示される
・削除したタスクが削除済みのタスクに表示される
・削除したタスクを復元することができる
・削除済みのリストで「ゴミ箱を空にする」を押すと削除済みタスクが抹消される
管理される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>
  )
}

