You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript React中useRef获取表单值提交API的类型错误问题

解决React + TypeScript中useRef表单元素的类型错误问题

我来帮你搞定这个TypeScript类型不匹配的问题,核心原因是useRef的current值可能为null,而FormData构造函数不接受null类型,同时React的ref属性要求current的类型是HTMLFormElement | null,不能是undefined。下面给你两种靠谱的解决方案:


方案1:添加null检查(最安全,推荐)

在使用formElement.current之前先判断它是否存在,TypeScript会自动缩小类型范围,确认current是有效的HTMLFormElement:

const submit = async (event: React.FormEvent) => {
  event.preventDefault();
  setState((prev) => ({ ...prev, loading: true }));
  
  // 先检查表单元素是否存在,避免null报错
  if (!formElement.current) {
    setState((prev) => ({ ...prev, loading: false }));
    return;
  }
  
  const body = new URLSearchParams(new FormData(formElement.current));
  const response = await fetch(baseUrl, { body, method });
  const content = await response.json();
  setState((prev) => ({ ...prev, response, loading: false }));
  
  if (response.status === 200 && content.jwtToken) {
    dispatch({ type: BEARER_TOKEN, value: content.jwtToken });
  }
};

同时保持useRef的正确类型定义:

const formElement = useRef<HTMLFormElement>(null);

这种写法既满足React ref的类型要求,又能在运行时避免空值错误,是最稳妥的选择。


方案2:非空断言(适用于确定表单一定存在的场景)

如果你能100%确定表单组件一定会被渲染(不会被条件渲染隐藏),可以用非空断言!告诉TypeScript current一定不为null:

const formElement = useRef<HTMLFormElement>(null);

const submit = async (event: React.FormEvent) => {
  event.preventDefault();
  setState((prev) => ({ ...prev, loading: true }));
  
  // 非空断言,仅当表单必然存在时使用
  const body = new URLSearchParams(new FormData(formElement.current!));
  const response = await fetch(baseUrl, { body, method });
  const content = await response.json();
  setState((prev) => ({ ...prev, response, loading: false }));
  
  if (response.status === 200 && content.jwtToken) {
    dispatch({ type: BEARER_TOKEN, value: content.jwtToken });
  }
};

⚠️ 注意:非空断言要谨慎使用,如果表单有可能不渲染,运行时会抛出错误。


修正后的完整Poster组件代码

这里用方案1的写法,同时做了几个小优化:

import React, { useContext, useRef, useState } from 'react';
import { BEARER_TOKEN, Dispatch, Global } from '../globalState';

interface PropsInterface {
  auth: boolean;
  baseUrl: string;
  keys: string;
  method: string;
}

const Poster: React.FC<PropsInterface> = (props: PropsInterface) => {
  const { global } = useContext(Global);
  const { dispatch } = useContext(Dispatch);
  const [state, setState] = useState({
    auth: props.auth,
    baseUrl: props.baseUrl,
    keys: props.keys.split(','),
    method: props.method,
    loading: false,
    response: null
  });
  const { auth, baseUrl, keys, method, loading } = state;
  const { bearer } = global;
  
  const baseUrlRef = useRef<HTMLInputElement>(null);
  // 保持正确的类型定义:HTMLFormElement | null
  const formElement = useRef<HTMLFormElement>(null);

  const submit = async (event: React.FormEvent) => {
    event.preventDefault();
    setState((prev) => ({ ...prev, loading: true }));
    
    // 检查表单元素是否存在
    if (!formElement.current) {
      setState((prev) => ({ ...prev, loading: false }));
      return;
    }
    
    const body = new URLSearchParams(new FormData(formElement.current));
    const response = await fetch(baseUrl, { body, method });
    const content = await response.json();
    
    setState((prev) => ({ ...prev, response, loading: false }));
    
    if (response.status === 200 && content.jwtToken) {
      dispatch({ type: BEARER_TOKEN, value: content.jwtToken });
    }
  };

  const copy = (event: React.MouseEvent) => {
    event.preventDefault();
    if (baseUrlRef.current) baseUrlRef.current.select();
    document.execCommand('copy');
    const button = event.target as HTMLButtonElement;
    button.focus();
  };

  return (
    <form className='poster' onSubmit={submit} ref={formElement}>
      <div className='poster--where'>
        <div className='poster--where-container-method'>
          <div className='poster--method'>{method}</div>
          <input type='text' value={baseUrl} ref={baseUrlRef} readOnly className='poster--url' />
        </div>
        <div className='poster--where-container-actions'>
          <button className='poster--send' disabled={loading}>Send</button>
          <button className='poster--copy' onClick={copy}>
            Copy
          </button>
        </div>
      </div>
      {auth && (
        <div className='poster--auth'>
          <div className='poster--auth-type'>Bearer Token</div>
          <input type='text' value={bearer} readOnly className='poster--url' />
        </div>
      )}
      {keys.map((item, index) => (
        <div className='poster--keys-values' key={index}>
          <div className='poster--key-title'>{item.trim()}:</div>
          <input type='text' id={item.trim()} className='poster--value-title' />
        </div>
      ))}
    </form>
  );
};

export { Poster };

优化点说明:

  • 初始化了loading和response字段,避免类型报错
  • 给Send按钮添加了disabled={loading},防止重复提交
  • 对keys数组的item做了trim()处理,避免分割后出现多余空格

内容的提问来源于stack exchange,提问作者Bill

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 13:47:41