本文為實作 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);