如何将React Hook Form Controller获取的文件值存入Redux并复用
实现方案
1. 修正现有文件字段的RHF绑定逻辑
原生input[type=file]存在安全限制,不支持外部手动赋值value属性,你当前直接把RHF的value传给组件会触发控制台报错,先调整Controller代码:
<Controller name={name} control={control} defaultValue={null} render={({ field: { onChange, onBlur, ref } }) => ( <ReactHookFormSelector selector={selector} type={type} multiline={multiline} inputRef={ref} onBlur={onBlur} onChange={(event) => { const selectedFiles = event.target.files; console.log(selectedFiles); onChange(selectedFiles?.length ? selectedFiles : null); // 如果需要支持重复选择同一个文件,打开下方注释 // event.target.value = '' }} /> )} />
不要给file类型的输入框传value属性,只需要在用户选文件时把拿到的FileList交给RHF托管即可。
2. 提交时将文件存入Redux
Redux默认的序列化检查会对File/FileList类实例抛出警告,需要先配置忽略规则,再在表单提交回调中dispatch存值逻辑。
Redux侧配置(Redux Toolkit为例)
export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { // 替换成你自己的存文件action type和store路径 ignoredActions: ['form/saveFormFiles'], ignoredPaths: ['form.fileCache.fileList'] } }) });
表单提交逻辑
const onSubmit = (formValues) => { // 替换成你实际的文件字段名 const { uploadFile, ...restValues } = formValues; dispatch( saveFormFiles({ fileList: uploadFile, relatedFormValues: restValues }) ); };
3. 在另一个file输入组件中复用已存文件
浏览器安全规则禁止直接给input[type=file]赋值,所以要通过「自定义视觉展示层+透明原生输入框覆盖」的方式实现回显效果,不影响原生文件选择功能:
import { useSelector } from 'react-redux'; import { TextField, InputAdornment, IconButton, Typography } from '@mui/material'; import ClearIcon from '@mui/icons-material/Clear'; // 从Redux读取之前存的文件和关联值 const storedFileCache = useSelector(state => state.form.fileCache); <Controller name="secondFileField" control={secondFormControl} defaultValue={storedFileCache?.fileList || null} render={({ field: { onChange, onBlur, ref, value } }) => { // 优先展示当前新选的文件,没有则用Redux存的历史文件 const showFiles = value || storedFileCache?.fileList; const fileNameText = showFiles?.length ? Array.from(showFiles).map(file => file.name).join(', ') : '请选择文件'; return ( <TextField type="file" fullWidth label="上传文件" onBlur={onBlur} inputRef={ref} onChange={(e) => { const newFiles = e.target.files; onChange(newFiles?.length ? newFiles : null); }} InputProps={{ startAdornment: ( <InputAdornment position="start" sx={{ maxWidth: '75%', overflow: 'hidden' }}> <Typography variant="body2" noWrap>{fileNameText}</Typography> </InputAdornment> ), endAdornment: showFiles ? ( <InputAdornment position="end"> <IconButton size="small" onClick={(e) => { e.stopPropagation(); onChange(null); }} > <ClearIcon fontSize="small" /> </IconButton> </InputAdornment> ) : null }} // 将原生file输入框设为透明覆盖在表层,既保留原生选择能力,又隐藏默认的文件名显示 inputProps={{ sx: { opacity: 0, position: 'absolute', width: '100%', height: '100%', cursor: 'pointer' } }} /> ) }} />
如果你不需要在输入框上展示已存的文件名,只需要提交时带上之前的文件,直接从Redux取文件合并到新表单的提交参数里即可,不需要修改输入框代码。
注意:不要尝试通过DOM操作直接修改file输入框的value属性,所有现代浏览器都会拦截该操作并抛出
InvalidStateError,自定义视觉层是目前唯一兼容的实现方案。
额外提示
- 单页内跳转场景下,Redux存File对象可以正常使用;如果需要支持页面刷新后保留文件,选文件后用
FileReader把文件转成base64字符串或ArrayBuffer存储,回显时再转回File对象即可。 - 多文件字段场景下,按字段名做key分区存储,避免不同字段的文件数据互相覆盖。
内容的提问来源于stack exchange,提问作者Gulshan Jangra
相关产品推荐
相关产品推荐

