React + Formik + Papaparse:无法访问更新的状态值,而是未定义
React + Formik + Papaparse: cannot access updated state value and instead get undefined
我正在尝试使用 React + Formik + 是的创建一个多步骤表单,我已经完成了大部分逻辑,但我应该处理 csv 文件的这一部分。我创建了一个单独的 CSVFileUploadField
组件并使用了 Formik
的 useField()
钩子。
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()
重构它,因为有太多相互关联的状态。
我正在尝试使用 React + Formik + 是的创建一个多步骤表单,我已经完成了大部分逻辑,但我应该处理 csv 文件的这一部分。我创建了一个单独的 CSVFileUploadField
组件并使用了 Formik
的 useField()
钩子。
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()
重构它,因为有太多相互关联的状态。