基于 React 的列表实现方案,包含创建和编辑状态,使用 Modal 弹框和表单的最佳实践

实现一个带有创建和编辑功能的列表,并使用 Modal 弹框中的 Form 表单,是前端开发中常见的需求。以下是基于 Ant Design 的最佳实践实现方案。


功能需求

  1. 展示一个列表,支持创建和编辑操作。
  2. 点击"创建"或"编辑"时,弹出 Modal 弹框,内部包含 Form 表单。
  3. 创建和编辑共用同一个 Modal 和 Form。
  4. 表单提交后,更新列表数据。

实现步骤

1. 安装依赖

确保已安装 antdreact

bash 复制代码
npm install antd

2. 实现代码

jsx 复制代码
import React, { useState } from 'react';
import { Table, Button, Modal, Form, Input } from 'antd';

const ListWithModalForm = () => {
  const [form] = Form.useForm(); // Form 实例
  const [visible, setVisible] = useState(false); // 控制 Modal 显示
  const [currentRecord, setCurrentRecord] = useState(null); // 当前操作的数据(创建时为 null,编辑时为具体数据)
  const [dataSource, setDataSource] = useState([
    { id: 1, name: 'John Doe', age: 25 },
    { id: 2, name: 'Jane Smith', age: 30 },
  ]); // 列表数据

  // 打开 Modal
  const showModal = (record = null) => {
    setCurrentRecord(record);
    form.resetFields(); // 重置表单
    if (record) {
      form.setFieldsValue(record); // 编辑时填充表单
    }
    setVisible(true);
  };

  // 关闭 Modal
  const handleCancel = () => {
    setVisible(false);
  };

  // 表单提交
  const handleSubmit = () => {
    form
      .validateFields()
      .then((values) => {
        if (currentRecord) {
          // 编辑逻辑
          const newData = dataSource.map((item) =>
            item.id === currentRecord.id ? { ...item, ...values } : item
          );
          setDataSource(newData);
        } else {
          // 创建逻辑
          const newRecord = { ...values, id: Date.now() }; // 生成唯一 ID
          setDataSource([...dataSource, newRecord]);
        }
        setVisible(false); // 关闭 Modal
      })
      .catch((error) => {
        console.log('Validation Failed:', error);
      });
  };

  // 列表列配置
  const columns = [
    { title: 'Name', dataIndex: 'name', key: 'name' },
    { title: 'Age', dataIndex: 'age', key: 'age' },
    {
      title: 'Action',
      key: 'action',
      render: (_, record) => (
        <Button type="link" onClick={() => showModal(record)}>
          Edit
        </Button>
      ),
    },
  ];

  return (
    <div>
      {/* 创建按钮 */}
      <Button type="primary" onClick={() => showModal()} style={{ marginBottom: 16 }}>
        Create
      </Button>

      {/* 列表 */}
      <Table dataSource={dataSource} columns={columns} rowKey="id" />

      {/* Modal 弹框 */}
      <Modal
        title={currentRecord ? 'Edit Record' : 'Create Record'}
        visible={visible}
        onCancel={handleCancel}
        onOk={handleSubmit}
      >
        <Form form={form} layout="vertical">
          <Form.Item
            name="name"
            label="Name"
            rules={[{ required: true, message: 'Please input the name!' }]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            name="age"
            label="Age"
            rules={[{ required: true, message: 'Please input the age!' }]}
          >
            <Input type="number" />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
};

export default ListWithModalForm;

代码说明

  1. 状态管理

    • visible:控制 Modal 的显示和隐藏。
    • currentRecord:当前操作的数据(null 表示创建,非 null 表示编辑)。
    • dataSource:列表数据。
  2. 表单逻辑

    • 使用 form.resetFields() 重置表单。
    • 使用 form.setFieldsValue() 在编辑时填充表单数据。
    • 表单提交时,根据 currentRecord 判断是创建还是编辑。
  3. 列表操作

    • 点击"Edit"按钮时,将当前记录传递给 showModal 方法。
    • 点击"Create"按钮时,调用 showModal 方法,不传递记录。
  4. Modal 和 Form

    • Modal 的标题根据 currentRecord 动态变化。
    • Form 使用 layout="vertical" 实现垂直布局。

在表单相对复杂的情况下,将 Modal 提取到一个独立的文件中是一个很好的设计选择。这样可以提高代码的可维护性和可读性,同时遵循单一职责原则。以下是实现的最佳实践:


设计思路

  1. 组件拆分

    • 将 Modal 和 Form 提取到一个独立的组件文件中。
    • 父组件负责管理列表数据和 Modal 的显示逻辑。
    • 子组件(Modal)负责表单的渲染和提交逻辑。
  2. 状态管理

    • 父组件通过 props 将必要的状态和方法传递给子组件。
    • 子组件通过回调函数将表单数据传递回父组件。
  3. 表单逻辑

    • 子组件内部管理表单的状态和验证逻辑。
    • 父组件通过 refprops 控制子组件的显示和隐藏。

实现步骤

将 Modal 和 Form 提取到一个独立的文件中,例如 FormModal.js

jsx 复制代码
import React from 'react';
import { Modal, Form, Input } from 'antd';

const FormModal = ({ visible, onCancel, onSubmit, initialValues }) => {
  const [form] = Form.useForm();

  // 初始化表单值
  React.useEffect(() => {
    if (visible) {
      form.resetFields();
      if (initialValues) {
        form.setFieldsValue(initialValues);
      }
    }
  }, [visible, initialValues, form]);

  // 提交表单
  const handleSubmit = () => {
    form
      .validateFields()
      .then((values) => {
        onSubmit(values); // 将表单数据传递回父组件
      })
      .catch((error) => {
        console.log('Validation Failed:', error);
      });
  };

  return (
    <Modal
      title={initialValues ? 'Edit Record' : 'Create Record'}
      visible={visible}
      onCancel={onCancel}
      onOk={handleSubmit}
    >
      <Form form={form} layout="vertical">
        <Form.Item
          name="name"
          label="Name"
          rules={[{ required: true, message: 'Please input the name!' }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          name="age"
          label="Age"
          rules={[{ required: true, message: 'Please input the age!' }]}
        >
          <Input type="number" />
        </Form.Item>
        {/* 更多复杂的表单字段 */}
      </Form>
    </Modal>
  );
};

export default FormModal;

在父组件中使用 FormModal,并管理列表数据和 Modal 的显示逻辑:

jsx 复制代码
import React, { useState } from 'react';
import { Table, Button } from 'antd';
import FormModal from './FormModal';

const ParentComponent = () => {
  const [visible, setVisible] = useState(false); // 控制 Modal 显示
  const [currentRecord, setCurrentRecord] = useState(null); // 当前操作的数据
  const [dataSource, setDataSource] = useState([
    { id: 1, name: 'John Doe', age: 25 },
    { id: 2, name: 'Jane Smith', age: 30 },
  ]); // 列表数据

  // 打开 Modal
  const showModal = (record = null) => {
    setCurrentRecord(record);
    setVisible(true);
  };

  // 关闭 Modal
  const handleCancel = () => {
    setVisible(false);
  };

  // 提交表单
  const handleSubmit = (values) => {
    if (currentRecord) {
      // 编辑逻辑
      const newData = dataSource.map((item) =>
        item.id === currentRecord.id ? { ...item, ...values } : item
      );
      setDataSource(newData);
    } else {
      // 创建逻辑
      const newRecord = { ...values, id: Date.now() }; // 生成唯一 ID
      setDataSource([...dataSource, newRecord]);
    }
    setVisible(false); // 关闭 Modal
  };

  // 列表列配置
  const columns = [
    { title: 'Name', dataIndex: 'name', key: 'name' },
    { title: 'Age', dataIndex: 'age', key: 'age' },
    {
      title: 'Action',
      key: 'action',
      render: (_, record) => (
        <Button type="link" onClick={() => showModal(record)}>
          Edit
        </Button>
      ),
    },
  ];

  return (
    <div>
      {/* 创建按钮 */}
      <Button type="primary" onClick={() => showModal()} style={{ marginBottom: 16 }}>
        Create
      </Button>

      {/* 列表 */}
      <Table dataSource={dataSource} columns={columns} rowKey="id" />

      {/* Modal 弹框 */}
      <FormModal
        visible={visible}
        onCancel={handleCancel}
        onSubmit={handleSubmit}
        initialValues={currentRecord}
      />
    </div>
  );
};

export default ParentComponent;

代码说明

  1. FormModal 组件

    • 通过 visible 控制 Modal 的显示和隐藏。
    • 通过 initialValues 接收父组件传递的表单初始值。
    • 通过 onSubmit 将表单数据传递回父组件。
  2. 父组件

    • 管理列表数据 dataSource 和当前操作的数据 currentRecord
    • 通过 showModal 方法打开 Modal,并区分创建和编辑状态。
    • 通过 handleSubmit 方法处理表单提交逻辑,更新列表数据。
  3. 状态传递

    • 父组件将 visibleinitialValuesonCancelonSubmit 作为 props 传递给 FormModal
    • FormModal 通过回调函数将表单数据传递回父组件。

优点

  1. 组件职责分离

    • FormModal 只负责表单的渲染和提交逻辑。
    • 父组件只负责列表数据的管理和 Modal 的显示逻辑。
  2. 可维护性

    • 表单逻辑独立封装,便于扩展和维护。
    • 父组件代码更简洁,逻辑更清晰。
  3. 复用性

    • FormModal 可以在其他需要表单弹框的地方复用。

在生产环境中多数情况下,数据逻辑会更加复杂,这时候我们需要将数据逻辑与纯样式组件隔离,我们可以使用 自定义 Hooks 来封装数据加载、状态管理和表单逻辑,而将 UI 部分(如 Modal、Form、Input 等)保留在纯样式组件中。这种方式符合 关注点分离 的原则,使代码更易于维护和测试。


设计思路

  1. 自定义 Hooks

    • 使用自定义 Hook(如 useFormData)封装表单的数据加载、状态管理和提交逻辑。
    • 将异步数据加载、字段依赖处理等逻辑放在自定义 Hook 中。
  2. 纯样式组件

    • 将 Modal、Form、Input 等 UI 组件单独提取为一个纯样式组件(如 FormModalUI)。
    • 纯样式组件只负责渲染 UI,不包含任何业务逻辑。
  3. 状态传递

    • 通过 props 将状态和方法从自定义 Hook 传递给纯样式组件。

实现步骤

1. 封装数据逻辑的自定义 Hook

创建一个自定义 Hook useFormData,用于管理表单的数据逻辑:

jsx 复制代码
import { useState, useEffect } from 'react';

const useFormData = (initialValues) => {
  const [formValues, setFormValues] = useState(initialValues || {});
  const [countries, setCountries] = useState([]);
  const [cities, setCities] = useState([]);
  const [loading, setLoading] = useState(false);

  // 加载国家数据
  useEffect(() => {
    const fetchCountries = async () => {
      setLoading(true);
      // 模拟 API 调用
      setTimeout(() => {
        setCountries([
          { id: 1, name: 'China' },
          { id: 2, name: 'USA' },
        ]);
        setLoading(false);
      }, 1000);
    };
    fetchCountries();
  }, []);

  // 加载城市数据(根据选择的国家)
  useEffect(() => {
    const fetchCities = async () => {
      const countryId = formValues.country;
      if (!countryId) {
        setCities([]);
        return;
      }
      setLoading(true);
      // 模拟 API 调用
      setTimeout(() => {
        setCities(
          countryId === 1
            ? [
                { id: 1, name: 'Beijing' },
                { id: 2, name: 'Shanghai' },
              ]
            : [
                { id: 3, name: 'New York' },
                { id: 4, name: 'Los Angeles' },
              ]
        );
        setLoading(false);
      }, 1000);
    };
    fetchCities();
  }, [formValues.country]);

  // 更新表单值
  const handleFormChange = (changedValues, allValues) => {
    setFormValues(allValues);
  };

  // 提交表单
  const handleSubmit = (values) => {
    console.log('Form Values:', values);
    // 在这里处理表单提交逻辑(如调用 API)
  };

  return {
    formValues,
    countries,
    cities,
    loading,
    handleFormChange,
    handleSubmit,
  };
};

export default useFormData;

2. 创建纯样式组件

创建一个纯样式组件 FormModalUI,只负责渲染 UI:

jsx 复制代码
import React from 'react';
import { Modal, Form, Input, Select, Spin } from 'antd';

const FormModalUI = ({
  visible,
  onCancel,
  onSubmit,
  formValues,
  countries,
  cities,
  loading,
  handleFormChange,
}) => {
  const [form] = Form.useForm();

  // 初始化表单值
  React.useEffect(() => {
    if (visible) {
      form.resetFields();
      form.setFieldsValue(formValues);
    }
  }, [visible, formValues, form]);

  return (
    <Modal
      title={formValues.id ? 'Edit Record' : 'Create Record'}
      visible={visible}
      onCancel={onCancel}
      onOk={() => form.submit()}
    >
      <Spin spinning={loading}>
        <Form
          form={form}
          layout="vertical"
          onValuesChange={handleFormChange}
          onFinish={onSubmit}
        >
          <Form.Item
            name="name"
            label="Name"
            rules={[{ required: true, message: 'Please input the name!' }]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            name="country"
            label="Country"
            rules={[{ required: true, message: 'Please select a country!' }]}
          >
            <Select placeholder="Select a country">
              {countries.map((country) => (
                <Select.Option key={country.id} value={country.id}>
                  {country.name}
                </Select.Option>
              ))}
            </Select>
          </Form.Item>
          <Form.Item
            name="city"
            label="City"
            rules={[{ required: true, message: 'Please select a city!' }]}
          >
            <Select placeholder="Select a city">
              {cities.map((city) => (
                <Select.Option key={city.id} value={city.id}>
                  {city.name}
                </Select.Option>
              ))}
            </Select>
          </Form.Item>
        </Form>
      </Spin>
    </Modal>
  );
};

export default FormModalUI;

3. 在父组件中集成

在父组件中使用自定义 Hook 和纯样式组件:

jsx 复制代码
import React, { useState } from 'react';
import { Table, Button } from 'antd';
import useFormData from './useFormData';
import FormModalUI from './FormModalUI';

const ParentComponent = () => {
  const [visible, setVisible] = useState(false);
  const [currentRecord, setCurrentRecord] = useState(null);
  const [dataSource, setDataSource] = useState([
    { id: 1, name: 'John Doe', country: 1, city: 1 },
    { id: 2, name: 'Jane Smith', country: 2, city: 3 },
  ]);

  const {
    formValues,
    countries,
    cities,
    loading,
    handleFormChange,
    handleSubmit,
  } = useFormData(currentRecord);

  const showModal = (record = null) => {
    setCurrentRecord(record);
    setVisible(true);
  };

  const handleCancel = () => {
    setVisible(false);
  };

  const handleFormSubmit = (values) => {
    handleSubmit(values); // 调用自定义 Hook 的提交逻辑
    if (currentRecord) {
      const newData = dataSource.map((item) =>
        item.id === currentRecord.id ? { ...item, ...values } : item
      );
      setDataSource(newData);
    } else {
      const newRecord = { ...values, id: Date.now() };
      setDataSource([...dataSource, newRecord]);
    }
    setVisible(false);
  };

  const columns = [
    { title: 'Name', dataIndex: 'name', key: 'name' },
    { title: 'Country', dataIndex: 'country', key: 'country' },
    { title: 'City', dataIndex: 'city', key: 'city' },
    {
      title: 'Action',
      key: 'action',
      render: (_, record) => (
        <Button type="link" onClick={() => showModal(record)}>
          Edit
        </Button>
      ),
    },
  ];

  return (
    <div>
      <Button type="primary" onClick={() => showModal()} style={{ marginBottom: 16 }}>
        Create
      </Button>
      <Table dataSource={dataSource} columns={columns} rowKey="id" />
      <FormModalUI
        visible={visible}
        onCancel={handleCancel}
        onSubmit={handleFormSubmit}
        formValues={formValues}
        countries={countries}
        cities={cities}
        loading={loading}
        handleFormChange={handleFormChange}
      />
    </div>
  );
};

export default ParentComponent;

优点

  1. 关注点分离

    • 数据逻辑和 UI 逻辑完全分离,代码更清晰。
    • 自定义 Hook 可以复用,纯样式组件也可以复用。
  2. 可维护性

    • 数据逻辑集中在自定义 Hook 中,便于测试和扩展。
    • UI 组件只负责渲染,逻辑更简单。
  3. 灵活性

    • 支持动态加载异步数据和字段依赖。
    • 可以轻松扩展表单字段和逻辑。

通过这种设计,你可以更好地管理复杂的表单场景,同时保持代码的可维护性和可扩展性。

扩展

如果表单非常复杂,可以进一步拆分:

  • 静态页面部分将表单字段拆分为多个子组件。
  • 数据部分使用 context 或状态管理工具(如 Redux、Zustand)管理表单状态,接入异步状态管理React Query或者SWR等

以下是一些常用的异步状态管理工具,以及它们如何适用于我们的最佳实践中。


常用的异步状态管理工具

1. React Query

  • 特点
    • 专为数据获取和缓存设计。
    • 提供了强大的缓存、后台刷新、数据同步功能。
    • 支持乐观更新和自动重试。
  • 适用场景
    • 需要频繁从 API 获取数据的场景。
    • 需要缓存和同步数据的场景。
  • 官网React Query

2. SWR

  • 特点
    • 轻量级的数据获取库。
    • 支持缓存、重新验证、轮询等功能。
    • 基于 React Hooks 设计,易于集成。
  • 适用场景
    • 需要简单、快速实现数据获取的场景。
    • 需要实时数据更新的场景。
  • 官网SWR

3. Redux Toolkit + RTK Query

  • 特点
    • Redux Toolkit 是 Redux 的官方工具集,简化了 Redux 的使用。
    • RTK Query 是 Redux Toolkit 的数据获取和缓存解决方案。
    • 支持自动生成 API 层代码。
  • 适用场景
    • 已经在使用 Redux 的项目。
    • 需要强大状态管理和数据获取能力的场景。
  • 官网Redux Toolkit

4. Recoil

  • 特点
    • React 原生的状态管理库。
    • 支持异步状态管理和依赖关系。
    • 简单易用,API 设计符合 React 哲学。
  • 适用场景
    • 需要轻量级状态管理的场景。
    • 需要处理复杂状态依赖的场景。
  • 官网Recoil

5. Zustand

  • 特点
    • 轻量级状态管理库。
    • 支持异步状态和中间件。
    • API 简单,易于集成。
  • 适用场景
    • 需要简单、灵活的状态管理。
    • 不需要复杂的状态管理工具。
  • 官网Zustand

在最佳实践中使用 React Query

以下是如何在上述最佳实践中使用 React Query 来管理异步状态的示例。

1. 安装 React Query

bash 复制代码
npm install @tanstack/react-query

2. 配置 React Query 的 Provider

在应用的根组件中配置 QueryClientProvider

jsx 复制代码
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

const App = () => (
  <QueryClientProvider client={queryClient}>
    <ParentComponent />
  </QueryClientProvider>
);

export default App;

3. 使用 React Query 管理异步数据

在自定义 Hook 中使用 React Query 的 useQueryuseMutation

jsx 复制代码
import { useQuery, useMutation } from '@tanstack/react-query';

const fetchCountries = async () => {
  // 模拟 API 调用
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: 'China' },
        { id: 2, name: 'USA' },
      ]);
    }, 1000);
  });
};

