fix: Controlled multiple files upload list sync (#26612)

* init hooks

* Use raf to sync list

* clean up
This commit is contained in:
二货机器人 2020-09-07 16:41:28 +08:00 committed by GitHub
parent 7041af92fb
commit a26517f9ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 150 additions and 33 deletions

View File

@ -19,6 +19,7 @@ import defaultLocale from '../locale/default';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import useForceUpdate from '../_util/hooks/useForceUpdate'; import useForceUpdate from '../_util/hooks/useForceUpdate';
import useFreshState from './useFreshState';
export { UploadProps }; export { UploadProps };
@ -47,11 +48,11 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
const [dragState, setDragState] = React.useState<string>('drop'); const [dragState, setDragState] = React.useState<string>('drop');
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
// `fileListRef` used for internal state sync to avoid control mode set it back when sync update. // Refresh always use fresh data
// `visibleFileList` used for display in UploadList instead. const [getFileList, setFileList] = useFreshState<UploadFile<any>[]>(
// It's a workaround and not the best solution. fileListProp || defaultFileList || [],
const fileListRef = React.useRef(fileListProp || defaultFileList || []); fileListProp,
const visibleFileList = fileListProp || fileListRef.current; );
const upload = React.useRef<any>(); const upload = React.useRef<any>();
@ -63,16 +64,8 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
); );
}, []); }, []);
React.useEffect(() => {
if (fileListProp !== undefined && fileListProp !== fileListRef.current) {
fileListRef.current = fileListProp;
forceUpdate();
}
}, [fileListProp]);
const onChange = (info: UploadChangeParam) => { const onChange = (info: UploadChangeParam) => {
fileListRef.current = info.fileList; setFileList(info.fileList);
forceUpdate();
const { onChange: onChangeProp } = props; const { onChange: onChangeProp } = props;
if (onChangeProp) { if (onChangeProp) {
@ -87,7 +80,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
const targetItem = fileToObject(file); const targetItem = fileToObject(file);
targetItem.status = 'uploading'; targetItem.status = 'uploading';
const nextFileList = fileListRef.current.concat(); const nextFileList = getFileList().concat();
const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid); const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid);
if (fileIndex === -1) { if (fileIndex === -1) {
@ -110,7 +103,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
} catch (e) { } catch (e) {
/* do nothing */ /* do nothing */
} }
const targetItem = getFileItem(file, fileListRef.current); const targetItem = getFileItem(file, getFileList());
// removed // removed
if (!targetItem) { if (!targetItem) {
return; return;
@ -120,12 +113,12 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
targetItem.xhr = xhr; targetItem.xhr = xhr;
onChange({ onChange({
file: { ...targetItem }, file: { ...targetItem },
fileList: fileListRef.current.concat(), fileList: getFileList().concat(),
}); });
}; };
const onProgress = (e: { percent: number }, file: UploadFile) => { const onProgress = (e: { percent: number }, file: UploadFile) => {
const targetItem = getFileItem(file, fileListRef.current); const targetItem = getFileItem(file, getFileList());
// removed // removed
if (!targetItem) { if (!targetItem) {
return; return;
@ -134,12 +127,12 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
onChange({ onChange({
event: e, event: e,
file: { ...targetItem }, file: { ...targetItem },
fileList: fileListRef.current.concat(), fileList: getFileList().concat(),
}); });
}; };
const onError = (error: Error, response: any, file: UploadFile) => { const onError = (error: Error, response: any, file: UploadFile) => {
const targetItem = getFileItem(file, fileListRef.current); const targetItem = getFileItem(file, getFileList());
// removed // removed
if (!targetItem) { if (!targetItem) {
return; return;
@ -149,7 +142,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
targetItem.status = 'error'; targetItem.status = 'error';
onChange({ onChange({
file: { ...targetItem }, file: { ...targetItem },
fileList: fileListRef.current.concat(), fileList: getFileList().concat(),
}); });
}; };
@ -160,7 +153,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
return; return;
} }
const removedFileList = removeFileItem(file, fileListRef.current); const removedFileList = removeFileItem(file, getFileList());
if (removedFileList) { if (removedFileList) {
file.status = 'removed'; file.status = 'removed';
@ -189,11 +182,13 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
if (result === false) { if (result === false) {
// Get unique file list // Get unique file list
const uniqueList: UploadFile<any>[] = []; const uniqueList: UploadFile<any>[] = [];
fileListRef.current.concat(fileListArgs.map(fileToObject)).forEach(f => { getFileList()
if (uniqueList.every(uf => uf.uid !== f.uid)) { .concat(fileListArgs.map(fileToObject))
uniqueList.push(f); .forEach(f => {
} if (uniqueList.every(uf => uf.uid !== f.uid)) {
}); uniqueList.push(f);
}
});
onChange({ onChange({
file, file,
@ -212,7 +207,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
onSuccess, onSuccess,
onProgress, onProgress,
onError, onError,
fileList: fileListRef.current, fileList: getFileList(),
upload: upload.current, upload: upload.current,
forceUpdate, forceUpdate,
})); }));
@ -251,7 +246,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
return ( return (
<UploadList <UploadList
listType={listType} listType={listType}
items={visibleFileList} items={getFileList(true)}
previewFile={previewFile} previewFile={previewFile}
onPreview={onPreview} onPreview={onPreview}
onDownload={onDownload} onDownload={onDownload}
@ -279,9 +274,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
prefixCls, prefixCls,
{ {
[`${prefixCls}-drag`]: true, [`${prefixCls}-drag`]: true,
[`${prefixCls}-drag-uploading`]: fileListRef.current.some( [`${prefixCls}-drag-uploading`]: getFileList().some(file => file.status === 'uploading'),
file => file.status === 'uploading',
),
[`${prefixCls}-drag-hover`]: dragState === 'dragover', [`${prefixCls}-drag-hover`]: dragState === 'dragover',
[`${prefixCls}-disabled`]: disabled, [`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',

View File

@ -17,6 +17,15 @@ describe('Upload', () => {
beforeEach(() => setup()); beforeEach(() => setup());
afterEach(() => teardown()); afterEach(() => teardown());
// Mock for rc-util raf
window.requestAnimationFrame = (callback) => {
window.setTimeout(callback, 16);
};
window.cancelAnimationFrame = (id) => {
window.clearTimeout(id);
};
// https://github.com/react-component/upload/issues/36 // https://github.com/react-component/upload/issues/36
it('should get refs inside Upload in componentDidMount', () => { it('should get refs inside Upload in componentDidMount', () => {
let ref; let ref;
@ -255,6 +264,7 @@ describe('Upload', () => {
}); });
it('should be controlled by fileList', () => { it('should be controlled by fileList', () => {
jest.useFakeTimers();
const fileList = [ const fileList = [
{ {
uid: '-1', uid: '-1',
@ -266,8 +276,11 @@ describe('Upload', () => {
const ref = React.createRef(); const ref = React.createRef();
const wrapper = mount(<Upload ref={ref} />); const wrapper = mount(<Upload ref={ref} />);
expect(ref.current.fileList).toEqual([]); expect(ref.current.fileList).toEqual([]);
wrapper.setProps({ fileList }); wrapper.setProps({ fileList });
jest.runAllTimers();
expect(ref.current.fileList).toEqual(fileList); expect(ref.current.fileList).toEqual(fileList);
jest.useRealTimers();
}); });
describe('util', () => { describe('util', () => {

View File

@ -26,6 +26,14 @@ const fileList = [
]; ];
describe('Upload List', () => { describe('Upload List', () => {
// Mock for rc-util raf
window.requestAnimationFrame = callback => {
window.setTimeout(callback, 16);
};
window.cancelAnimationFrame = id => {
window.clearTimeout(id);
};
// jsdom not support `createObjectURL` yet. Let's handle this. // jsdom not support `createObjectURL` yet. Let's handle this.
const originCreateObjectURL = window.URL.createObjectURL; const originCreateObjectURL = window.URL.createObjectURL;
window.URL.createObjectURL = jest.fn(() => ''); window.URL.createObjectURL = jest.fn(() => '');
@ -913,4 +921,51 @@ describe('Upload List', () => {
wrapper.setProps({ showUploadList: false }); wrapper.setProps({ showUploadList: false });
expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(false); expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(false);
}); });
// https://github.com/ant-design/ant-design/issues/26536
it('multiple file upload should keep the internal fileList async', async () => {
jest.useFakeTimers();
const uploadRef = React.createRef();
const MyUpload = () => {
const [testFileList, setTestFileList] = React.useState([]);
return (
<Upload
ref={uploadRef}
fileList={testFileList}
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
multiple
onChange={info => {
setTestFileList([...info.fileList]);
}}
>
<button type="button">Upload</button>
</Upload>
);
};
mount(<MyUpload />);
// Mock async update in a frame
const files = ['light', 'bamboo', 'little'];
/* eslint-disable no-await-in-loop */
for (let i = 0; i < files.length; i += 1) {
await Promise.resolve();
uploadRef.current.onStart({
uid: files[i],
name: files[i],
});
}
/* eslint-enable */
expect(uploadRef.current.fileList).toHaveLength(files.length);
jest.runAllTimers();
expect(uploadRef.current.fileList).toHaveLength(files.length);
jest.useRealTimers();
});
}); });

View File

@ -0,0 +1,56 @@
import { useRef, useEffect } from 'react';
import raf from 'rc-util/lib/raf';
import useForceUpdate from '../_util/hooks/useForceUpdate';
// Note. Only for upload usage. Do not export to global util hooks
export default function useFreshState<T>(
defaultValue: T,
propValue?: T,
): [(displayValue?: boolean) => T, (newValue: T) => void] {
const valueRef = useRef(defaultValue);
const forceUpdate = useForceUpdate();
const rafRef = useRef<number>();
// Set value
function setValue(newValue: T) {
valueRef.current = newValue;
forceUpdate();
}
function cleanUp() {
raf.cancel(rafRef.current!);
}
function rafSyncValue(newValue: T) {
cleanUp();
rafRef.current = raf(() => {
setValue(newValue);
});
}
// Get value
function getValue(displayValue = false) {
if (displayValue) {
return propValue || valueRef.current;
}
return valueRef.current;
}
// Effect will always update in a next frame to avoid sync state overwrite current processing state
useEffect(() => {
if (propValue) {
rafSyncValue(propValue);
}
}, [propValue]);
// Clean up
useEffect(
() => () => {
cleanUp();
},
[],
);
return [getValue, setValue];
}

View File

@ -147,7 +147,7 @@
"rc-tree": "~3.9.0", "rc-tree": "~3.9.0",
"rc-tree-select": "~4.1.1", "rc-tree-select": "~4.1.1",
"rc-trigger": "~4.4.0", "rc-trigger": "~4.4.0",
"rc-upload": "~3.3.0", "rc-upload": "~3.3.1",
"rc-util": "^5.1.0", "rc-util": "^5.1.0",
"scroll-into-view-if-needed": "^2.2.25", "scroll-into-view-if-needed": "^2.2.25",
"warning": "^4.0.3" "warning": "^4.0.3"