Skip to content

Commit 214505b

Browse files
ForsectMrKampla
andauthored
feat: wrap openFilePicker with useCallback (#92)
* feat: wrap openFilePicker with useCallback * Add memoization to validator functions and a stable reference to empty validator array * Bump version to v2.1.2 --------- Co-authored-by: MrKampla <mrkampla@gmail.com>
1 parent 18a4f6d commit 214505b

File tree

5 files changed

+103
-63
lines changed

5 files changed

+103
-63
lines changed

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["plugin:react-hooks/recommended"]
3+
}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "use-file-picker",
33
"description": "Simple react hook to open browser file selector.",
4-
"version": "2.1.1",
4+
"version": "2.1.2",
55
"license": "MIT",
66
"author": "Milosz Jankiewicz",
77
"homepage": "https://github.com/Jaaneek/useFilePicker",
@@ -109,6 +109,7 @@
109109
"eslint-plugin-prettier": "^4.2.1",
110110
"husky": "^4.3.6",
111111
"jest": "^29.5.0",
112+
"prettier": "2.8.8",
112113
"react": "^18.2.0",
113114
"react-dom": "^18.2.0",
114115
"react-is": "^17.0.1",

src/useFilePicker.ts

+62-41
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
} from './interfaces';
1111
import { openFileDialog } from './helpers/openFileDialog';
1212
import { useValidators } from './validators/useValidators';
13+
import { Validator } from './validators';
14+
15+
// empty array reference in order to avoid re-renders when no validators are passed as props
16+
const EMPTY_ARRAY: Validator[] = [];
1317

1418
function useFilePicker<
1519
CustomErrors = unknown,
@@ -20,16 +24,18 @@ function useFilePicker<
2024
multiple = true,
2125
readAs = 'Text',
2226
readFilesContent = true,
23-
validators = [],
27+
validators = EMPTY_ARRAY,
2428
initializeWithCustomParameters,
2529
} = props;
2630

2731
const [plainFiles, setPlainFiles] = useState<File[]>([]);
2832
const [filesContent, setFilesContent] = useState<FileContent<ExtractContentTypeFromConfig<ConfigType>>[]>([]);
2933
const [fileErrors, setFileErrors] = useState<UseFilePickerError<CustomErrors>[]>([]);
3034
const [loading, setLoading] = useState<boolean>(false);
31-
const { onFilesSelected, onFilesSuccessfullySelected, onFilesRejected, onClear } =
32-
useValidators<ConfigType, CustomErrors>(props);
35+
const { onFilesSelected, onFilesSuccessfullySelected, onFilesRejected, onClear } = useValidators<
36+
ConfigType,
37+
CustomErrors
38+
>(props);
3339

3440
const clear: () => void = useCallback(() => {
3541
setPlainFiles([]);
@@ -42,45 +48,48 @@ function useFilePicker<
4248
onClear?.();
4349
}, [clear, onClear]);
4450

45-
const parseFile = (file: FileWithPath) =>
46-
new Promise<FileContent<ExtractContentTypeFromConfig<ConfigType>>>(
47-
async (
48-
resolve: (fileContent: FileContent<ExtractContentTypeFromConfig<ConfigType>>) => void,
49-
reject: (reason: UseFilePickerError) => void
50-
) => {
51-
const reader = new FileReader();
52-
53-
//availible reader methods: readAsText, readAsBinaryString, readAsArrayBuffer, readAsDataURL
54-
const readStrategy = reader[`readAs${readAs}` as ReaderMethod] as typeof reader.readAsText;
55-
readStrategy.call(reader, file);
56-
57-
const addError = ({ ...others }: UseFilePickerError) => {
58-
reject({ ...others });
59-
};
60-
61-
reader.onload = async () =>
62-
Promise.all(
63-
validators.map(validator =>
64-
validator.validateAfterParsing(props, file, reader).catch(err => Promise.reject(addError(err)))
65-
)
66-
)
67-
.then(() =>
68-
resolve({
69-
...file,
70-
content: reader.result as string,
71-
name: file.name,
72-
lastModified: file.lastModified,
73-
} as FileContent<ExtractContentTypeFromConfig<ConfigType>>)
51+
const parseFile = useCallback(
52+
(file: FileWithPath) =>
53+
new Promise<FileContent<ExtractContentTypeFromConfig<ConfigType>>>(
54+
async (
55+
resolve: (fileContent: FileContent<ExtractContentTypeFromConfig<ConfigType>>) => void,
56+
reject: (reason: UseFilePickerError) => void
57+
) => {
58+
const reader = new FileReader();
59+
60+
//availible reader methods: readAsText, readAsBinaryString, readAsArrayBuffer, readAsDataURL
61+
const readStrategy = reader[`readAs${readAs}` as ReaderMethod] as typeof reader.readAsText;
62+
readStrategy.call(reader, file);
63+
64+
const addError = ({ ...others }: UseFilePickerError) => {
65+
reject({ ...others });
66+
};
67+
68+
reader.onload = async () =>
69+
Promise.all(
70+
validators.map(validator =>
71+
validator.validateAfterParsing(props, file, reader).catch(err => Promise.reject(addError(err)))
72+
)
7473
)
75-
.catch(() => {});
76-
77-
reader.onerror = () => {
78-
addError({ name: 'FileReaderError', readerError: reader.error, causedByFile: file });
79-
};
80-
}
81-
);
74+
.then(() =>
75+
resolve({
76+
...file,
77+
content: reader.result as string,
78+
name: file.name,
79+
lastModified: file.lastModified,
80+
} as FileContent<ExtractContentTypeFromConfig<ConfigType>>)
81+
)
82+
.catch(() => {});
83+
84+
reader.onerror = () => {
85+
addError({ name: 'FileReaderError', readerError: reader.error, causedByFile: file });
86+
};
87+
}
88+
),
89+
[props, readAs, validators]
90+
);
8291

83-
const openFilePicker = () => {
92+
const openFilePicker = useCallback(() => {
8493
const fileExtensions = accept instanceof Array ? accept.join(',') : accept;
8594
openFileDialog(
8695
fileExtensions,
@@ -156,7 +165,19 @@ function useFilePicker<
156165
},
157166
initializeWithCustomParameters
158167
);
159-
};
168+
}, [
169+
props,
170+
accept,
171+
clear,
172+
initializeWithCustomParameters,
173+
multiple,
174+
onFilesRejected,
175+
onFilesSelected,
176+
onFilesSuccessfullySelected,
177+
parseFile,
178+
readFilesContent,
179+
validators,
180+
]);
160181

