この記事の概要
React Typescript redux-toolkit chakra-uiを用いTodoアプリを実装してみました。
Source code https://github.com/ryota-ak/todo_app_chakra
実行環境
- Node.js 14.15.1
- React 17.0.2
- typescript 4.1.5
ディレクトリ構成
todoapp
├── node_modules
├── public
├── src
│ ├── App.tsx
│ ├── app
│ │ ├── hooks.ts
│ │ └── store.ts
│ ├── component
│ │ └── Header.tsx
│ ├── features
│ │ └── task
│ │ ├── TaskInput.tsx
│ │ ├── TaskItem.tsx
│ │ ├── TaskList.tsx
│ │ ├── taskSlice.ts
│ │ └── types.ts
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── setupTests.ts
├── tsconfig.json
├── package.json
└── yarn.lock
1.環境構築
create-react-app
$ npx create-react-app todoapp --template redux-typescript
redux-typescriptをテンプレートに指定し、Reactの雛形ファイルをインストールします。
必要なパッケージをインストール
chakra-ui
$ yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
UIはchakra-uiを利用します。
chakra-ui関連のパッケージをインストールします。また、chakra-ui/iconsも使いたいのでインストールしておきます。
$ yarn add @chakra-ui/icons
2.taskSliceを作成
状態管理にはredux-toolkitを利用します。createSlice関数を用いてTaskに関するstoreを作成します。
stateの型を定義
export interface Task {
id: number;
title: string;
completed: boolean;
}
export interface TaskState {
tasks: Task[];
editedTask: Task;
taskCount: number;
}
Reduxで管理するStateの型です。
- tasks => Task型の配列
- editedTask => 編集中のtask
- taskCount => taskを新規作成した時にidを一意に割り当てるための数値
Stateの初期値を定義
export const initialState: TaskState = {
tasks: [],
editedTask: { id: 0, title: "", completed: false },
taskCount: 0,
};
editedTaskのidが0の場合はtaskの新規作成となるよう実装します。
createSlice
export const taskSlice = createSlice({
name: "task",
initialState,
reducers: {
addTask: (state, action: PayloadAction<string>) => {
const newTask = {
id: state.taskCount + 1,
title: action.payload,
completed: false,
};
state.tasks = [...state.tasks, newTask];
state.taskCount = state.taskCount + 1;
state.editedTask = initialState.editedTask;
},
updateTask: (state, action: PayloadAction<Task>) => {
state.tasks = state.tasks.map((task) =>
task.id === action.payload.id ? action.payload : task
);
state.editedTask = initialState.editedTask;
},
deleteTask: (state, action: PayloadAction<Task>) => {
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
},
editTask(state, action: PayloadAction<Task>) {
state.editedTask = action.payload;
},
},
});
createSlice関数を用いてtaskSliceを作成します。
reducersの中にstateを更新するためのactionを定義しています。
- addTask => tasksに新たなtaskを追加する関数
- updateTask => tasksの中で指定したtaskを更新する関数
- deleteTask => tasksの中で指定したtaskを消去する関数
- editTask => 編集中のtaskであることを示すeditedTaskを更新する関数
必要なものをexport
export const { addTask, updateTask, editTask, deleteTask } = taskSlice.actions;
export const selectTasks = (state: RootState) => state.task.tasks;
export const selectEditedTask = (state: RootState) => state.task.editedTask;
export default taskSlice.reducer;
コンポーネントで使うactionやselectorをexportしておきます。
storeを作成、ProviderでWrap
export const store = configureStore({
reducer: {
task: taskReducer,
},
});
/src/index.tsをでreduxの状態を保持するためのProviderとchakra-uiを利用するためのChakraProviderでコンポーネントをラッピング
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<ChakraProvider>
<App />
</ChakraProvider>
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
これで状態管理の設定は完了です。
この時点でRedux devtoolsはこのようになっています。