const fetchCities = async (countryId) => {
  // 模拟 API 调用
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(
        countryId === 1
          ? [
              { id: 1, name: 'Beijing' },
              { id: 2, name: 'Shanghai' },
            ]
          : [
              { id: 3, name: 'New York' },
              { id: 4, name: 'Los Angeles' },
            ]
      );
    }, 1000);
  });
};

const useFormData = (initialValues) => {
  const [formValues, setFormValues] = useState(initialValues || {});

  // 使用 React Query 获取国家数据
  const { data: countries, isLoading: isCountriesLoading } = useQuery(
    ['countries'],
    fetchCountries
  );

  // 使用 React Query 获取城市数据
  const { data: cities, isLoading: isCitiesLoading } = useQuery(
    ['cities', formValues.country],
    () => fetchCities(formValues.country),
    {
      enabled: !!formValues.country, // 仅在选择了国家后加载城市数据
    }
  );

  // 更新表单值
  const handleFormChange = (changedValues, allValues) => {
    setFormValues(allValues);
  };

  // 提交表单
  const handleSubmit = (values) => {
    console.log('Form Values:', values);
    // 在这里处理表单提交逻辑(如调用 API)
  };

  return {
    formValues,
    countries: countries || [],
    cities: cities || [],
    loading: isCountriesLoading || isCitiesLoading,
    handleFormChange,
    handleSubmit,
  };
};

