React + Formik + Papaparse:无法访问更新的状态值,而是未定义

React + Formik + Papaparse: cannot access updated state value and instead get undefined

我正在尝试使用 React + Formik + 是的创建一个多步骤表单,我已经完成了大部分逻辑,但我应该处理 csv 文件的这一部分。我创建了一个单独的 CSVFileUploadField 组件并使用了 FormikuseField() 钩子。

Formik 和 Yup 验证文件数据。我需要首先确保用户上传的csv文件是有效的,并且不包含任何奇怪的格式问题。我正在使用 papaparse 来解析文件,并且能够检测到错误并更新状态。我打算使用此错误字段来确定是否可以进一步验证文件(例如检查行数和列数),但 csvFileError 状态始终未定义。非常感谢任何 hints/help。

CSVFileUploadField.tsx

import { useField } from "formik";
import React, { InputHTMLAttributes, useEffect, useRef, useState } from "react";
import Papa from "papaparse";
/* ...truncated import */

function CSVFileUploadField({ color: _, ...props }: TCSVFileUploadField) {
  const [field, { error, value }, { setValue, setError }] = useField(props);
  const dispatch: Dispatch = useDispatch();
  const [csvFileError, setCsvFileError] = useState<string>(undefined);
  const [fileName, setFileName] = useState<string>("");
  const [csvData, setCsvData] = useState<unknown[]>([]);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const classes = useStyle();

  // if the user goes to another part of the form and comes back, need to reinitialize the filename
  useEffect(() => {
    setFileName((previousValue) => (value ? value.name : "No file selected"));
  }, [value]);

  const synchronizeErrors = (message: string) => {
    setError(message); // form field error, but this gets overriden by further yup validation
    setCsvFileError((prevMessage) => message); // component state
    dispatch(taskFileErrors(message)); // redux state which is used to disable Next/Submit button in case of error
  };

  const processChunk = function ({ data, errors, meta }, parser) {
    // this is supposed to callback after reading every 1 MB data
    if (csvFileError) {
      parser.abort();
    }

    if (errors.length > 0) {
      // just use the first parsing error
      synchronizeErrors(errors[0].message);
    } else {
      setCsvData((previousData) => {
        let newData = previousData;
        data.forEach((row) => newData.push(row));
        return newData;
      });
    }
  };

  const parseCsv = (file: File) => {
    const papaParseOptions: Papa.ParseConfig = {
      chunkSize: 1024 * 1024 * 1, // 1 MB
      chunk: processChunk,
    };

    Papa.parse(file, papaParseOptions);
  };

  const handleFileChange = function (
    event: React.ChangeEvent<HTMLInputElement>,
  ) {
    const fileObj = event.target.files[0];

    if (fileObj) {
      // // clear any previous errors
      synchronizeErrors(undefined);
      setValue(fileObj);
      setFileName((previousName) => fileObj.name);
      parseCsv(fileObj);          // this is supposed to update the error states
      console.log(csvFileError);  // <-- this is always undefined, even with setTimeout(() => console.log(csvFileError), 5000)
    }
  };

  return (
    <FormControl error={!!error} className={classes.formControl}>
      <FormLabel htmlFor={field.name}>{props.label}</FormLabel>
      <input
        name={field.name}
        onChange={handleFileChange}
        {...props}
        accept={props.accept || ".csv, .tsv"}
        id={field.name}
        type="file"
        ref={fileInputRef}
        style={{ display: "none" }}
      />
      <Box mt={2} display="flex" alignItems="center">
        <Button
          onClick={(_) => {
            fileInputRef.current.click();
          }}
          className={classes.button}
        >
          Choose CSV File
        </Button>
        <Typography className={classes.fileNameLabel}>{fileName}</Typography>
      </Box>

      {(error || csvFileError) && (
        <FormHelperText error={!!error || !!csvFileError}>
          {error || csvFileError}
        </FormHelperText>
      )}
    </FormControl>
  );
}

export default CSVFileUploadField;

编辑:

我创建了一个简化的 codesandbox 演示 here。上传格式错误的 csv 文件后查看控制台。错误呈现为 HTML 但它在控制台中未定义。我想使用错误来决定是否要根据其他条件进一步验证此文件。

