React useState 实践:React 实现 CV 渲染项目
下面我们通过讲解一下使用 React 实现 CV 渲染的小项目,进一步讲解 React State 的一些知识。
最终效果如下(非常简陋的 CSS…):
1. 组件状态管理
状态设计
首先我们对 CV 进行组件划分,这很简单:我们设计 GeneralInfoForm、PraticalForm、EducationForm 这三个表单组件。
然后我们分析每个组件需要记住哪些状态来渲染。首先是 GeneralInfoForm。在渲染一个表格时,我们需要知道:
- 它是否在编辑中?如果在编辑中,我们需要给用户提供输入窗口和提交按钮;如果已经提交了,我们就不需要提供这些东西了、直接展示用户提交的结果。
- 用户输入的表单值?我们要在卡片中渲染这些值。
- 错误信息?这是一个常用的状态,当前端遇到错误时、我们可以把它渲染出来。
其实这个就是受控组件,不过我们把它细化一下、考虑得更加周全些。
然后我们就可以用 useState 记录这些状态了:
const [form, setForm] = useState({
fullName: initialValues.fullName || "",
email: initialValues.email || "",
phone: initialValues.phone || "",
});
const initiallyEmpty = !form.fullName && !form.email && !form.phone;
const [isEditing, setIsEditing] = useState(initiallyEmpty);
const [errors, setErrors] = useState({});
对于 PraticalForm、EducationForm,我们要处理动态添加的表格项,于是我们需要记录以下当前编辑的行的 id 和是否处于 Adding 状态(Adding 状态添加一个新的行,然后转换为 Editting 模式):
const [form, setForm] = useState(empty);
const [isAdding, setIsAdding] = useState(false);
const [editingId, setEditingId] = useState(null);
const [errors, setErrors] = useState({});
状态的使用
定义了这些状态后,我们就需要设置一下一些表格操作的回调、来正确更新这些状态。
- 填写表单。这个很简单,使用
setForm设置一下就行。这里要注意:应该使用解包语法创建一个新的对象:
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
- 提交表单:我们验证一下输入,调用父组件的
onSubmit回调,然后关掉isEditting即可。注意把表单的默认行为禁止:
function handleSubmit(e) {
e.preventDefault();
const err = validate(form);
setErrors(err);
if (Object.keys(err).length > 0) return;
if (typeof onSubmit === "function") onSubmit(form);
setIsEditing(false);
}
- 关闭表单:主动关闭表单后,我们把表单相关状态全部恢复原来的样子:
function handleCancel() {
setForm({
fullName: initialValues.fullName || "",
email: initialValues.email || "",
phone: initialValues.phone || "",
});
setErrors({});
setIsEditing(false);
}
对于另外两个表单,用相同的思路处理 Adding 和 Editting 状态即可:
function startAdd() {
setForm(empty);
setIsAdding(true);
setEditingId(null);
}
function startEdit(item) {
setForm({
school: item.school,
title: item.title,
dateFrom: item.dateFrom,
dateTo: item.dateTo,
});
setEditingId(item.id);
setIsAdding(false);
}
这里的提交表单处理需要注意,有两种形式的提交:Adding 阶段的创建空行和 Editting 后提交填完的行,需要分开处理:
function handleSubmit(e) {
e.preventDefault();
const err = validate(form);
setErrors(err);
if (Object.keys(err).length > 0) return;
if (editingId) {
onUpdate && onUpdate(editingId, form);
} else {
onAdd && onAdd(form);
}
cancel();
}
这里补充一点:为什么要传
onAdd这样的回调函数作为参数呢?因为 OnAdd 和 OnUpdate 的状态是父组件App.jsx负责管的(这个的逻辑是把当前的 Form 的结果提交到父组件的结果集合中),子组件只能通过这个状态提升操作来更新父组件状态、从而正确渲染。
2. App.jsx 状态管理
在完成组件自己的状态管理后,我们开始设计 App.jsx 的状态管理。
App.jsx 只需要管理每个不同的 Education 行和 Pratical 行。由于 GeneralInfo 只有一个,它不需要管理。
我们用两个数组来装 Education 和 Pratical 的填写结果,然后封装它们的 CRUD 操作给 App.jsx 调用即可。这一部分是处理全局的数据状态的,因此我们可以把它放在 src/hooks/useCvEntries.js 中:
import { useState } from "react";
export default function useCvEntries(
initialEducation = [],
initialPractical = [],
) {
const [education, setEducation] = useState(initialEducation);
const [practical, setPractical] = useState(initialPractical);
function makeId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function handleAddEducation(entry) {
setEducation((prev) => [...prev, { id: makeId(), ...entry }]);
}
function handleUpdateEducation(id, entry) {
setEducation((prev) =>
prev.map((item) => (item.id === id ? { id, ...entry } : item)),
);
}
function handleRemoveEducation(id) {
setEducation((prev) => prev.filter((item) => item.id !== id));
}
function handleAddPractical(entry) {
setPractical((prev) => [...prev, { id: makeId(), ...entry }]);
}
function handleUpdatePractical(id, entry) {
setPractical((prev) =>
prev.map((item) => (item.id === id ? { id, ...entry } : item)),
);
}
function handleRemovePractical(id) {
setPractical((prev) => prev.filter((item) => item.id !== id));
}
return {
education,
practical,
handleAddEducation,
handleUpdateEducation,
handleRemoveEducation,
handleAddPractical,
handleUpdatePractical,
handleRemovePractical,
};
}
最后在 App.jsx 中组装这些组件和 hooks 即可:
import { useState } from "react";
import "./App.css";
export default function App() {
const [general, setGeneral] = useState({
fullName: "",
email: "",
phone: "",
});
const {
education,
practical,
handleAddEducation,
handleUpdateEducation,
handleRemoveEducation,
handleAddPractical,
handleUpdatePractical,
handleRemovePractical,
} = useCvEntries();
// Education and practical entry state + handlers are provided by the useCvEntries hook
return (
<div className="container">
<div className="panel form-panel">
<GeneralInfoForm
initialValues={general}
onSubmit={(vals) => setGeneral(vals)}
/>
<EducationForm
entries={education}
onAdd={handleAddEducation}
onUpdate={handleUpdateEducation}
onRemove={handleRemoveEducation}
/>
<PracticalForm
entries={practical}
onAdd={handleAddPractical}
onUpdate={handleUpdatePractical}
onRemove={handleRemovePractical}
/>
</div>
<aside className="panel preview-panel">
<CVPreview
general={general}
education={education}
practical={practical}
/>
</aside>
</div>
);
}