3.各コンポーネントの作成
Appコンポーネント
const App: VFC = () => {
return (
<>
<Box bg="gray.100" w="100vw" h="100vh">
<Box w={800} mx="auto" py={10}>
<Flex direction="column">
<Header />
<TaskInput />
<TaskList />
</Flex>
</Box>
</Box>
</>
);
};
後述するHeader,TaskInput,TaskListコンポーネントを縦に並べました。
Headerコンポーネント
const Header = () => {
const dispatch = useAppDispatch();
return (
<Heading
textAlign="center"
color="orange.300"
cursor="pointer"
onClick={() => dispatch(editTask(initialState.editedTask))}
>
TodoApp
</Heading>
);
};
Headerにはアプリのタイトルとして、TodoApp表示します。また、クリックすると編集中のtaskが初期値となるよう実装しました。
TaskInputコンポーネント
const TaskInput: VFC = () => {
const dispatch = useAppDispatch();
const editedTask = useAppSelector(selectEditedTask);
const isDisabled = editedTask.title.length === 0;
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(editTask({ ...editedTask, title: e.target.value }));
};
const handleOnKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !isDisabled) {
e.preventDefault();
editedTask.id === 0 ? addClicked() : updateClicked();
}
};
const addClicked = () => {
dispatch(addTask(editedTask.title));
};
const updateClicked = () => {
dispatch(updateTask(editedTask));
};
return (
<Box mt={5} textAlign="center">
<Flex>
<Input
mr={5}
bg="white"
placeholder="task"
type="text"
value={editedTask.title}
onChange={handleInputChange}
onKeyPress={handleOnKeyPress}
/>
{editedTask.id === 0 ? (
<Button
type="submit"
colorScheme="yellow"
variant="solid"
cursor="pointer"
leftIcon={<AddIcon />}
disabled={isDisabled}
onClick={addClicked}
>
ADD
</Button>
) : (
<Button
type="submit"
colorScheme="yellow"
variant="solid"
cursor="pointer"
disabled={isDisabled}
onClick={updateClicked}
>
UPDATE
</Button>
)}
</Flex>
</Box>
);
};
TaskInputコンポーネントでは、taskを新規作成または更新するためのロジックを実装しました。editedTask.idが初期値か否かによって、作成か更新かを分岐させています。
TaskListコンポーネント
const TaskList: VFC = () => {
const tasks = useAppSelector(selectTasks);
return (
<Box bg="white" mt={5} maxH={500} overflowY="scroll">
{tasks.length > 0 &&
tasks.map((task) => <TaskItem key={task.id} task={task} />)}
</Box>
);
};
taskの一覧を表示するコンポーネントです。map関数を使い、TaskItemコンポーネントにpropsとしてtaskを渡しています。
TaskItemコンポーネント
type Props = {
task: Task;
};
const TaskItem: VFC<Props> = memo(({ task }) => {
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
const handleDelete = () => {
dispatch(deleteTask(task));
dispatch(editTask(initialState.editedTask));
onClose();
};
return (
<>
<Box p={2} borderBottom="1px" borderColor="gray.200" h={12}>
<Flex>
<Checkbox
mr={4}
pb={2}
colorScheme="red"
checked={task.completed}
onChange={() => {
dispatch(updateTask({ ...task, completed: !task.completed }));
}}
/>
<Box pt={1} textDecoration={task.completed ? "line-through" : "none"}>
{task.title}
</Box>
<Spacer />
<IconButton
aria-label="Search database"
colorScheme="linkedin"
variant="outline"
mr={2}
size="sm"
icon={<EditIcon />}
onClick={() => dispatch(editTask(task))}
/>
<IconButton
aria-label="Search database"
colorScheme="red"
variant="outline"
size="sm"
icon={<DeleteIcon />}
onClick={onOpen}
/>
</Flex>
</Box>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalBody textAlign="center" mt={2} fontWeight="bold">
Do you delete task really?
</ModalBody>
<ModalFooter>
<Button mr={3} colorScheme="red" onClick={handleDelete}>
Yes
</Button>
<Button colorScheme="blue" onClick={onClose}>
No
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
});
taskを表示するコンポーネントです。
左端のcheckboxでtaskが完了したかを管理しています。
次にtaskのタイトルを表示し、右端にはicon属性に@chakra-ui/iconsからimportした<EditIcon/>と<DeleteIcon/>を指定した二つの<IconButton>を配置しています。
<EditIcon/>をクリックするとこのコンポーネント内のtaskを編集中のtaskに指定することができます。
<DeleteIcon/>をクリックするとModalが立ち上がり、taskを消去することができます。
また、このコンポーネントはmemo化し不要な再描画を減らしています。
以上で、Todoアプリの完成です。


