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

React组件内存泄漏警告 异步表单校验未生效跳转问题排查

问题排查结论

异步校验失效、页面错误跳转原因

  • 核心逻辑错误:isCodeExists是基于fetch的异步函数,在isValid中调用它时,仅挂载了.then回调,但没有等待异步请求返回,就直接同步执行了函数末尾的return true。写在.then里的return false只属于Promise回调的返回值,不会成为外层isValid的返回结果,因此同步校验(正则、前缀判断)通过后,isValid会立刻返回true,handleSubmit拿到结果直接触发nextStep()跳页。
  • 时序问题:等接口返回状态码2的时候,组件已经因为跳页被卸载,此时执行setErrorMessage、setHasError不仅不会渲染错误提示,还会触发React的未挂载组件状态更新警告。
  • 次要逻辑错误:接口返回2时设置的错误提示和前缀校验提示完全重复,属于复制代码未修改的笔误,不符合错误提示的对应逻辑。

内存泄漏警告原因

  • 跳页后当前PaperFormPage组件被卸载,但之前发出的fetch请求没有被中断,请求完成后仍会尝试执行状态更新逻辑,React检测到对已卸载组件的状态更新操作,就会抛出内存泄漏警告。
  • 额外逻辑瑕疵:isCodeExists函数开头就重置错误状态,和onUserInput中的重置逻辑重复,容易导致校验过程中错误提示闪烁。
修复方案
  1. 引入AbortController管理异步请求,组件卸载时自动取消未完成的请求,解决内存泄漏问题
  2. 将isValid、handleSubmit改为异步函数,等待接口校验结果返回后再决定是否允许跳页
  3. 修正错误提示的对应关系,补全请求异常的分支处理

核心修改后的代码如下:

import React, { useState, useEffect, useRef } from 'react';
import TextboxScannerElement from './TextboxScannerElement';

export default function PaperFormPage(props) {
  const [pageUserInput, setPageUserInput] = useState([]);
  const [hasError, setHasError] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  // 存储请求控制器,用于取消未完成的请求
  const abortControllerRef = useRef(null);

  // 组件卸载时取消所有未完成请求
  useEffect(() => {
    return () => {
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
    }
  }, []);

  useEffect(() => {
    if (props.pageData !== undefined) {
      setPageUserInput([...props.pageData]);
    }
  }, [props.pageData]);

  function onUserInput(element, value) {
    setErrorMessage('');
    setHasError(false);

    if (typeof element === 'string') {
      let pageUserInputCopy = [...pageUserInput];
      let index = pageUserInputCopy.findIndex((obj) => obj.key === element);
      let itemCopy = { ...pageUserInputCopy[index] };

      itemCopy['userInput'] = value;
      pageUserInputCopy[index] = itemCopy;

      setPageUserInput(pageUserInputCopy);
    }
  }

  function isCodeExists(userInput) {
    // 取消上一次重复发起的未完成请求
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    abortControllerRef.current = new AbortController();

    let code = encodeURIComponent(userInput);
    const testURL =
      loris.BaseURL +
      '/biosample/ajax/validateScannableCode.php' +
      '?scannable_code=' + code;

    return fetch(
      testURL,
      {
        credentials: 'include',
        method: 'GET',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
        signal: abortControllerRef.current.signal
      })
      .then((response) => response.json())
      .then((responseData) => {
        return responseData == 2 ? 2 : 1;
      })
      .catch(error => {
        // 主动取消的请求不需要抛出警告
        if (error.name !== 'AbortError') {
          console.warn(error);
        }
        // 请求异常返回0,按校验不通过处理
        return 0;
      });
  }

  async function isValid(userInput) {
    if (!/^[a-zA-Z0-9]+$/.test(userInput)) {
      setErrorMessage('Cannot contain non-alphanumeric characters.');
      setHasError(true);
      return false;
    }

    if (!/^.{10,}$/.test(userInput)) {
      setErrorMessage('Must be at least 10 characters in length.');
      setHasError(true);
      return false;
    }

    if (!userInput.startsWith('HBCD')) {
      setErrorMessage('Paper Form ID must begin with "HBCD".');
      setHasError(true);
      return false;
    }

    // 等待异步校验结果返回后再判断
    const res = await isCodeExists(userInput);
    if (res == 2) {
      // 替换为接口返回2对应的实际错误提示
      setErrorMessage('This scannable code already exists.');
      setHasError(true);
      return false;
    }
    if (res == 0) {
      setErrorMessage('Validation request failed, please try again.');
      setHasError(true);
      return false;
    }

    return true;
  }

  async function handleSubmit(e) {
    e.preventDefault();
    const passValidation = await isValid(pageUserInput[0].userInput);
    if (passValidation) {
      props.updatePageData(pageUserInput);
      props.nextStep();
    }
  }

  const form =
    pageUserInput.length === 0 ? null : (
      <TextboxScannerElement
        keyStr={pageUserInput[0].key}
        label={pageUserInput[0].label}
        name={pageUserInput[0].key}
        onUserInput={onUserInput}
        value={pageUserInput[0].userInput}
        errorMessage={errorMessage}
      />
    );

  return (
    <>
      <div className="instructions">
        <strong>Instructions:</strong>
        <p>Scan the Paper Form found in the kit.</p>
      </div>
      <FormElement
        className="FormElement"
        name="form"
        fileUpload={false}
        onSubmit={handleSubmit}
        onUserInput={onUserInput}
      >
        {form}
        <ButtonElement label="Next" onUserInput={onUserInput} />
      </FormElement>
    </>
  );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 09:36:18