AI 编程时代手工匠人代码打造 React 项目实战 (一):增删改查

152 阅读4分钟

主包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.添加

添加一个表单,表单项为

  1. 名字:输入框

  2. 年龄:输入框

  3. 所在地:下拉选择

  4. 角色:下拉选择

  5. 操作项:

    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>
  );
}

小结

至此我们完成了一个基础的数据表格和增删改查功能,接下来我们将在这个代码的基础上一步步修改

image-20250807161232849.png