user3378165
user3378165

Reputation: 6916

Prevent state reset

I have an application that allows the user to upload photos, to edit them (crop, zoom and rotate) and to download.

I'm trying to add a preview mode, so the user can see how the file he's downloading will look like.

What I did works fine except for one thing - when the user clicks on the Clear Preview button the photos go back to their original state (the state is being reset on Clear Preview button click), without keeping the changes the user made to them.

Any idea what am I missing and how to prevent the state reset?

export default function ImagesGrid() {
  const classes = useStyles({})
  const {
    photos,
    preparePhotos,
    checkUnusedSuppliedTags,
    usedTagsList,
    suppliedTags
  } = useContext(SitePhotosContext)

  const taggedPhotos = photos.filter(photo => photo.tags.length)
  const pages = Math.ceil(taggedPhotos.length / 6)
  const [isPreviewMode, setIsPreviewMode] = useState(false)
  const [preparedPhotos, setPreparedPhotos] = useState([])
  const renderedPhotos = isPreviewMode ? preparedPhotos : photos
  let countColor: TypographyProps['color'] = 'initial'

  const tags = usedTagsList.concat(
    checkUnusedSuppliedTags(usedTagsList, suppliedTags)
  )

  useEffect(() => {
    preparePhotos(photos).then(photos => setPreparedPhotos(photos))
  }, [photos])

  return (
    <div className={classes.root}>
      <AppBar position="sticky" color="default">
        <Toolbar>
          {!isPreviewMode ? (
            <div className={classes.toolbarText}>
              <Typography
                variant="h6"
                color={countColor}
                className={classes.pageCount}
              >
                {`${taggedPhotos.length} Tagged Photos / ${pages} Pages`}
              </Typography>
              {taggedPhotos.length === 0 && (
                <Typography
                  variant="body2"
                  color={countColor}
                  className={classes.instructions}
                >
                  Tag some photos to generate a report
                </Typography>
              )}
            </div>
          ) : (
            <DownloadPreflightModal
              photos={preparedPhotos}
              usedTagsList={usedTagsList}
            />
          )}
          <DownloadAndPreview
            isPreviewMode={isPreviewMode}
            setIsPreviewMode={setIsPreviewMode}
            tags={tags}
          />
        </Toolbar>
      </AppBar>
      <Grid
        container
        justify="flex-start"
        spacing={2}
        className={classes.imageGrid}
      >

        {renderedPhotos.map(photo => (
          <Grid
            item
            xs={12}
            sm={6}
            md={isPreviewMode ? 6 : 4}
            key={photo.id}
            className={isPreviewMode ? classes.imageInternalGrid : ''}
          >
            <Image photo={photo} mode={isPreviewMode ? 'preview' : 'editor'} />
          </Grid>
        ))}
      </Grid>
    </div>
  )
}

DownloadAndPreview component:

export default function DownloadAndPreview({
  isPreviewMode,
  setIsPreviewMode,
  tags
}) {
  const classes = useStyles({})
  const generatorRef = useRef()
  const {photos} = useContext(SitePhotosContext)

  return (
    <>
      {!isPreviewMode ? (
        <Button color="inherit" onClick={() => setIsPreviewMode(true)}>
          Preview
          <VisibilityIcon />
        </Button>
      ) : (
        <div>
          <Button
            color="inherit"
            onClick={() => setIsPreviewMode(false)}
            className={classes.previewModeBtn}
          >
            Clear Preview
            <ClearIcon />
          </Button>
          <Button
            color="inherit"
            onClick={() => {
              downloadDoc(generatorRef, photos, tags)
            }}
          >
            Download
          </Button>
        </div>
      )}
    </>
  )
}

<Image /> component:

export default function Image({photo, mode}: {photo: ImageItem; mode: string}) {
  const classes = useStyles({})
  const {
    setImageEditData
  } = useContext(SitePhotosContext)

  return mode === 'editor' ? (
    <Paper>
      {!photo.src && (
        <div className={classes.loading}>Loading {photo.data?.name}...</div>
      )}

      <div>
        <ImageEditor
          photo={photo}
          onEditComplete={editData => {
            setImageEditData(photo.id, editData)
          }}
        />
      </div>
    </Paper>
  ) : (
    <div>
      <img src={photo.src} className={classes.img} />
      <label>{photo.tags.join(' / ')}</label>
    </div>
  )
}

<ImageEditor /> component:

