用 React hooks 實作一個 todo list


Posted by Christy on 2021-12-01

本文為實作 todo list 新增、刪除、標記完成/未完成、清空、篩選功能,用的是 React hooks,時空背景是在還沒學到 React Router 與 Redux 之前

基礎設定從這裡開始 來學 React 吧之一_以 todo list 為例學會 React 基礎與 useState 介紹

1. 基本設定

a. 建了另一個新的資料夾,裝好環境以後,記得要先安裝 styled-components 才可以用。

$npm install --save styled-components

b. 想要用 ESlint prettier 要先安裝 $npm install --save-dev prettier

這裡的錯誤訊息會是:Cannot find module 'prettier'

先安裝好以後,如果還有錯誤訊息,可以先參考 ESlint prettier 在 VSCode 的指南,把該裝的都裝一裝,接著重開 VSCode,應該會有用。

2. 實作相關

a. 遇到的問題

好吧我暫時把檔案整理好了,把 component 包起來,讓我的檔案有點混亂,但是先這樣吧

a.1 當我輸入 todo 並按下新增時,輸入框沒有自動清空,因為我的 input 的 component 沒有傳 props value={value}

a.2 當我按下 delete button 時,沒有刪除 todo,原因是 btn 要 onClick 而不是傳 props

a.3 忘記 props 的 style 怎麼寫了

  const TodoContent = styled.div`
    ${(props) =>
      props.$isDone &&
      `
      text-decoration: line-through;
    `}
  `;

a.4 todo list 的篩選功能怎麼做?

一開始按照之前的邏輯實作,發現按了完成再按已完成,篩選器就壞了,但是網路上大部分的參考都是用 redux 做的,因此參考了老師及同學的範例

重點在於篩選要產生一個新的 state,而不是去改原本的 todos

要先有 [filter, setFilter] = useState();

概念可以參考 W21 + W22_React 學習過程筆記 提到的 W22 隨意聊內容

// 關鍵程式碼
{todos
  .filter((todo) => {
    if (filter === "all") return todo;
    return filter === "done" ? todo.isDone : !todo.isDone;
  })
  .map((todo) => (
    <TodoItem
      key={todo.id}
      handleDeleteTodo={handleDeleteTodo}
      handleTodoIsDone={handleTodoIsDone}
      todo={todo}
    />
))}

a.5 所有程式碼

分成 index.js, App.js, TodoContainer.js, TodoItem.js

TodoContainer 是整個 todo,主要寫邏輯用的;TodoItem把 todo 的內容獨立出來

// index.js

import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));
// App.js
import TodoContainer from "./components/TodoContainer";

export default function App() {
  return (
    <div className="App">
      <TodoContainer />
    </div>
  );
}
// TodoContainer.js
import React, { useState, useRef, useCallback } from "react";
import TodoItem from "./TodoItem";
import styled from "styled-components";

const TodoWrapper = styled.div`
  font-family: "ubuntu";
  margin: 30px auto;
  width: 480px;
  border: 3px solid #f5f5f5;
  border-radius: 5px;
  padding: 30px;
  text-align: center;
  box-shadow: 3px 3px 5px #ccc;
`;

const Title = styled.h1`
  color: #28262c;
  font-size: 48px;
`;

const CreateTodo = styled.div`
  margin: 20px auto;
`;

const TodoInput = styled.input`
  margin-right: 10px;
  width: 300px;
  height: 24px;
  border: 1px solid #ccc;
  border-radius: 5px;
  box-shadow: 1px 1px 3px #ccc;
`;

const AddButton = styled.button`
  font-family: "ubuntu";
  width: 80px;
  background: black;
  color: white;
  border-radius: 3px;
  box-shadow: 1px 1px 3px #666;
  border: none;
  padding: 5px;
`;

const SelectTodo = styled.div``;

const AllButton = styled.button`
  font-family: "ubuntu";
  width: 80px;
  background: #39393a;
  color: white;
  border-radius: 3px;
  box-shadow: 1px 1px 3px #666;
  border: none;
  padding: 5px;
`;

const ActiveButton = styled.button`
  font-family: "ubuntu";
  width: 80px;
  background: #ffe74c;
  color: #333;
  border-radius: 3px;
  box-shadow: 1px 1px 3px #666;
  border: none;
  padding: 5px;
  margin: 0 10px;
`;

const CompletedButton = styled.button`
  font-family: "ubuntu";
  width: 80px;
  background: #666;
  color: white;
  border-radius: 3px;
  box-shadow: 1px 1px 3px #666;
  border: none;
  padding: 5px;
`;

const TodoList = styled.div`
  margin-top: 10px;
`;

