Dark Dwarf Blog background

React useState 实践:React 实现 CV 渲染项目

React useState 实践:React 实现 CV 渲染项目

下面我们通过讲解一下使用 React 实现 CV 渲染的小项目,进一步讲解 React State 的一些知识。

最终效果如下(非常简陋的 CSS…):

alt text

1. 组件状态管理

a.a. 状态设计

首先我们对 CV 进行组件划分,这很简单:我们设计 GeneralInfoForm、PraticalForm、EducationForm 这三个表单组件。

然后我们分析每个组件需要记住哪些状态来渲染。首先是 GeneralInfoForm。在渲染一个表格时,我们需要知道:

  1. 它是否在编辑中?如果在编辑中,我们需要给用户提供输入窗口和提交按钮;如果已经提交了,我们就不需要提供这些东西了、直接展示用户提交的结果。
  2. 用户输入的表单值?我们要在卡片中渲染这些值。
  3. 错误信息?这是一个常用的状态,当前端遇到错误时、我们可以把它渲染出来。

其实这个就是受控组件,不过我们把它细化一下、考虑得更加周全些。

然后我们就可以用 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({});

b.b. 状态的使用

定义了这些状态后,我们就需要设置一下一些表格操作的回调、来正确更新这些状态。

  1. 填写表单。这个很简单,使用 setForm 设置一下就行。这里要注意:应该使用解包语法创建一个新的对象:
function handleChange(e) {
  const { name, value } = e.target;
  setForm((prev) => ({ ...prev, [name]: value }));
}
  1. 提交表单:我们验证一下输入,调用父组件的 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);
}
  1. 关闭表单:主动关闭表单后,我们把表单相关状态全部恢复原来的样子:
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>
  );
}