export default useFormData;

4. 在父组件中集成

父组件的代码无需修改,直接使用自定义 Hook 和纯样式组件即可。


优点

  1. 数据缓存

    • React Query 会自动缓存数据,避免重复请求。
    • 支持后台刷新和自动重试。
  2. 代码简洁

    • 使用 useQueryuseMutation 简化了异步数据管理。
    • 无需手动管理加载状态和错误处理。
  3. 实时更新

    • 支持乐观更新和实时数据同步。
  4. 灵活性

    • 可以轻松扩展为更复杂的场景(如分页、无限加载等)。

其他工具的选择

  • 如果项目已经使用 Redux ,可以选择 RTK Query
  • 如果需要轻量级解决方案,可以选择 SWRZustand
  • 如果需要处理复杂的状态依赖,可以选择 Recoil

以上全文。

相关推荐
零凌林15 分钟前
vue3中解决组件间 css 层级问题最佳实践(Teleport的使用)
前端·css·vue.js·新特性·vue3.0·teleport
拉不动的猪1 小时前
刷刷题17(webpack)
前端·javascript·面试
烂蜻蜓1 小时前
深入理解 Uniapp 中的 px 与 rpx
前端·css·vue.js·uni-app·html
木亦Sam1 小时前
响应式网页设计中媒体查询的进阶运用
前端·响应式设计
diemeng11191 小时前
2024系统编程语言风云变幻:Rust持续领跑,Zig与Ada异军突起
开发语言·前端·后端·rust
烂蜻蜓1 小时前
Uniapp 中布局魔法:display 属性
前端·javascript·css·vue.js·uni-app·html
java1234_小锋2 小时前
一周学会Flask3 Python Web开发-redirect重定向
前端·python·flask·flask3
琑952 小时前
nextjs项目搭建——头部导航
开发语言·前端·javascript
light多学一点2 小时前
视频的分片上传
前端
Gazer_S3 小时前
【Windows系统node_modules删除失败(EPERM)问题解析与应对方案】
前端·javascript·windows