161182
return {
162183
openFilePicker,

src/validators/useValidators.ts

+30-20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useCallback } from 'react';
12
import {
23
SelectedFilesOrErrors,
34
ExtractContentTypeFromConfig,
@@ -14,30 +15,39 @@ export const useValidators = <ConfigType extends UseFilePickerConfig<CustomError
1415
validators,
1516
}: ConfigType) => {
1617
// setup validators' event handlers
17-
const onFilesSelected = (data: SelectedFilesOrErrors<ExtractContentTypeFromConfig<ConfigType>, CustomErrors>) => {
18-
onFilesSelectedProp?.(data as any);
19-
validators?.forEach(validator => {
20-
validator.onFilesSelected(data as any);
21-
});
22-
};
23-
const onFilesSuccessfullySelected = (data: SelectedFiles<ExtractContentTypeFromConfig<ConfigType>>) => {
24-
onFilesSuccessfullySelectedProp?.(data as any);
25-
validators?.forEach(validator => {
26-
validator.onFilesSuccessfullySelected(data);
27-
});
28-
};
29-
const onFilesRejected = (errors: FileErrors<CustomErrors>) => {
30-
onFilesRejectedProp?.(errors);
31-
validators?.forEach(validator => {
32-
validator.onFilesRejected(errors);
33-
});
34-
};
35-
const onClear = () => {
18+
const onFilesSelected = useCallback(
19+
(data: SelectedFilesOrErrors<ExtractContentTypeFromConfig<ConfigType>, CustomErrors>) => {
20+
onFilesSelectedProp?.(data as any);
21+
validators?.forEach(validator => {
22+
validator.onFilesSelected(data as any);
23+
});
24+
},
25+
[onFilesSelectedProp, validators]
26+
);
27+
const onFilesSuccessfullySelected = useCallback(
28+
(data: SelectedFiles<ExtractContentTypeFromConfig<ConfigType>>) => {
29+
onFilesSuccessfullySelectedProp?.(data as any);
30+
validators?.forEach(validator => {
31+
validator.onFilesSuccessfullySelected(data);
32+
});
33+
},
34+
[validators, onFilesSuccessfullySelectedProp]
35+
);
36+
const onFilesRejected = useCallback(
37+
(errors: FileErrors<CustomErrors>) => {
38+
onFilesRejectedProp?.(errors);
39+
validators?.forEach(validator => {
40+
validator.onFilesRejected(errors);
41+
});
42+
},
43+
[validators, onFilesRejectedProp]
44+
);
45+
const onClear = useCallback(() => {
3646
onClearProp?.();
3747
validators?.forEach(validator => {
3848
validator.onClear?.();
3949
});
40-
};
50+
}, [validators, onClearProp]);
4151

4252
return {
4353
onFilesSelected,

yarn.lock

+6-1
Original file line numberDiff line numberDiff line change
@@ -6464,7 +6464,7 @@ eslint-plugin-prettier@^4.2.1:
64646464

64656465
eslint-plugin-react-hooks@^4.6.0:
64666466
version "4.6.0"
6467-
resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz"
6467+
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
64686468
integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==
64696469

64706470
eslint-plugin-react@^7.31.11:
@@ -11039,6 +11039,11 @@ prettier-linter-helpers@^1.0.0:
1103911039
dependencies:
1104011040
fast-diff "^1.1.2"
1104111041

11042+
prettier@2.8.8:
11043+
version "2.8.8"
11044+
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
11045+
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
11046+
1104211047
"prettier@>=2.2.1 <=2.3.0":
1104311048
version "2.3.0"
1104411049
resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz"

0 commit comments

Comments
 (0)