const ClearTodo = styled.button`
  font-family: "ubuntu";
  width: 120px;
  background: #c8553d;
  color: white;
  border-radius: 3px;
  box-shadow: 1px 1px 3px #666;
  border: none;
  padding: 5px;
  margin: 10px;
`;

export default function TodoContainer() {
  const id = useRef(1);
  const [todos, setTodos] = useState([]);
  const [value, setValue] = useState("");
  const [filter, setFilter] = useState("all");

  const handleAddTodo = useCallback(() => {
    if (!value) return alert("wanna type something?");
    setTodos([{ id: id.current, content: value }, ...todos]);
    setValue("");
    id.current++;
  }, [todos, value]);

  const handleInputChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);

  const handleDeleteTodo = useCallback(
    (id) => {
      setTodos(todos.filter((todo) => todo.id !== id));
    },
    [todos]
  );

  const handleTodoIsDone = useCallback(
    (id) => {
      setTodos(
        todos.map((todo) => {
          if (todo.id !== id) return todo;
          return {
            ...todo,
            isDone: !todo.isDone,
          };
        })
      );
    },
    [todos]
  );

  const handleTodoClear = useCallback(() => {
    setTodos(todos.filter((todo) => todo.isDone !== true));
  }, [todos]);

  const filterAll = useCallback(() => {
    setFilter("all");
  }, []);

  const filterDone = useCallback(() => {
    setFilter("done");
  }, []);

  const filterUndone = useCallback(() => {
    setFilter("undone");
  }, []);

  return (
    <TodoWrapper>
      <Title>Todo List</Title>
      <CreateTodo>
        <TodoInput value={value} onChange={handleInputChange}></TodoInput>
        <AddButton onClick={handleAddTodo}>Add Todo</AddButton>
      </CreateTodo>
      <SelectTodo>
        <AllButton onClick={filterAll}>All</AllButton>
        <ActiveButton onClick={filterUndone}>Active</ActiveButton>
        <CompletedButton onClick={filterDone}>Completed</CompletedButton>
      </SelectTodo>
      <TodoList>
        {todos
          .filter((todo) => {
            if (filter === "all") return todo;
            return filter === "done" ? todo.isDone : !todo.isDone;
          })
          .map((todo) => (
            <TodoItem
              key={todo.id}
              handleDeleteTodo={handleDeleteTodo}
              handleTodoIsDone={handleTodoIsDone}
              todo={todo}
            />
          ))}
      </TodoList>
      <ClearTodo onClick={handleTodoClear}>Clear Completed</ClearTodo>
    </TodoWrapper>
  );
}
// TodoItem.js
import styled from "styled-components";
import { memo } from "react";

const Todo = styled.div`
  border: 1px solid #ccc;
  & + & {
    margin-top: 10px;
  }
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-radius: 5px;
  box-shadow: 1px 1px 3px #ccc;
`;

const TodoContent = styled.div`
  ${(props) =>
    props.$isDone &&
    `
    text-decoration: line-through;
  `}
`;

const DeleteButton = styled.button`
  font-family: "ubuntu";
  margin-right: 10px;
  width: 80px;
  background: #c8553d;
  color: white;
  border-radius: 3px;
  border: none;
  box-shadow: 1px 1px 3px #666;
  padding: 5px;
`;

const CheckButton = styled.button`
  font-family: "ubuntu";
  width: 80px;
  background: #73a6ad;
  color: white;
  border-radius: 3px;
  border: none;
  box-shadow: 1px 1px 3px #666;
  padding: 5px;
`;

const ButtonWrapper = styled.div`
  display: flex;
  justify-content: space-between;
`;

function TodoItem({ todo, handleDeleteTodo, handleTodoIsDone }) {
  const handleDeleteClick = () => {
    handleDeleteTodo(todo.id);
  };

  const handleIsDoneClick = () => {
    handleTodoIsDone(todo.id);
  };

  return (
    <Todo data-todo-id={todo.id}>
      <TodoContent $isDone={todo.isDone}>{todo.content}</TodoContent>
      <ButtonWrapper>
        <DeleteButton onClick={handleDeleteClick}>Delete</DeleteButton>
        <CheckButton onClick={handleIsDoneClick}>
          {todo.isDone ? "Undone" : "Done"}
        </CheckButton>
      </ButtonWrapper>
    </Todo>
  );
}

export default memo(TodoItem);









Related Posts

Day 17-OOP & Quiz Game

Day 17-OOP & Quiz Game

腳踏實地往前走 — 團隊的可執行事項

腳踏實地往前走 — 團隊的可執行事項

讓你好像很厲害的Command Line

讓你好像很厲害的Command Line


Comments