We want to upload image files and are able to get the uploaded images from the child component to the parent component through the parameter and a function to set the specific prop we are passing to the child component.
However, we can’t create a new array out of two arrays, one being the local state of the parent component, the other being the newly uploaded images sent from the child.
Parent component — ImageUpload.js
const EntryEditPage = () => {
const [currentImages, setCurrentImages] = useState([]);
const handleImagesUpload = (uploadedImages) => {
console.log('handleImagesUpload -- curIm:', currentImages); // output: right image(s)
console.log('handleImagesUpload -- uplIm:', uploadedImages); // output: right image(s)
console.log('handleImagesUpload -- expected result:', [
...currentImages,
...uploadedImages,
]); // output: []
setCurrentImages((prevState) => [...prevState, ...uploadedImages]);
};
return (
<section>
<ImageUpload
currentImages={currentImages}
handleImagesUpload={(images) => handleImagesUpload(images)}
/>
<section>
{currentImages.map((image) => (
<div
key={image.name}
>
<img src={image.url} alt={image.name} />
/>
</div>
))}
</section>
</section>
)
}
export default EntryEditPage;
Child component:
const ImageUpload = ({ currentImages, handleImagesUpload }) => {
const [uploadedImages, setUploadedImages] = useState([]);
const [isErrorFilesLimit, setIsErrorFilesLimit] = useState(false);
const [errorFileName, setErrorFileName] = useState('');
const [errorFileType, setErrorFileType] = useState('');
const [errorFileSize, setErrorFileSize] = useState('');
const allowedFileExtensions = /.(jpe?g|png)$/i;
const maxFiles = 10;
const maxFileSize = 10485760; // 10MB per image
const { register } = useFormContext();
const addError = (errors, newError) =>
`${errors}${errors ? 'n' : ''}${newError}`;
const handleChange = (event) => {
const validUploadedImages = [];
let currentErrorsFileName = '';
let currentErrorsFileType = '';
let currentErrorsFileSize = '';
[...event.target.files].every((file) => {
// check FILES LIMIT
if ([...currentImages, ...validUploadedImages].length >= maxFiles) {
setIsErrorFilesLimit(true);
return false;
}
// check FILE NAME
if (
[...currentImages, ...validUploadedImages].findIndex(
(f) => f.name === file.name
) >= 0
) {
currentErrorsFileName = addError(currentErrorsFileName, file.name);
return true;
}
// check FILE TYPE
if (!file.name.match(allowedFileExtensions)) {
currentErrorsFileType = addError(currentErrorsFileType, file.name);
return true;
}
// check FILE SIZE
if (file.size > maxFileSize) {
currentErrorsFileSize = addError(currentErrorsFileSize, file.name);
return true;
}
// FILE checked and OK
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const constructedFile = {
name: file.name,
url: reader.result,
file,
};
validUploadedImages.push(constructedFile);
};
return true;
});
!currentErrorsFileName || setErrorFileName(currentErrorsFileName);
!currentErrorsFileType || setErrorFileType(currentErrorsFileType);
!currentErrorsFileSize || setErrorFileSize(currentErrorsFileSize);
if (validUploadedImages.length > 0) {
setUploadedImages(validUploadedImages);
}
console.log(validUploadedImages); // right images get printed
handleImagesUpload(validUploadedImages);
};
return (
<div>
<section>
<p className={styles.dropzoneAreaSubtext}>
Supported formats: JPEG and PNG
</p>
<input
{...register('images')}
id="image-input"
multiple
type="file"
accept="image/jpeg, image/png"
title="Upload image"
onChange={handleChange}
className={styles.dropzoneAreaInput}
/>
</section>
<div className={styles.dropzoneSubtitleWrapper}>
{uploadedImages.length > 0 && (
<p className={styles.dropzoneSubtitle}>
Uploaded {uploadedImages.length} photos
</p>
)}
{isErrorFilesLimit && (
<p className={styles.dropzoneError} data-testid="filesLimitExceeded">
<span className={styles.dropzoneErrorHeadline}>
You reached the limit of {maxFiles} photos. Remove a photo to
upload a new one.
</span>
</p>
)}
{errorFileName && (
<p className={styles.dropzoneError}>
<span className={styles.dropzoneErrorHeadline}>
Image file name already exists:
</span>
<br />
{errorFileName}
</p>
)}
{errorFileType && (
<p className={styles.dropzoneError}>
<span className={styles.dropzoneErrorHeadline}>
Unsupported format:
</span>
<br />
{errorFileType}
</p>
)}
{errorFileSize && (
<p className={styles.dropzoneError}>
<span className={styles.dropzoneErrorHeadline}>
Maximum file size (10MB) exceeded:
</span>
<br />
{errorFileSize}
</p>
)}
</div>
</div>
);
};
export default ImageUpload;
I left out all imports, but other than this I kept everything;
We used the spread operator, concat() and pushed to the local state already, but somehow the merging does not work.
2
Answers
The problem is that the FileReader is asynchronous. That means we had to rebuild some things:
We don't push the constructedFile, but now a simple object with needed properties to the validUploadedImages, because we want to read the file as dataURL only, when it has arrived at the parent component. Then we do the transformation from the type File to dataUrl, which is needed to display the images properly. This, we had to await and make the handleImagesUpload function async.
// Inside EntryEditPage:
...
// In the child component: // changed...
// to...
setCurrentImages is a method to set the value of the state. You need such as:
But, the method handleImagesUpload may not take currentImages from the outside of the function. Thus, you can give the currentImages as a parameter such as currentState.
And return part can be:
Or:
In the second one, you should also update the child component.