实现一个带有创建和编辑功能的列表,并使用 Modal 弹框中的 Form 表单,是前端开发中常见的需求。以下是基于 Ant Design 的最佳实践实现方案。
功能需求
- 展示一个列表,支持创建和编辑操作。
- 点击"创建"或"编辑"时,弹出 Modal 弹框,内部包含 Form 表单。
- 创建和编辑共用同一个 Modal 和 Form。
- 表单提交后,更新列表数据。
实现步骤
1. 安装依赖
确保已安装 antd
和 react
:
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;
代码说明
-
状态管理
visible
:控制 Modal 的显示和隐藏。currentRecord
:当前操作的数据(null
表示创建,非null
表示编辑)。dataSource
:列表数据。
-
表单逻辑
- 使用
form.resetFields()
重置表单。 - 使用
form.setFieldsValue()
在编辑时填充表单数据。 - 表单提交时,根据
currentRecord
判断是创建还是编辑。
- 使用
-
列表操作
- 点击"Edit"按钮时,将当前记录传递给
showModal
方法。 - 点击"Create"按钮时,调用
showModal
方法,不传递记录。
- 点击"Edit"按钮时,将当前记录传递给
-
Modal 和 Form
- Modal 的标题根据
currentRecord
动态变化。 - Form 使用
layout="vertical"
实现垂直布局。
- Modal 的标题根据
在表单相对复杂的情况下,将 Modal 提取到一个独立的文件中是一个很好的设计选择。这样可以提高代码的可维护性和可读性,同时遵循单一职责原则。以下是实现的最佳实践:
设计思路
-
组件拆分
- 将 Modal 和 Form 提取到一个独立的组件文件中。
- 父组件负责管理列表数据和 Modal 的显示逻辑。
- 子组件(Modal)负责表单的渲染和提交逻辑。
-
状态管理
- 父组件通过
props
将必要的状态和方法传递给子组件。 - 子组件通过回调函数将表单数据传递回父组件。
- 父组件通过
-
表单逻辑
- 子组件内部管理表单的状态和验证逻辑。
- 父组件通过
ref
或props
控制子组件的显示和隐藏。
实现步骤
1. 创建独立 Modal 组件
将 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;
2. 父组件管理列表和 Modal
在父组件中使用 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;
代码说明
-
FormModal 组件
- 通过
visible
控制 Modal 的显示和隐藏。 - 通过
initialValues
接收父组件传递的表单初始值。 - 通过
onSubmit
将表单数据传递回父组件。
- 通过
-
父组件
- 管理列表数据
dataSource
和当前操作的数据currentRecord
。 - 通过
showModal
方法打开 Modal,并区分创建和编辑状态。 - 通过
handleSubmit
方法处理表单提交逻辑,更新列表数据。
- 管理列表数据
-
状态传递
- 父组件将
visible
、initialValues
、onCancel
和onSubmit
作为props
传递给FormModal
。 FormModal
通过回调函数将表单数据传递回父组件。
- 父组件将
优点
-
组件职责分离
FormModal
只负责表单的渲染和提交逻辑。- 父组件只负责列表数据的管理和 Modal 的显示逻辑。
-
可维护性
- 表单逻辑独立封装,便于扩展和维护。
- 父组件代码更简洁,逻辑更清晰。
-
复用性
FormModal
可以在其他需要表单弹框的地方复用。
在生产环境中多数情况下,数据逻辑会更加复杂,这时候我们需要将数据逻辑与纯样式组件隔离,我们可以使用 自定义 Hooks 来封装数据加载、状态管理和表单逻辑,而将 UI 部分(如 Modal、Form、Input 等)保留在纯样式组件中。这种方式符合 关注点分离 的原则,使代码更易于维护和测试。
设计思路
-
自定义 Hooks
- 使用自定义 Hook(如
useFormData
)封装表单的数据加载、状态管理和提交逻辑。 - 将异步数据加载、字段依赖处理等逻辑放在自定义 Hook 中。
- 使用自定义 Hook(如
-
纯样式组件
- 将 Modal、Form、Input 等 UI 组件单独提取为一个纯样式组件(如
FormModalUI
)。 - 纯样式组件只负责渲染 UI,不包含任何业务逻辑。
- 将 Modal、Form、Input 等 UI 组件单独提取为一个纯样式组件(如
-
状态传递
- 通过
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;
优点
-
关注点分离
- 数据逻辑和 UI 逻辑完全分离,代码更清晰。
- 自定义 Hook 可以复用,纯样式组件也可以复用。
-
可维护性
- 数据逻辑集中在自定义 Hook 中,便于测试和扩展。
- UI 组件只负责渲染,逻辑更简单。
-
灵活性
- 支持动态加载异步数据和字段依赖。
- 可以轻松扩展表单字段和逻辑。
通过这种设计,你可以更好地管理复杂的表单场景,同时保持代码的可维护性和可扩展性。
扩展
如果表单非常复杂,可以进一步拆分:
- 静态页面部分将表单字段拆分为多个子组件。
- 数据部分使用
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 的 useQuery
和 useMutation
:
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 和纯样式组件即可。
优点
-
数据缓存
- React Query 会自动缓存数据,避免重复请求。
- 支持后台刷新和自动重试。
-
代码简洁
- 使用
useQuery
和useMutation
简化了异步数据管理。 - 无需手动管理加载状态和错误处理。
- 使用
-
实时更新
- 支持乐观更新和实时数据同步。
-
灵活性
- 可以轻松扩展为更复杂的场景(如分页、无限加载等)。
其他工具的选择
- 如果项目已经使用 Redux ,可以选择 RTK Query。
- 如果需要轻量级解决方案,可以选择 SWR 或 Zustand。
- 如果需要处理复杂的状态依赖,可以选择 Recoil。
以上全文。