Skip to content

Commit

Permalink
feat: wrap openFilePicker with useCallback (#92)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Forsect and MrKampla authored Apr 24, 2024
1 parent 18a4f6d commit 214505b
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 63 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["plugin:react-hooks/recommended"]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "use-file-picker",
"description": "Simple react hook to open browser file selector.",
"version": "2.1.1",
"version": "2.1.2",
"license": "MIT",
"author": "Milosz Jankiewicz",
"homepage": "https://github.com/Jaaneek/useFilePicker",
Expand Down Expand Up @@ -109,6 +109,7 @@
"eslint-plugin-prettier": "^4.2.1",
"husky": "^4.3.6",
"jest": "^29.5.0",
"prettier": "2.8.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-is": "^17.0.1",
Expand Down
103 changes: 62 additions & 41 deletions src/useFilePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
} from './interfaces';
import { openFileDialog } from './helpers/openFileDialog';
import { useValidators } from './validators/useValidators';
import { Validator } from './validators';

// empty array reference in order to avoid re-renders when no validators are passed as props
const EMPTY_ARRAY: Validator[] = [];

function useFilePicker<
CustomErrors = unknown,
Expand All @@ -20,16 +24,18 @@ function useFilePicker<
multiple = true,
readAs = 'Text',
readFilesContent = true,
validators = [],
validators = EMPTY_ARRAY,
initializeWithCustomParameters,
} = props;

const [plainFiles, setPlainFiles] = useState<File[]>([]);
const [filesContent, setFilesContent] = useState<FileContent<ExtractContentTypeFromConfig<ConfigType>>[]>([]);
const [fileErrors, setFileErrors] = useState<UseFilePickerError<CustomErrors>[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const { onFilesSelected, onFilesSuccessfullySelected, onFilesRejected, onClear } =
useValidators<ConfigType, CustomErrors>(props);
const { onFilesSelected, onFilesSuccessfullySelected, onFilesRejected, onClear } = useValidators<
ConfigType,
CustomErrors
>(props);

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

const parseFile = (file: FileWithPath) =>
new Promise<FileContent<ExtractContentTypeFromConfig<ConfigType>>>(
async (
resolve: (fileContent: FileContent<ExtractContentTypeFromConfig<ConfigType>>) => void,
reject: (reason: UseFilePickerError) => void
) => {
const reader = new FileReader();

//availible reader methods: readAsText, readAsBinaryString, readAsArrayBuffer, readAsDataURL
const readStrategy = reader[`readAs${readAs}` as ReaderMethod] as typeof reader.readAsText;
readStrategy.call(reader, file);

const addError = ({ ...others }: UseFilePickerError) => {
reject({ ...others });
};

reader.onload = async () =>
Promise.all(
validators.map(validator =>
validator.validateAfterParsing(props, file, reader).catch(err => Promise.reject(addError(err)))
)
)
.then(() =>
resolve({
...file,
content: reader.result as string,
name: file.name,
lastModified: file.lastModified,
} as FileContent<ExtractContentTypeFromConfig<ConfigType>>)
const parseFile = useCallback(
(file: FileWithPath) =>
new Promise<FileContent<ExtractContentTypeFromConfig<ConfigType>>>(
async (
resolve: (fileContent: FileContent<ExtractContentTypeFromConfig<ConfigType>>) => void,
reject: (reason: UseFilePickerError) => void
) => {
const reader = new FileReader();

//availible reader methods: readAsText, readAsBinaryString, readAsArrayBuffer, readAsDataURL
const readStrategy = reader[`readAs${readAs}` as ReaderMethod] as typeof reader.readAsText;
readStrategy.call(reader, file);

const addError = ({ ...others }: UseFilePickerError) => {
reject({ ...others });
};

reader.onload = async () =>
Promise.all(
validators.map(validator =>
validator.validateAfterParsing(props, file, reader).catch(err => Promise.reject(addError(err)))
)
)
.catch(() => {});

reader.onerror = () => {
addError({ name: 'FileReaderError', readerError: reader.error, causedByFile: file });
};
}
);
.then(() =>
resolve({
...file,
content: reader.result as string,
name: file.name,
lastModified: file.lastModified,
} as FileContent<ExtractContentTypeFromConfig<ConfigType>>)
)
.catch(() => {});

reader.onerror = () => {
addError({ name: 'FileReaderError', readerError: reader.error, causedByFile: file });
};
}
),
[props, readAs, validators]
);

const openFilePicker = () => {
const openFilePicker = useCallback(() => {
const fileExtensions = accept instanceof Array ? accept.join(',') : accept;
openFileDialog(
fileExtensions,
Expand Down Expand Up @@ -156,7 +165,19 @@ function useFilePicker<
},
initializeWithCustomParameters
);
};
}, [
props,
accept,
clear,
initializeWithCustomParameters,
multiple,
onFilesRejected,
onFilesSelected,
onFilesSuccessfullySelected,
parseFile,
readFilesContent,
validators,
]);

return {
openFilePicker,
Expand Down
50 changes: 30 additions & 20 deletions src/validators/useValidators.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import {
SelectedFilesOrErrors,
ExtractContentTypeFromConfig,
Expand All @@ -14,30 +15,39 @@ export const useValidators = <ConfigType extends UseFilePickerConfig<CustomError
validators,
}: ConfigType) => {
// setup validators' event handlers
const onFilesSelected = (data: SelectedFilesOrErrors<ExtractContentTypeFromConfig<ConfigType>, CustomErrors>) => {
onFilesSelectedProp?.(data as any);
validators?.forEach(validator => {
validator.onFilesSelected(data as any);
});
};
const onFilesSuccessfullySelected = (data: SelectedFiles<ExtractContentTypeFromConfig<ConfigType>>) => {
onFilesSuccessfullySelectedProp?.(data as any);
validators?.forEach(validator => {
validator.onFilesSuccessfullySelected(data);
});
};
const onFilesRejected = (errors: FileErrors<CustomErrors>) => {
onFilesRejectedProp?.(errors);
validators?.forEach(validator => {
validator.onFilesRejected(errors);
});
};
const onClear = () => {
const onFilesSelected = useCallback(
(data: SelectedFilesOrErrors<ExtractContentTypeFromConfig<ConfigType>, CustomErrors>) => {
onFilesSelectedProp?.(data as any);
validators?.forEach(validator => {
validator.onFilesSelected(data as any);
});
},
[onFilesSelectedProp, validators]
);
const onFilesSuccessfullySelected = useCallback(
(data: SelectedFiles<ExtractContentTypeFromConfig<ConfigType>>) => {
onFilesSuccessfullySelectedProp?.(data as any);
validators?.forEach(validator => {
validator.onFilesSuccessfullySelected(data);
});
},
[validators, onFilesSuccessfullySelectedProp]
);
const onFilesRejected = useCallback(
(errors: FileErrors<CustomErrors>) => {
onFilesRejectedProp?.(errors);
validators?.forEach(validator => {
validator.onFilesRejected(errors);
});
},
[validators, onFilesRejectedProp]
);
const onClear = useCallback(() => {
onClearProp?.();
validators?.forEach(validator => {
validator.onClear?.();
});
};
}, [validators, onClearProp]);

return {
onFilesSelected,
Expand Down
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6464,7 +6464,7 @@ eslint-plugin-prettier@^4.2.1:

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

eslint-plugin-react@^7.31.11:
Expand Down Expand Up @@ -11039,6 +11039,11 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"

prettier@2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==

"prettier@>=2.2.1 <=2.3.0":
version "2.3.0"
resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz"
Expand Down

0 comments on commit 214505b

Please sign in to comment.