0

我可以在我的 react 应用 上使用Uppy上传图片:Uppy仪表板提示用户上传

选择并上传图像后,我会看到图像的预览,以及删除、添加更多等选项: 选择并上传图像后

点击“继续”按钮后,reporting(from const [reporting, setReporting] = useState(false);) 的状态设置为 true,我认为这会触发 DOM 的重新渲染,包括UploadManager组件的消失,其中包含上图所示的 Uppy 仪表板。现在,相反,<ReportForm>渲染了一个组件:

...
{reporting ? (
        <ReportForm assetReferences={files} exifData={exifData} />
      ) : (
        <>
          <Grid item style={{ marginTop: 20 }}>
            <UploadManager
              onUploadStarted={() => setUploadInProgress(true)}
              onUploadComplete={() => setUploadInProgress(false)}
...

在此处输入图像描述

当用户单击组件中的“返回照片” <ReportForm>(见上图)时,它只是将返回状态重置reporting为 false。但是,Uppy 仪表板现在再次显示其默认设置“将文件拖放到此处或浏览文件”(参见第一张图片)。我想查看我刚刚上传的图像的预览。

我对 React 很陌生,但我可以看到 uppyInstance 是在 useEffect 挂钩中创建的,它似乎在reporting状态更改时被调用。我怀疑这就是“重置” uppy 仪表板的原因(请参阅下面与 uppy 相关的组件的代码):

上传管理器.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>
  );
}

以下是 UploadManager 所在的父组件的代码:

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>
  );
}

我怀疑问题是 useEffect 正在触发 UploadManager 的重新渲染,但是

  1. 我不确定这是否属实
  2. 我正在寻找一种防止上述重新渲染的好策略。我应该以某种方式使用disabled=reporting上传管理器逻辑吗?我应该限制什么触发 useEffect 钩子吗?如果是这样,具体来说,我该怎么做?我可以将reporting触发 useEffect 的事物(例如 )列入黑名单吗?

这是我正在使用的存储库的分支,以便在需要时提供更多参考。事实证明,在 stackblitz 上创建示例并非易事。

非常感谢您的任何建议!

4

2 回答 2

1

我无法运行您提供的重现,因为它似乎需要私有 API 密钥。

但是,检查代码,这似乎是正在发生的事情:

  1. 组件第<ReportSighting>一次呈现reporting状态为false. 这会导致<UploadManager>组件渲染,Uppy并由该组件中的效果创建一个新实例,并将其存储在的状态实例<UploadManager>
  2. 用户上传文件,然后点击“继续”按钮,将reporting状态设置为true. 这会导致<ReportSighting>组件重新渲染,并<ReportForm>改为显示组件。原始<UploadManager>组件被卸载,并且在其状态中存在的东西,特别是Uppy具有用户上传文件的实例,都丢失了。
  3. 用户点击“返回照片”按钮,将reporting状态设置回false. 这会导致渲染一个的实例<UploadManager>,并且创建一个实例的效果Uppy重新运行。创建的实例也是新的,因此不会显示上次渲染时包含的内容。

解决此问题的一种方法是提升<UploadManager>需要与已安装和卸载到父<ReportSighting>组件中保持相同的状态。因此<ReportSighting>组件将负责创建 Uppy 实例,并将其作为道具传递给<UploadManager>.

另请参阅Uppy 文档的这一部分

功能组件在每次渲染时重新运行。这可能导致每次意外地重新创建一个新的 Uppy 实例,从而导致状态被重置和资源被浪费。

@uppy/react 包提供了一个钩子 useUppy() 可以为你管理一个 Uppy 实例的生命周期。它将在您的组件首次渲染时创建,并在您的组件卸载时销毁。

如果感觉单个组件中发生了太多事情,您还可以通过其他方式重构您的应用程序。但指导原则是:“不要在单个用户流中多次创建 uppy 实例”。

于 2021-11-09T19:46:17.323 回答
0

这实际上是来自https://uppy.io/docs/react/initializing/的答案

重要的是,useUppy() 钩子接受一个返回 Uppy 实例的函数。这样,useUppy() 钩子可以决定何时创建它。否则,您仍然会在每次渲染时创建一个未使用的 Uppy 实例。

这里的例子:

const uppy = useUppy(() => {
    return new Uppy({
      autoProceed: false,
      restrictions: {
        maxFileSize: 15 * 1024 * 1024,
        maxNumberOfFiles: 1,
        allowedFileTypes: ['image/*'],
      }
    }).use(Webcam)
      .use(ImageEditor, {
        id: "ImageEditor",
        quality: 0.8
      })
  })

  uppy.on('file-editor:complete', (result) => {
    props.images(result)
  })
于 2021-12-26T22:03:20.607 回答