背景:选择文件有复杂的UI需求。
结论:
封装一个选择文件组件,关键点有:
- Children 响应 onClick,以便实现 UI 无关
- 通过换 key 解决相同文件 onChange 不触发问题
最近有文件选择的需求,而且对 UI 有一定的要求。比如简单的
还有更复杂的点击重新上传。
然而默认的很丑。
那怎么办?
可以看看 antd 的实现 ant-design.antgroup.com/components/…。
可以看到 input file 隐藏在了后面,大概率就能联想到通过间接的方式点击了 input file,这个不难实现,domInput.click()即可。
于是,UI 和原生 input file 没关系了,那么灵活性就很高了。
我们可以很快写出第一版组件
const noneStyle = {
display: 'none',
};
function File(props){
const { onChange } = props;
const refInput = useRef(null);
const handleClick = useCallback(() => {
refInput.current.click();
}, [])
return (
<>
<input ref={refInput} type="file" onChange={onChange} style={noneStyle} />
<Button onClick={handleClick}>选择文件</Button>
</>
)
}
那如果触发选择的不是一个按钮,而是这种怎么处理?
把 Children 变的更灵活,Children 只需响应 onClick 即可。改改代码是
function File(props){
const { children, onChange } = props;
const refInput = useRef(null);
const handleClick = useCallback(() => {
refInput.current.click();
}, [])
return (
<>
<input ref={refInput} type="file" onChange={onChange} style={noneStyle} />
{children({ onClick: handleClick })}
</>
)
}
调用方代码是这样
function Demo(){
return (
<File onChange={(e) => console.log(e.target.files[0])}>
{({onClick}) => (
<SomeComponent>
<span onClick={onClick}>选择文件</a>
</SomeComponent>
)}
</File>
)
}
实际使用过程中,我们会发现一些 bug,还是这张图
1 UserA 点击触发弹窗,选择了 FileA,呈现在界面上。
2 UserA 在界面上移除了次文件。
3 UserA 再次选择文件,选择了 FileA。问题来了:onChange 不会触发了。
出现上述问题的原因是对于 input file 来说,之前是 FileA 现在还是 FielA,所以不会触发 onChange。
这种问题虽然边缘,但是遇到了还是挺懵逼的,因为很少会注意这种情况,但是想了想 onChange 合情合理。
那怎么规避这种“预期之外”的问题?
问题转换:避免 onChange 不触发 =》每次 onChange 都可以触发 =》对于 input file 来说,每次内容都是变化的 =》FileA 没法变,那么可以改变 input 实例。=》通过 key 变更 input 实例。
代码变成这样,(再补充 accept)。
细心的小伙伴会发现 props 的 onChange 改成了 onSelect。因为 onChange 合情合理,这个命名的行为就是如此。所以我们需要换个名字 onSelect。
import React, { useCallback, useRef, useState } from 'react';
interface FileProps {
children: ({ onClick }) => React.ReactNode;
onSelect: (files: FileList | null) => void;
accept?: string;
}
const noneStyle = {
display: 'none',
};
const File = (props: FileProps) => {
const [n, setN] = useState(1);
const { children, accept, onSelect } = props;
const refInput = useRef<HTMLInputElement>(null);
const handleClick = useCallback(() => {
refInput.current?.click();
}, []);
const handleChange = useCallback(
(e) => {
setN((prev) => prev + 1);
onSelect(e.target.files);
},
[onSelect]
);
return (
<>
<input
// 每次渲染新的,这样 onChange 总能响应
key={n}
ref={refInput}
type="file"
accept={accept}
style={noneStyle}
onChange={handleChange}
/>
{children({ onClick: handleClick })}
</>
);
};