2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TodoアプリをReact Typescript redux-toolkit chakra-uiで作成

2
Last updated at Posted at 2021-08-03

この記事の概要

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の型を定義

types.ts
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の初期値を定義

taskSlice.ts
export const initialState: TaskState = {
  tasks: [],
  editedTask: { id: 0, title: "", completed: false },
  taskCount: 0,
};

editedTaskのidが0の場合はtaskの新規作成となるよう実装します。

createSlice

taskSlice.ts
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

taskSlice.ts
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

store.ts
export const store = configureStore({
  reducer: {
    task: taskReducer,
  },
});

/src/index.tsをでreduxの状態を保持するためのProviderとchakra-uiを利用するためのChakraProviderでコンポーネントをラッピング

index.tsx
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <ChakraProvider>
        <App />
      </ChakraProvider>
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

これで状態管理の設定は完了です。
この時点でRedux devtoolsはこのようになっています。
スクリーンショット 2021-08-03 16.08.00.png

3.各コンポーネントの作成

Appコンポーネント

App.tsx
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コンポーネント

Header.tsx
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コンポーネント

TaskInput.tsx
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コンポーネント

TaskList.tsx
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コンポーネント

TaskInput.tsx
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アプリの完成です。

4.完成品

新規作成

task_add.gif

更新

task_update.gif

削除

task_delete.gif

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?