export default function ImageEditor({
  photo,
  onEditComplete,
  showControls
}: editorProps) {
  const classes = useStyles({})
  const [crop, setCrop] = useState({x: 0, y: 0})

  const [rotation, setRotation] = useState(0)
  const [zoom, setZoom] = useState(1)

  const onCropComplete = useCallback(
    (croppedArea, croppedAreaPixels) => {
      const editSettings: editData = {crop: croppedAreaPixels, rotate: rotation}
      onEditComplete(editSettings)
    },
    [rotation, onEditComplete]
  )

  return (
    <>
      <Box className={classes.cropContainer}>
        <Cropper
          image={photo.src}
          aspect={maxImageWidth / maxImageHeight}
          crop={crop}
          rotation={rotation}
          zoom={zoom}
          zoomWithScroll={false}
          onCropChange={setCrop}
          onRotationChange={setRotation}
          onCropComplete={onCropComplete}
          onZoomChange={setZoom}
        />
      </Box>
      {showControls && (
        <Box display="flex" className={classes.controls}>
          <IconButton
            color="primary"
            onClick={() => setRotation(rotation => rotation - 90)}
          >
            <RotateLeftIcon />
          </IconButton>
          <Box display="inline-flex" width="100%">
            <ImageIcon
              fontSize="small"
              color="action"
              className={classes.imageIcon}
            />
            <Slider
              className={classes.slider}
              value={zoom}
              min={1}
              max={3}
              step={0.1}
              onChange={(e, zoom) => setZoom(zoom)}
            />
            <ImageIcon
              fontSize="large"
              color="action"
              className={classes.imageIcon}
            />
          </Box>
          <IconButton
            color="primary"
            onClick={() => setRotation(rotation => rotation + 90)}
          >
            <RotateRightIcon />
          </IconButton>
        </Box>
      )}
    </>
  )
}

context functions:

  const preparePhotos = async (photos: PhotoList) => {
    const res = photos
      .filter(photo => photo.tags.length)
      .sort(sortByPriority)

    if (res.length % 2) res.pop()

    const edittedPhotos = await Promise.all(res.map(editImage))
    return edittedPhotos
  }

  const setImageEditData = (id, editData) => {
    setPhotos(photos =>
      photos.map(photo => {
        if (photo.id === id) {
          return {...photo, editData}
        }
        return photo
      })
    )
  }

  const editImage = async (image: ImageItem) => {
    if (!image.editData) return image
    const {crop, rotate} = image.editData
    const edittedImage = await cropAndRotateImage(image.src, crop, rotate)
    return {
      ...image,
      src: edittedImage,
      dimensions: {width: crop.width, height: crop.height}
    }
  }

Expected behavior is similar to this: https://codesandbox.io/s/react-easy-crop-custom-image-demo-y09komm059?from-embed=&file=/src/index.js:3141-3151

After the user closes the preview modal the photo stay in the same position it was - before clicking on the preview button. (What by me - it doesn't)

Upvotes: 3

Views: 666

Answers (2)

x00
x00

Reputation: 13853

Your question still lacks some vital information. But let me guess...

  1. Let's assume that you're using https://github.com/ricardo-ch/react-easy-crop (It's just the first library I came across, but even if you're using another library, I suppose, they mostly work the same way).
  2. If so, then on every rerender <Cropper> calls onCropComplete. And it rerenders every time you switch into editor-mode.
  3. On image load react-easy-crop sets crop, rotation and zoom to respective values passed to <Cropper> which you hold in <ImageEditor> component's state.
  4. But here
    function Image(...) {
      ...
      return mode === 'editor' ? ( ... <ImageEditor ... /> ... ) : ( ... )
    }
    
    you loose <ImageEditor> component's state on every mode switch.
  5. As a result: when you switch from preview-mode into editor-mode <Cropper> receives default values for crop, rotation and zoom, calls onCropComplete and your implementation of onCropComplete sets editData to these default values.

There are several ways to fix this. But the main issue, as I see it, is that you don't have a single source of truth for photo's editData. So I think you should get rid of <ImageEditor> component's state and instead pass editData to the <Cropper>. Like so:

<Cropper
  image    = {photo.src}
  crop     = {photo.editData.crop}
  rotation = {photo.editData.rotation}
  zoom     = {photo.editData.zoom}
  ...
/>

You don't have a minimal reproducible example, so I can't test it. For this to works you should probably at least add some default values for photo.editData

Upvotes: 3

Giovanni Esposito
Giovanni Esposito

Reputation: 11166

Ciao, I think you could blob your image modified, then save it to localStorage and, when user exit from preview mode, show image blobbed instead of original photo.

I try to explain better: lets say you have this image in preview mode and user zooms it. So now original image is modified. Then, user clicks Clear Preview button. Just before call setIsPreviewMode(false) you could store modified image using, for example, dom-to-image. Something like:

domtoimage.toBlob(document.getElementById("ImageModifiedId"))
        .then(function (blob) {
            // here you have the image modified blobbed into blob item
            // then localStorage.setItem("ImageKey", blob);
            // and finally setIsPreviewMode(false)
        });

Ok now the last step, instead of passing to <Image> always the same photo, you could verify if there is a modifed photo in your localStorage and, if there is, load localStorage.getItem("ImageKey") into <Image>.

Upvotes: 0

Related Questions