Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent state reset

Tags:

reactjs

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)

like image 304
user3378165 Avatar asked Feb 18 '26 02:02

user3378165


1 Answers

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

like image 73
x00 Avatar answered Feb 20 '26 01:02

x00



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!