前言
useSubmit
是 React Router v6.4+
引入的强大钩子,用于以编程方式提交表单数据。
它提供了对表单提交过程的精细控制,特别适合需要自定义提交行为或非标准表单场景的应用。
一、useSubmit 核心用途
- 编程式表单提交:不依赖
<form>
元素手动提交数据 - 自定义提交逻辑:在提交前/后执行额外操作
- 动态表单构建:根据应用状态生成提交数据
- 替代传统表单:在复杂UI中实现表单功能
二、useSubmit 基本用法
import { useSubmit } from 'react-router-dom';
function CustomSubmitButton() {
const submit = useSubmit();
const handleSubmit = () => {
// 构建表单数据
const formData = new FormData();
formData.append('username', 'john_doe');
formData.append('password', 'secure123');
// 提交数据
submit(formData, {
method: 'post',
action: '/login',
encType: 'application/x-www-form-urlencoded'
});
};
return (
<button onClick={handleSubmit}>登录</button>
);
}
三、useSubmit 参数详解
3.1、submit 函数参数
```javascript
submit(
data: FormData | URLSearchParams | { [key: string]: string } | null,
options?: {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
action?: string;
encType?: 'application/x-www-form-urlencoded' | 'multipart/form-data';
replace?: boolean;
}
)
```
3.2、选项说明:
method
:HTTP方法(默认为"GET
")
action
:提交的目标URL(默认为当前URL
)
encType
:表单编码类型(默认为"application/x-www-form-urlencoded
")
replace
:是否替换历史记录而不是添加新条目(默认为false
)
四、useSubmit 实际应用场景
4.1、自定义删除按钮
import { useSubmit } from 'react-router-dom';
function DeleteButton({ itemId }) {
const submit = useSubmit();
const handleDelete = () => {
if (window.confirm('确定要删除此项吗?')) {
submit(
{ id: itemId },
{
method: 'delete',
action: `/items/${itemId}`
}
);
}
};
return (
<button
onClick={handleDelete}
className="delete-btn"
aria-label="删除项目"
>
🗑️ 删除
</button>
);
}
4.2、动态搜索表单
import { useState, useEffect } from 'react';
import { useSubmit, useLocation } from 'react-router-dom';
function SearchBar() {
const submit = useSubmit();
const location = useLocation();
const [query, setQuery] = useState('');
// 使用防抖自动提交搜索
useEffect(() => {
const timerId = setTimeout(() => {
if (query.trim() !== '') {
const formData = new FormData();
formData.append('q', query);
submit(formData, {
method: 'get',
action: '/search',
replace: query === '' // 空查询时替换历史记录
});
}
}, 300);
return () => clearTimeout(timerId);
}, [query, submit]);
// 初始化时从URL读取查询参数
useEffect(() => {
const params = new URLSearchParams(location.search);
setQuery(params.get('q') || '');
}, [location.search]);
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
className="search-input"
/>
{query && (
<button
onClick={() => setQuery('')}
className="clear-btn"
>
✕
</button>
)}
</div>
);
}
4.3、多步骤表单
import { useState } from 'react';
import { useSubmit } from 'react-router-dom';
function MultiStepForm() {
const submit = useSubmit();
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
personal: {},
address: {},
preferences: {}
});
const handleNextStep = () => {
if (step < 3) {
setStep(step + 1);
} else {
// 最终提交
submitForm();
}
};
const handlePrevStep = () => {
setStep(step - 1);
};
const updateFormData = (section, data) => {
setFormData(prev => ({
...prev,
[section]: { ...prev[section], ...data }
}));
};
const submitForm = () => {
// 构建完整表单数据
const finalData = {
...formData.personal,
...formData.address,
...formData.preferences
};
submit(finalData, {
method: 'post',
action: '/register',
encType: 'application/json'
});
};
return (
<div className="multi-step-form">
<div className="progress-bar">
<div className={`step ${step >= 1 ? 'active' : ''}`}>个人信息</div>
<div className={`step ${step >= 2 ? 'active' : ''}`}>地址信息</div>
<div className={`step ${step >= 3 ? 'active' : ''}`}>偏好设置</div>
</div>
<div className="form-content">
{step === 1 && (
<PersonalInfo
data={formData.personal}
onChange={data => updateFormData('personal', data)}
/>
)}
{step === 2 && (
<AddressInfo
data={formData.address}
onChange={data => updateFormData('address', data)}
/>
)}
{step === 3 && (
<Preferences
data={formData.preferences}
onChange={data => updateFormData('preferences', data)}
/>
)}
</div>
<div className="form-navigation">
{step > 1 && (
<button onClick={handlePrevStep}>上一步</button>
)}
<button onClick={handleNextStep}>
{step < 3 ? '下一步' : '提交注册'}
</button>
</div>
</div>
);
}
4.4、文件上传
import { useRef, useState } from 'react';
import { useSubmit } from 'react-router-dom';
function FileUploader() {
const submit = useSubmit();
const fileInputRef = useRef(null);
const [isUploading, setIsUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
setIsUploading(true);
try {
// 创建FormData对象
const formData = new FormData();
formData.append('file', file);
formData.append('description', '用户上传的文件');
// 创建XMLHttpRequest以获取上传进度
const xhr = new XMLHttpRequest();
// 进度事件处理
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
setProgress(percent);
}
});
// 完成事件处理
xhr.addEventListener('load', () => {
setIsUploading(false);
setProgress(0);
alert('文件上传成功!');
});
// 错误处理
xhr.addEventListener('error', () => {
setIsUploading(false);
alert('文件上传失败');
});
// 打开并发送请求
xhr.open('POST', '/upload');
xhr.send(formData);
// 或者使用submit钩子(但无法获取进度)
// submit(formData, {
// method: 'post',
// action: '/upload',
// encType: 'multipart/form-data'
// });
} catch (error) {
setIsUploading(false);
console.error('上传失败:', error);
}
};
return (
<div className="file-uploader">
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<button
onClick={() => fileInputRef.current.click()}
disabled={isUploading}
>
选择文件
</button>
{isUploading && (
<div className="upload-progress">
<div
className="progress-bar"
style={{ width: `${progress}%` }}
></div>
<span>{progress}%</span>
</div>
)}
</div>
);
}
4.5、乐观更新与表单提交
import { useState } from 'react';
import { useSubmit } from 'react-router-dom';
function TodoItem({ todo }) {
const submit = useSubmit();
const [isCompleted, setIsCompleted] = useState(todo.completed);
const [isUpdating, setIsUpdating] = useState(false);
const handleToggle = async () => {
// 乐观更新:立即更新UI
const newCompleted = !isCompleted;
setIsCompleted(newCompleted);
setIsUpdating(true);
try {
// 准备表单数据
const formData = new FormData();
formData.append('id', todo.id);
formData.append('completed', newCompleted.toString());
// 提交更新
submit(formData, {
method: 'patch',
action: `/todos/${todo.id}`,
encType: 'application/x-www-form-urlencoded'
});
} catch (error) {
// 出错时回滚状态
setIsCompleted(!newCompleted);
console.error('更新失败:', error);
} finally {
setIsUpdating(false);
}
};
return (
<li className={`todo-item ${isCompleted ? 'completed' : ''}`}>
<input
type="checkbox"
checked={isCompleted}
onChange={handleToggle}
disabled={isUpdating}
/>
<span className="todo-text">{todo.text}</span>
{isUpdating && <span className="updating-indicator">更新中...</span>}
</li>
);
}
五、useSubmit 与相关API对比
六、useSubmit 最佳实践
数据验证:提交前验证数据
用户反馈:提交时显示加载状态
错误处理:捕获并处理提交错误
编码类型:
a.简单数据:
application/x-www-form-urlencoded
b.文件上传:
multipart/form-data
c.JSON数据:
手动序列化并设置适当headers
性能优化:大文件上传使用分块或流式上传
// 带验证的提交示例
function ValidatedForm() {
const submit = useSubmit();
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = () => {
// 验证邮箱格式
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setError('请输入有效的邮箱地址');
return;
}
// 提交数据
submit({ email }, { method: 'post', action: '/subscribe' });
};
return (
<div className="subscribe-form">
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError('');
}}
placeholder="输入您的邮箱"
className={error ? 'error' : ''}
/>
{error && <div className="error-message">{error}</div>}
<button onClick={handleSubmit}>订阅</button>
</div>
);
}
七、useSubmit 注意事项
路由上下文:必须在路由组件中使用(在
<Router>
上下文中)数据格式:
对象会被转换为
URLSearchParams
文件上传必须使用
FormData
GET请求:数据会作为查询参数添加到URL
重定向:提交后服务器应返回重定向响应
状态管理:提交不会自动管理加载状态,需自行实现
总结
useSubmit
是 React Router
中处理表单提交的高级工具,特别适合以下场景:
- 自定义表单逻辑:当需要超出标准表单的行为时
- 动态数据提交:根据应用状态生成提交数据
- 非表单元素提交:从按钮或其他UI元素触发提交
- 复杂交互流程:如多步骤表单、乐观更新
- 文件上传:处理二进制数据提交
通过合理使用 useSubmit
,您可以构建更灵活、更强大的表单交互体验,同时保持与 React Router
数据路由模型的无缝集成。