原来我的猴脑已经忘记了过时的状态。

  • 如果您是 运行 一个“仅限浏览器”的代码,组件状态将是陈旧的值。这种代码应该在useEffect().
  • 里面
  • 如果您的代码实际上会使用状态将某些内容呈现给 DOM,您将在那里获得正确的值。

就我而言,解析 CSV 文件是一项仅限浏览器的任务。所以,我不得不稍微改变一下。


function CSVFileUploadField({ color: _, ...props }: TCSVFileUploadField) {
  const [field, { error, value }, { setValue, setError }] = useField(props);

  const dispatch: Dispatch = useDispatch();

  const [csvFileError, setCsvFileError] = useState<string>(undefined);
  const [fileName, setFileName] = useState<string>("");
  const [fileReading, setFileReading] = useState<boolean>(false);
  const [numRows, setNumRows] = useState<number>(0);
  const [numCols, setNumCols] = useState<number>(0);
  const [progress, setProgress] = useState<number>(0);

  const fileInputRef = useRef<HTMLInputElement>(null);

  const classes = useStyle();

  useEffect(() => {
    setFileName((previousValue) => (value ? value.name : "No file selected"));
  }, [value]);

  useEffect(() => {
    // once file reading has ended, reset progress
    if (!fileReading) {
      setProgress(0);
    }

    // once reading is done we have concrete values
    if (value && numRows > 0 && numCols > 0 && !error && !csvFileError) {
      // no errors, send the row and column numbers to redux to use them in the next step of form
      dispatch(taskFileSelected(numRows, numCols));
    }
  }, [fileReading]);

  const synchronizeErrors = (message: string) => {
    setError(message); // form field error, but this gets overriden by further yup validation
    setCsvFileError((prevMessage) => message); // component state
    dispatch(taskFileErrors(message)); // redux state which is used to disable Next/Submit button in case of error
  };

  const parseCsv = (fileObj: File) => {
    const fileSize = fileObj.size;

    const parseConfig: Papa.ParseConfig = {
      worker: true, // use worker thread
      chunkSize: 1024 * 1024 * 15, // 15 MB
      chunk: ({ data, errors, meta }, parser) => {
        if (errors.length > 0) {
          synchronizeErrors(errors[0].message);
        } else {
          setNumRows((prevValue) => prevValue + data.length);
          setNumCols(data[0].length);
          setProgress((prevValue) =>
            Math.round((meta.cursor / fileSize) * 100),
          );
        }
      },
      complete: (results, fileObj) => {
        setFileReading((prevValue) => false);
      },
    };

    // when using worker, parse function is async
    // https://www.papaparse.com/faq#workers
    Papa.parse(fileObj, parseConfig);
  };

  const handleFileChange = function (
    event: React.ChangeEvent<HTMLInputElement>,
  ) {
    const fileObj = event.target.files[0];

    if (fileObj) {
      // clear any previous errors
      synchronizeErrors(undefined);
      // reset state
      setNumCols(0);
      setNumRows(0);

      setValue(fileObj);
      setFileName((previousName) => fileObj.name);

      setFileReading((prevValue) => true);
      parseCsv(fileObj);
    }
  };

  return (
    <FormControl error={!!error} className={classes.formControl}>
      <FormLabel htmlFor={field.name}>{props.label}</FormLabel>
      <input
        name={field.name}
        onChange={handleFileChange}
        {...props}
        accept={props.accept || ".csv, .tsv"}
        id={field.name}
        type="file"
        ref={fileInputRef}
        style={{ display: "none" }}
      />
      <Box mt={2} display="flex" alignItems="center">
        <Button
          onClick={(_) => {
            fileInputRef.current.click();
          }}
          className={classes.button}
        >
          Choose CSV File
        </Button>
        <Typography className={classes.fileNameLabel}>{fileName}</Typography>
      </Box>

      {fileReading && <LinearProgressBarWithLabel value={progress} />}

      {(error || csvFileError) && (
        <FormHelperText error={!!error || !!csvFileError}>
          {error || csvFileError}
        </FormHelperText>
      )}
    </FormControl>
  );
}

export default CSVFileUploadField;

最后我也添加了一个进度条,所以,耶!

另外,我可能最终会用 useReducer() 重构它,因为有太多相互关联的状态。