本篇將使用 React 完成一個 CRUD 專案,會用到 useContext 及套件 React-router-dom、uuid、bootstrap、reactstrap。
*CRUD 新增/修改/刪除/查詢,各種程式最重要的四大功能。
前製作業 preparation
- 新增專案
npx create-react-app react_crud
- 在 github 開一個新 repo
- 在 vscode 開啟 react_crud 專案
- 整理 src 資料夾,只保留 App.css / App.js / index.css / index.js
- 調整 App.js / index.js 如下
- 打開終端機,啟動專案
yarn start
上述步驟都完成的話應該會在網頁(http://localhost:3000/)上看到:
然後,也是很重要的一步驟,記得先將目前的檔案 push 至 github 上。
接著就準備進入專案本身的 coding 啦 ٩(ˊᗜˋ )و
寫 code! Step-By-Step Coding
會分為兩個部份,第一部份是很簡易版本,不用 router、不用 useContext 、不調整 css樣式、不元件化,基本的增改刪查。
第二趴進階部份會將 router 、 useContext 、bootstrap 結合到專案中,並將元件拆分。
基礎版
一、新增/顯示
- 先安裝本專案需要用到的套件們,在終端機下指令:
yarn add bootstrap react-router-dom reactstrap uuid
- 在 App.js 中,新增 div 及標題 (新增/查看使用者)
這時候網頁上會看到(記得 yarn start
將專案 run 起來)
- 引入 useState
import React, { useState } from "react";
- 定義一個 User 預設值
const usersData = [
{ id: 1, name: "Tania", username: "floppydiskette" },
{ id: 2, name: "Craig", username: "siliconeidolon" },
{ id: 3, name: "Ben", username: "benisphere" },
];
- useState
const [users, setUsers] = useState(usersData);
- 在 App.js 中新增一個顯示 User 的元件
const UserTable = (props) => (
<table>
<thead>
<tr>
<th>Name</th>
<th>Username</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name data</td>
<td>Username data</td>
<td>
<button className="button muted-button">Edit</button>
<button className="button muted-button">Delete</button>
</td>
</tr>
</tbody>
</table>
);
並在 App 元件中使用 UserTable
- 因為我們在 UserTable 把資料寫死,所以不會看到我們預設的 users ,故,需要把 props 傳進去 UserTable 裡面,code 會變成這樣:
const UserTable = (props) => (
<table>
<thead>
<tr>
<th>Name</th>
<th>Username</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{props.users.length > 0 ? (
props.users.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.username}</td>
<td>
<button>
Edit
</button>
<button>
Delete
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={3}>No users</td>
</tr>
)}
</tbody>
</table>
);
- 接著再新增一個元件 AddUserForm,用來新增使用者,並在 App 中新增一個 function
const addUser = (user) => {
user.id = users.length + 1;
setUsers([...users, user]);
};
- AddUserForm
const AddUserForm = (props) => {
return (
<form>
<label>Name</label>
<input type="text" name="name" value="" />
<label>Username</label>
<input type="text" name="username" value="" />
<button>Add new user</button>
</form>
);
};
- App 元件中引用
<div>
<h2>Add user</h2>
<AddUserForm addUser={addUser} />
</div>
- 然後在 AddUserForm 給定一個預設值
const initialFormState = { id: null, name: '', username: '' }
const [user, setUser] = useState(initialFormState)
- 改變 value 時要異動的部份
const handleInputChange = (event) => {
const { name, value } = event.target
setUser({ ...user, [name]: value })
}
- AddUserForm 所有的 code(onSubmit 為送出時要處理的事件)
const AddUserForm = (props) => {
const initialFormState = { id: null, name: "", username: "" };
const [user, setUser] = useState(initialFormState);
const handleInputChange = (event) => {
const { name, value } = event.target;
setUser({ ...user, [name]: value });
};
return (
<form
onSubmit={(event) => {
event.preventDefault();
if (!user.name || !user.username) return;
props.addUser(user);
setUser(initialFormState);
}}
>
<label>Name</label>
<input
type="text"
name="name"
value={user.name}
onChange={handleInputChange}
/>
<label>Username</label>
<input
type="text"
name="username"
value={user.username}
onChange={handleInputChange}
/>
<button>Add new user</button>
</form>
);
};
沒錯,我們已經完成新增的功能了!
二、刪除
- 在 App.js 中新增一個 function
const deleteUser = (id) => {
setUsers(users.filter((user) => user.id !== id));
};
// 用 filter 過濾掉當前的 id
- 把刪除功能當作 props 傳給 UserTable
<UserTable users={users} deleteUser={deleteUser} />
- UserTable 中的 Delete 按鈕綁定 onClick
<button onClick={() => props.deleteUser(user.id)}>
Delete
</button>
對,刪除就是這樣,完成!
三、改/更新
- 在 App 元件中新增以下的 code
- editing 是否正在編輯 / initialFormState 初始值 / currentUser 目前選定的 user / 點了 edit 後就呼叫 editRow 這個function / 點了更新就呼叫 updateUser
const [editing, setEditing] = useState(false);
const initialFormState = { id: null, name: "", username: "" };
const [currentUser, setCurrentUser] = useState(initialFormState);
const editRow = (user) => {
setEditing(true);
setCurrentUser({
id: user.id,
name: user.name,
username: user.username,
});
};
const updateUser = (id, updatedUser) => {
setEditing(false);
setUsers(users.map((user) => (user.id === id ? updatedUser : user)));
};
摁~看起來是完成了,
BUT!當我們選定一個 user 後在點擇其他 user 編輯,就會卡死不動,為了解決這個問題我們需要使用 useEffect 。
- 在 EditUserForm 中新增這段,監聽 props 的變動。
useEffect(() => {
setUser(props.currentUser);
}, [props]);
完整的扣放在 gist 上,於本篇最下方!
完成 ٩(ˊᗜˋ )و
進階
- 新增資料夾 components
- 新增資料夾 context
- 清空 App.js 重新開始
- 在 components 底下新增 AddUser.js 、EditUser.js、Heading.js、Home.js、UserList.js ,並且在檔案中下 rfce 指令
- 到空空的 App.js 中 import 這些檔案
import React, { useState, useEffect } from "react";
import "./App.css";
import Home from "./components/Home";
import AddUser from "./components/AddUser";
import EditUser from "./components/EditUser";function App() {
return (
<div className="App">
<Home />
<AddUser />
<EditUser />
</div>
);
}export default App;
- 將 React-router-dom 會用到的函數 import 進來,並包一包
import React, { useState, useEffect } from "react";
import "./App.css";
import Home from "./components/Home";
import AddUser from "./components/AddUser";
import EditUser from "./components/EditUser";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";function App() {
return (
<div className="App">
<Router>
<h1>Nav</h1>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/add" element={<AddUser />} />
<Route path="/edit/:id" element={<EditUser />} />
</Routes>
</Router>
</div>
);
}export default App;
現在在瀏覽器輸入 http://localhost:3000/add ,會到不同頁面啦~
- 完成 router 設定後,開始處理畫面的部份,一樣在 App.js 中
import "bootstrap/dist/css/bootstrap.min.css";
- 切換到 /add 的頁面,先處理新增使用者的畫面 AddUser.js
import React from "react";
import { Form, FormGroup, Label, Input, Button } from "reactstrap";
import { Link } from "react-router-dom";function AddUser() {
return (
<Form>
<FormGroup>
<Label>Name</Label>
<Input type="text" placeholder="Enter name"></Input>
</FormGroup>
<Button type="submit">Submit</Button>
<Link to="/" className="btn btn-danger ml-2">
Cancel
</Link>
</Form>
);
}export default AddUser;
- 修改的畫面跟新增一樣,到 /edit/a 的頁面,處理 EditUser.js
import React from "react";
import { Form, FormGroup, Label, Input, Button } from "reactstrap";
import { Link } from "react-router-dom";function AddUser() {
return (
<Form>
<FormGroup>
<Label>Name</Label>
<Input type="text" placeholder="Enter name"></Input>
</FormGroup>
<Button type="submit">Submit</Button>
<Link to="/" className="btn btn-danger ml-2">
Cancel
</Link>
</Form>
);
}export default AddUser;
- 到 Home.js 中,import Heading 跟 UserList
import React from "react";
import Heading from "./Heading";
import UserList from "./UserList";function Home() {
return (
<>
<Heading />
<UserList />
</>
);
}export default Home;
- Heading.js
import React from "react";
import { Link } from "react-router-dom";
import { Navbar, Nav, NavItem, NavbarBrand, Container } from "reactstrap";
function Heading() {
return (
<Navbar color="dark" dark>
<Container>
<NavbarBrand href="/">My Team</NavbarBrand>
<Nav>
<NavItem>
<Link className="btn btn-primary" to="/add">
Add User
</Link>
</NavItem>
</Nav>
</Container>
</Navbar>
);
}export default Heading;
- UserList.js
import React from "react";
import { Link } from "react-router-dom";
import { ListGroup, ListGroupItem, Button } from "reactstrap";function UserList() {
return (
<ListGroup className="mt-4">
<ListGroupItem className="d-flex">
<strong>User One</strong>
<div style={{ marginLeft: "auto" }}>
<Link className="btn btn-warning" to="/edit/1">
Edit
</Link>
<Button color="danger" style={{ marginLeft: "10px" }}>
Delect
</Button>
</div>
</ListGroupItem>
</ListGroup>
);
}export default UserList;
終於!畫面部份完成了,接著就是實做功能的部份。(累)
功能實做
- 新增資料夾 context 並新增兩個檔案 AppReducer.js GlobalState.js,用來使用 useContext
- GlobalState.js
import React, { createContext, useReducer } from "react";
import AppReducer from "./AppReducer";// initconst initialState = {
users: [
{ id: 1, name: "User One" },
{ id: 2, name: "User Two" },
{ id: 3, name: "User Three" },
],
};// createexport const GlobalContext = createContext(initialState);// providerexport const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
return (
<GlobalContext.Provider value={{ users: state.users }}>
{children}
</GlobalContext.Provider>
);
};
- AppReducer.js
export default (state, action) => {
switch (action.type) {
default:
return state;
}
};
- 在 App.js 中,使用
import React, { useState, useEffect } from "react";
import "./App.css";
import Home from "./components/Home";
import AddUser from "./components/AddUser";
import EditUser from "./components/EditUser";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { GlobalProvider } from " ./context/GlobalState";
import "bootstrap/dist/css/bootstrap.min.css";function App() {
return (
<div style={{ maxWidth: "30rem", margin: "4rem auto" }}>
<GlobalProvider>
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/add" element={<AddUser />} />
<Route path="/edit/:id" element={<EditUser />} />
</Routes>
</Router>
</GlobalProvider>
</div>
);
}export default App;
- 到 UserList.js 中使用它
import { GlobalContext } from "../context/GlobalState";
// 其他省略
const { users } = useContext(GlobalContext);
console.log(users);
// 其他省略...
查看功能
- 有了資料後就是讓畫面呈現,使用 map 跑出資料
<ListGroup className="mt-4">
{users.map((user) => (
<ListGroupItem className="d-flex" key={user.id}>
<strong>{user.name}</strong>
<div style={{ marginLeft: "auto" }}>
<Link
className="btn btn-warning"
to={`/edit/${user.id}`}
>
Edit
</Link>
<Button color="danger" style={{ marginLeft: "10px" }}>
Delect
</Button>
</div>
</ListGroupItem>
))}
</ListGroup>
刪除功能
- GlobalProvider 裡面新增一個 function 並傳出去
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);const removeUser = (id) => {
dispatch({ type: "REMOVE_USER", payload: id });
};return (
<GlobalContext.Provider value={{ users: state.users, removeUser }}>
{children}
</GlobalContext.Provider>
);
};
- UserList 中使用它
const { users, removeUser } = useContext(GlobalContext);
// 省略..
<Button
color="danger"
style={{ marginLeft: "10px" }}
onClick={() => removeUser(user.id)}
>
Delect
</Button>
耶思,刪除功能也完成了!
接著做新增 user 的功能
- 在 GlobalState.js 裡面 GlobalProvider 中新增
const addUser = (user) => {
dispatch({ type: "ADD_USER", payload: user });
};
// ...
<GlobalContext.Provider
value={{ users: state.users, removeUser, addUser }}
>
- 到 AppReducer.js 中新增 case
const AppReducer = (state, action) => {
switch (action.type) {
case "REMOVE_USER":
return {
users: state.users.filter((user) => {
return user.id !== action.payload;
}),
};
case "ADD_USER":
return {
users: [action.payload, ...state.users],
};
default:
return state;
}
};export default AppReducer;
- 至 AddUser.js 中使用 useContext
import React, { useContext } from "react";
import { GlobalContext } from "../context/GlobalState";
// ...
const { addUser } = useContext(GlobalContext);
- 點擊送出後要回到首頁,我們使用 react-router-dom 的 useNavigate
*舊一點的版本會使用 useHistory,但新版已經不支援,會跳error:Attempted import error: ‘useHistory’ is not exported from ‘react-router-dom’ ,只要將 useHistory 改成 useNavigate 就可以了!
import React, { useContext } from "react";
import { Form, FormGroup, Label, Input, Button } from "reactstrap";
import { Link, useNavigate } from "react-router-dom";
import { GlobalContext } from "../context/GlobalState";
function AddUser() {
const { addUser } = useContext(GlobalContext);
const navigate = useNavigate();
const onSubmit = () => {
navigate("/");
};
// 以下省略
- 繼續完成 onSubmit 送出的功能
const onSubmit = () => {
const newUser = {
id: 4,
name: "User Four",
};
addUser(newUser);
navigate("/");
};
- 看起來正常後就把值帶入
import { v4 as uuid } from "uuid";// ...
const [name, setName] = useState("");
const onSubmit = () => {
const newUser = {
id: uuid(),
name: name,
};
addUser(newUser);
navigate("/");
};
const onChange = (e) => {
setName(e.target.value);
};// ...
<Input
type="text"
value={name}
onChange={onChange}
placeholder="Enter name"
></Input>
新增完成!
編輯功能
- 一樣在 GlobalState 中新增
const editUser = (user) => {
dispatch({
type: "EDIT_USER",
payload: user,
});
};
- AppReducer.js 中增加 case
case "EDIT_USER":
// 選定的 user
const updateUser = action.payload;
// 更新後的所有 user
const updateUsers = state.users.map((user) => {
if (user.id === updateUser.id) {
return updateUser;
}
return user;
});
return {
users: updateUsers,
};
- EditUser.js
import React, { useState, useContext, useEffect } from "react";
import { Form, FormGroup, Label, Input, Button } from "reactstrap";
import { Link, useNavigate, useParams } from "react-router-dom";
import { GlobalContext } from "../context/GlobalState";function EditUser() {
const { users, editUser } = useContext(GlobalContext);
const navigate = useNavigate();const { id } = useParams();
const [selectedUser, setSelectedUser] = useState({
id: "",
name: "",
});useEffect(() => {
const userId = id;
const selectedUser = users.find((user) => user.id === userId);
setSelectedUser(selectedUser);
}, [id, users]);
const onSubmit = () => {
editUser(selectedUser);
navigate("/");
};
const onChange = (e) => {
setSelectedUser({ ...selectedUser, [e.target.name]: e.target.value });
};
return (
<Form onSubmit={onSubmit}>
<FormGroup>
<Label>Name</Label>
<Input
type="text"
name="name"
value={selectedUser.name}
onChange={onChange}
placeholder="Enter name"
></Input>
</FormGroup>
<Button type="submit">Edit Name</Button>
<Link to="/" className="btn btn-danger ml-2">
Cancel
</Link>
</Form>
);
}export default EditUser;
完成!
基礎版本的 code :
進階的 code 放在 github repo:
後記
最近的專案都不太用到這些基本功能,沒有常寫就快忘光了 Q.Q
在 repo 裡面有一個 Basic.js 的檔案,先寫了最基本的功能,其中的編輯功能想了好久 ╥﹏╥ ,然後覺得自己很爛開始懷疑自己的價值,然後覺得就繼續在這裡混吃等史好ㄌ,然後陷入悲傷的黑洞中 (´༎ຶД༎ຶ`)
總結
今天得出的一句話:腦袋要保持運轉,寫扣要保持手感!٩(๑・ิᴗ・ิ)۶٩(・ิᴗ・ิ๑)۶
完成 ٩(ˊᗜˋ )و