I am able to upload images using Uppy on my react app:
Upon selection and upload of an image, I see a preview of the image, along with options to remove it, add more, etc.:
After clicking the "Continue" button,
the state of reporting
(from const [reporting, setReporting] = useState(false);
) is set to true, which I think triggers a re-render of the DOM, including the disappearance of the UploadManager
component, which contains the Uppy dashboard pictured above. Now, instead, a <ReportForm>
component is rendered:
...
{reporting ? (
<ReportForm assetReferences={files} exifData={exifData} />
) : (
<>
<Grid item style={{ marginTop: 20 }}>
<UploadManager
onUploadStarted={() => setUploadInProgress(true)}
onUploadComplete={() => setUploadInProgress(false)}
...
When users click, "Back to Photographs" from the <ReportForm>
component (see image above), it simply resets the state of reporting
back to false. However, the Uppy Dashboard now shows its default, "Drop files here or browse files" again (see first image). I would like to see a preview of the images I just uploaded instead.
I am very new to React, but I can see that the uppyInstance is created in a useEffect hook, which seems to get called upon change of the reporting
state. I suspect that this is what is "resetting" the uppy Dashboard (see code for the uppy-related component below):
UploadManager.jsx:
import React, { useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { get } from 'lodash-es';
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import Dashboard from '@uppy/react/lib/Dashboard';
import Skeleton from '@material-ui/lab/Skeleton';
import Cropper from './Cropper';
import '@uppy/core/dist/style.css';
import '@uppy/dashboard/dist/style.css';
const dashboardWidth = 600;
const dashboardHeight = 400;
export default function UploadManager({
files,
assetSubmissionId,
onUploadStarted = Function.prototype,
onUploadComplete = Function.prototype,
setFiles,
exifData,
disabled = false,
alreadyFiles = false,
}) {
const intl = useIntl();
const currentExifData = useRef();
currentExifData.current = exifData;
const [uppy, setUppy] = useState(null);
const [cropper, setCropper] = useState({
open: false,
imgSrc: null,
});
/* Resolves closure / useEffect issue */
// https://www.youtube.com/watch?v=eTDnfS2_WE4&feature=youtu.be
const fileRef = useRef([]);
fileRef.current = files;
if (alreadyFiles) {
// MAYBE I CAN DO SOMETHING HERE???
}
useEffect(() => {
console.log('deleteMe useEffect entered');
const uppyInstance = Uppy({
meta: { type: 'Report sightings image upload' },
restrictions: {
allowedFileTypes: ['.jpg', '.jpeg', '.png'],
},
autoProceed: true,
// browserBackButtonClose: true,
});
uppyInstance.use(Tus, {
endpoint: `${__houston_url__}/api/v1/asset_groups/tus`,
headers: {
'x-tus-transaction-id': assetSubmissionId,
},
});
uppyInstance.on('upload', onUploadStarted);
uppyInstance.on('complete', uppyState => {
const uploadObjects = get(uppyState, 'successful', []);
const assetReferences = uploadObjects.map(o => ({
path: o.name,
transactionId: assetSubmissionId,
}));
onUploadComplete();
// console.log('deleteMe fileRef.current is: ');
// console.log(fileRef.current);
// console.log('deleteMe ...fileRef.current is: ');
// console.log(...fileRef.current);
// // eslint-disable-next-line no-debugger
// debugger;
setFiles([...fileRef.current, ...assetReferences]);
});
uppyInstance.on('file-removed', (file, reason) => {
if (reason === 'removed-by-user') {
const newFiles = fileRef.current.filter(
f => f.path !== file.name,
);
setFiles(newFiles);
}
});
setUppy(uppyInstance);
return () => {
if (uppyInstance) uppyInstance.close();
};
}, []);
return (
<div
style={{
opacity: disabled ? 0.5 : 1,
pointerEvents: disabled ? 'none' : undefined,
}}
>
{cropper.open && (
<Cropper
imgSrc={cropper.imgSrc}
onClose={() => setCropper({ open: false, imgSrc: null })}
setCrop={croppedImage => {
const currentFile = files.find(
f => f.filePath === cropper.imgSrc,
);
const otherFiles = files.filter(
f => f.filePath !== cropper.imgSrc,
);
setFiles([
...otherFiles,
{ ...currentFile, croppedImage },
]);
}}
/>
)}
{uppy ? (
<div style={{ marginBottom: 32, maxWidth: dashboardWidth }}>
<Dashboard
uppy={uppy}
note={intl.formatMessage({ id: 'UPPY_IMAGE_NOTE' })}
showLinkToFileUploadResult={false}
showProgressDetails
showRemoveButtonAfterComplete
doneButtonHandler={null}
height={dashboardHeight}
locale={{
strings: {
dropHereOr: intl.formatMessage({
id: 'UPPY_DROP_IMAGES',
}),
browse: intl.formatMessage({ id: 'UPPY_BROWSE' }),
uploading: intl.formatMessage({
id: 'UPPY_UPLOADING',
}),
complete: intl.formatMessage({ id: 'UPPY_COMPLETE' }),
uploadFailed: intl.formatMessage({
id: 'UPPY_UPLOAD_FAILED',
}),
paused: intl.formatMessage({ id: 'UPPY_PAUSED' }),
retry: intl.formatMessage({ id: 'UPPY_RETRY' }),
cancel: intl.formatMessage({ id: 'UPPY_CANCEL' }),
filesUploadedOfTotal: {
0: intl.formatMessage({
id: 'UPPY_ONE_FILE_PROGRESS',
}),
1: intl.formatMessage({
id: 'UPPY_MULTIPLE_FILES_PROGRESS',
}),
},
dataUploadedOfTotal: intl.formatMessage({
id: 'UPPY_DATA_UPLOADED',
}),
xTimeLeft: intl.formatMessage({
id: 'UPPY_TIME_LEFT',
}),
uploadXFiles: {
0: intl.formatMessage({
id: 'UPPY_UPLOAD_ONE_FILE',
}),
1: intl.formatMessage({
id: 'UPPY_UPLOAD_MULTIPLE_FILES',
}),
},
uploadXNewFiles: {
0: intl.formatMessage({
id: 'UPPY_PLUS_UPLOAD_ONE_FILE',
}),
1: intl.formatMessage({
id: 'UPPY_PLUS_UPLOAD_MULTIPLE_FILES',
}),
},
},
}}
/>
</div>
) : (
<Skeleton
variant="rect"
style={{
width: '100%',
maxWidth: dashboardWidth,
height: dashboardHeight,
}}
/>
)}
</div>
);
}
Here is the code for the parent component in which UploadManager lives:
import React, { useState, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { v4 as uuid } from 'uuid';
import Grid from '@material-ui/core/Grid';
import InfoIcon from '@material-ui/icons/InfoOutlined';
import UploadManager from '../../components/report/UploadManager';
import ReportSightingsPage from '../../components/report/ReportSightingsPage';
import Text from '../../components/Text';
import Link from '../../components/Link';
import Button from '../../components/Button';
import ReportForm from './ReportForm';
// import useAssetFiles from '../../hooks/useAssetFiles';
export default function ReportSighting({ authenticated }) {
// console.log('deleteMe ReportSighting called');
const assetSubmissionId = useMemo(uuid, []);
const [uploadInProgress, setUploadInProgress] = useState(false);
const [alreadyFiles, setAlreadyFiles] = useState(false);
const [files, setFiles] = useState([]);
const [exifData, setExifData] = useState([]);
const [reporting, setReporting] = useState(false);
const noImages = files.length === 0;
// const {
// setFilesFromComponent,
// getFilesFromComponent,
// } = useAssetFiles();
const onBack = () => {
window.scrollTo(0, 0);
// setFilesFromComponent(files);
// setFiles(files);
if (files) setAlreadyFiles(true);
setReporting(false);
};
let continueButtonText = 'CONTINUE';
if (noImages) continueButtonText = 'CONTINUE_WITHOUT_PHOTOGRAPHS';
if (uploadInProgress) continueButtonText = 'UPLOAD_IN_PROGRESS';
return (
<ReportSightingsPage
titleId="REPORT_A_SIGHTING"
authenticated={authenticated}
>
{reporting ? (
<Button
onClick={onBack}
style={{ marginTop: 8, width: 'fit-content' }}
display="back"
id="BACK_TO_PHOTOS"
/>
) : null}
{reporting ? (
<ReportForm assetReferences={files} exifData={exifData} />
) : (
<>
<Grid item style={{ marginTop: 20 }}>
<UploadManager
onUploadStarted={() => setUploadInProgress(true)}
onUploadComplete={() => setUploadInProgress(false)}
assetSubmissionId={assetSubmissionId}
exifData={exifData}
setExifData={setExifData}
files={
files
// getFilesFromComponent()
// ? getFilesFromComponent()
// : files
}
setFiles={setFiles}
alreadyFiles={alreadyFiles}
/>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 20,
}}
>
<InfoIcon fontSize="small" style={{ marginRight: 4 }} />
<Text variant="caption">
<FormattedMessage id="PHOTO_OPTIMIZE_1" />
<Link
external
href="https://docs.wildme.org/docs/researchers/photography_guidelines"
>
<FormattedMessage id="PHOTO_OPTIMIZE_2" />
</Link>
<FormattedMessage id="PHOTO_OPTIMIZE_3" />
</Text>
</div>
</Grid>
<Grid item>
<Button
id={continueButtonText}
display="primary"
disabled={uploadInProgress}
onClick={async () => {
window.scrollTo(0, 0);
setReporting(true);
}}
style={{ marginTop: 16 }}
/>
</Grid>
</>
)}
</ReportSightingsPage>
);
}
I suspect that the problem is that useEffect is triggering a re-render of UploadManager, but
disabled=reporting
on the Upload Manager logic? Should I limit what fires off the useEffect hook? If so, how, specifically, might I do that? Can I blacklist things (e.g., reporting
) from firing a useEffect?Here is the branch of the repository I'm working from for more reference if that's needed. Creating an example on stackblitz proved non-trivial.
Many thanks in advance for any suggestions!
I am unable to run the repro you provided because it appears to require private API keys.
However, inspecting the code, here's what appears to be happening:
<ReportSighting>
component renders for the first time with reporting
state as false
. This causes the <UploadManager>
component to render, and a new instance of Uppy
to be created by the effect within that component and stored in this instance of the <UploadManager>
's state.reporting
state to true
. This causes the <ReportSighting>
component to re-render, and show the <ReportForm>
component instead. The original <UploadManager>
component is unmounted, and the things that were present in its state, notably, the instance of Uppy
that had the user's uploaded file, are lost.reporting
state back to false
. A this causes a new instance of <UploadManager>
to render, and the effect that creates an instance of Uppy
re-runs. The instance that is created is new as well, and thus won't show what it contained the last time it was rendered.One way to solve this would be to lift up state that needs to remain the same as the <UploadManager>
is mounted and unmounted into the parent <ReportSighting>
component. So the <ReportSighting>
component would be responsible for creating the Uppy instance, and pass that as a prop to the <UploadManager>
.
See also this part of the Uppy docs:
Functional components are re-run on every render. This could lead to accidentally recreate a fresh Uppy instance every time, causing state to be reset and resources to be wasted.
The @uppy/react package provides a hook useUppy() that can manage an Uppy instance’s lifetime for you. It will be created when your component is first rendered, and destroyed when your component unmounts.
You could also re-factor your app in other ways if it feels like too much is happening in a single component. But the guiding principle would be: "don't create the uppy instance more than once in a single user flow".