React 第五十六节 Router 中useSubmit的使用详解及注意事项

发布于:2025-06-12 ⋅ 阅读:(17) ⋅ 点赞:(0)

前言

useSubmitReact Router v6.4+ 引入的强大钩子,用于以编程方式提交表单数据。
它提供了对表单提交过程的精细控制,特别适合需要自定义提交行为或非标准表单场景的应用。

一、useSubmit 核心用途

  1. 编程式表单提交:不依赖 <form> 元素手动提交数据
  2. 自定义提交逻辑:在提交前/后执行额外操作
  3. 动态表单构建:根据应用状态生成提交数据
  4. 替代传统表单:在复杂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 最佳实践

  1. 数据验证:提交前验证数据

  2. 用户反馈:提交时显示加载状态

  3. 错误处理:捕获并处理提交错误

  4. 编码类型:

    a.简单数据:application/x-www-form-urlencoded

    b.文件上传:multipart/form-data

    c.JSON数据:手动序列化并设置适当headers

  5. 性能优化:大文件上传使用分块或流式上传

// 带验证的提交示例
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 注意事项

  1. 路由上下文:必须在路由组件中使用(在 <Router> 上下文中)

  2. 数据格式:

    对象会被转换为 URLSearchParams

    文件上传必须使用 FormData

  3. GET请求:数据会作为查询参数添加到URL

  4. 重定向:提交后服务器应返回重定向响应

  5. 状态管理:提交不会自动管理加载状态,需自行实现

总结

useSubmitReact Router 中处理表单提交的高级工具,特别适合以下场景:

  1. 自定义表单逻辑:当需要超出标准表单的行为时
  2. 动态数据提交:根据应用状态生成提交数据
  3. 非表单元素提交:从按钮或其他UI元素触发提交
  4. 复杂交互流程:如多步骤表单、乐观更新
  5. 文件上传:处理二进制数据提交

通过合理使用 useSubmit,您可以构建更灵活、更强大的表单交互体验,同时保持与 React Router 数据路由模型的无缝集成