主包4年前端经验找工作,之前两家的技术栈都是 Vue,所以对 React 的知识水平还停留在远古的 Class Comonents 时期。出来后发现深圳的招聘市场大多数岗位都要求有 React 经验,本着框架只是语法差异的想法硬着头皮投了一些简历,结果全部都石沉大海,只能说你不会有的是人会。
所以主包准备在找工作的同时学习 React Hooks 的用法,计划从之前负责的业务后台系统的场景出发,用 React 实现常见的业务场景。本系列仅仅用来记录一下个人的思考步骤,如果有大佬恰好看到这篇文章,又恰好发现了我的错误,还请多多指正。
欢迎同样在看机会的友友们关注一下我的xhs账号:好想吃橙子。账号内容更新找工作的一些分享
前置工作
使用 npm create vite 创建一个名为 react-bootstrap 的项目,删除默认模板中的代码和样式文件,写一份符合我们要求的 db.json 当作数据来源
// db.json
{
"users": [
{
"userId": "1",
"name": "张三",
"age": 25,
"city": "深圳",
"role": "销售"
},
{
"userId": "2",
"name": "李四",
"age": 31,
"city": "广州",
"role": "销售"
},
{
"userId": "3",
"name": "王五",
"age": 29,
"city": "珠海",
"role": "销售"
},
{
"userId": "4",
"name": "赵六",
"age": 28,
"city": "广州",
"role": "销售经理"
}
]
}
编码流程
1.初始化 App
我们使用 table 组件来实现表格数据,根据我们前面定义的数据来初始化这个 table
// App.tsx
function App() {
const [data, setData] = useState<User[]>(sourceData.users);
return (
<div>
<table border={1}>
<thead>
<tr>
<th>用户id</th>
<th>姓名</th>
<th>年龄</th>
<th>所在地</th>
<th>角色名称</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{data.map((item) => {
return (
<tr key={item.userId}>
<td>{item.userId}</td>
<td>{item.name}</td>
<td>{item.age}</td>
<td>{item.city}</td>
<td>{item.role}</td>
<td>
<button>删除</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
2.删除
为列表添加“删除”按钮:点击删除按钮,删除对应的项;
// App.tsx
//...
const handleDelete = (targetId: string) => {
setData(data.filter((item) => item.userId !== targetId));
};
//...
<td>
<button onClick={() => handleDelete(item.userId)}>删除</button>
</td>
3.修改 & 保存
为列表增加“修改”按钮:点击修改按钮之后按钮文字替换成保存,条目名字出现输入框,自动聚焦到输入框,可输入修改内容;点击保存隐藏输入框,将按钮文字替换回修改;
// App.tsx
//...
// 使用一个 state 来记录正在修改的项,以此来保存副本和判断显示
const [editMap, setEditMap] = useState<Record<string, User>>({});
// 动态保存引用的 dom 元素
const itemsRef = useRef<null | Map<string, HTMLInputElement>>(null);
const handleEdit = (targetId: string) => {
const target = data.find((i) => i.userId === targetId)!;
setEditMap({
...editMap,
[targetId]: {
...target,
},
});
// 逻辑连贯的情况,直接使用 setTimeout 在 dom 更新后聚焦
setTimeout(() => {
itemsRef.current!.get(targetId)?.focus();
});
};
const handleConfirmEdit = (targetId: string) => {
// 获取保存的被修改的副本
const nextUser = editMap[targetId];
const nextMap = {
...editMap,
};
// 删除编辑记录中的对应项
delete nextMap[targetId];
setEditMap(nextMap);
setData(
data.map((item) => {
if (item.userId === nextUser.userId) {
return nextUser;
} else {
return item;
}
})
);
};
//...
<tbody>
{data.map((item) => {
return (
<tr key={item.userId}>
<td>{item.userId}</td>
<td>
{editMap[item.userId] ? (
/** 由于列表项是不固定的,所以 ref 采用回调来获取 */
<input
ref={(node) => {
const map = getMap();
map.set(item.userId, node as HTMLInputElement);
return () => {
map.delete(item.userId);
};
}}
value={editMap[item.userId].name}
onChange={(e) => handleChange(e, item.userId)}
/>
) : (
<span>
{item.name}
</span>
)}
</td>
<td>{item.age}</td>
<td>{item.city}</td>
<td>{item.role}</td>
<td>
{editMap[item.userId] ? (
<button onClick={() => handleConfirmEdit(item.userId)}>
保存
</button>
) : (
<button onClick={() => handleEdit(item.userId)}>
修改
</button>
)}
<button onClick={() => handleDelete(item.userId)}>
删除
</button>
</td>
</tr>
);
})}
</tbody>
4.添加
添加一个表单,表单项为
-
名字:输入框
-
年龄:输入框
-
所在地:下拉选择
-
角色:下拉选择
-
操作项:
a. "添加"按钮:添加当前表单输入的内容到列表
b. "重置"按钮:将表单重置为默认值
// App.tsx
const defaultFormData = {
name: "",
age: "",
city: "广州",
role: "销售",
};
type UserFormData = {
name: string;
age: number;
city: string;
role: string;
};
function UserForm({ onAddUser }: { onAddUser: (user: UserFormData) => void }) {
//不要直接把原对象传入 useState,保持原对象独立
//这样不会在意外的地方(例如忘了用setState去更新而是直接用.操作符去更新了对象,这样会导致原对象被改变)
const [formData, setFormData] = useState({ ...defaultFormData });
// 修改项
// React 内置处理了所有的表单元素
//例如原生的 <selection> 是没有 value 这个值的,框架为了统一处理给这些元素内置了 value
//因此在编码层面我们可以保持和 input 一样的处理逻辑而不需要去处理元素的 <option> 的 selected 属性
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
setFormData({
...formData,
[e.target.id]: e.target.value,
});
};
// 重置表单
const handleReset = () => {
setFormData({
...defaultFormData,
});
};
const handleSubmit = () => {
onAddUser({
...formData,
age: Number(formData.age),
});
};
return (
<form style={{ border: "1px solid", width: "fit-content" }}>
<div>
<label htmlFor="name">姓名</label>
<input
type="text"
value={formData.name}
name="name"
id="name"
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="age">年龄</label>
<input
type="text"
value={formData.age}
name="age"
id="age"
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="city">所在地</label>
<select
name="city"
id="city"
value={formData.city}
onChange={handleChange}
>
<option value="广州">广州</option>
<option value="深">深圳</option>
</select>
</div>
<div>
<label htmlFor="role">角色</label>
<select
name="role"
id="role"
value={formData.role}
onChange={handleChange}
>
<option value="销售">销售</option>
<option value="销售主管">销售主管</option>
</select>
</div>
<div>
<button type="button" onClick={handleSubmit}>
添加
</button>
<button type="button" onClick={handleReset}>
重置
</button>
</div>
</form>
);
}
function App() {
//...
const handleAdd = (user: UserFormData) => {
setData([
...data,
{
...user,
// 这个只用来模拟一下id,实际上很容易重复
userId: Math.ceil(Math.random() * 100) + "",
},
]);
};
//...
return (
<div>
<table border={1}>
</table>
<UserForm onAddUser={handleAdd} />
</div>
);
}
5.查询
在表格上方加入一个输入搜索框,输入文字时筛选列表中名字中含有输入字符的条目
// App.tsx
function App() {
//...
const [keyword, setKeyword] = useState('')
// 要显示的条目是从原数据中派生出来的,直接计算值而不需要用 useState 保存
const filterData = data.filter(item => {
return item.name.includes(keyword)
})
//...
return (
<div>
<input value={keyword} onChange={(e: ChangeEvent<HTMLInputElement>) => {
setKeyword(e.target.value)
}} />
<table border={1}>
{filterData.map(item) => {
//...
}}
</table>
<UserForm onAddUser={handleAdd} />
</div>
);
}
小结
至此我们完成了一个基础的数据表格和增删改查功能,接下来我们将在这个代码的基础上一步步修改