2

I'm currently working on an image uploader component in React. Everything works fine but the deleting method. I've read a couple of articles on how to update arrays/objects and the idea of immutable state. Here's what I've tried:

  1. .filter()
  2. .slice()
  3. .splice() (I doubt this would work as it modifies the original array)

And I always got this error no matter what I tried:

Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.

And this is my code:

ImageUploader.js

import React, { Component } from 'react';

import styled from 'styled-components';

import FileUploadButton from '../FileUploadButton';
import ImagePreviewer from './ImagePreviewer';

import { 
  Typography, 
  Button 
} from '@material-ui/core';
import theme from '../../../theme';

import uuidv5 from 'uuid/v5';

const StyledPreviewerContainer = styled.div`
  display: flex;
  margin: ${theme.spacing.unit}px 0;
  overflow: hidden;
  overflow-x: auto;
`;

export default class ImageUploader extends Component {
  state = {
    uploadedImages: []
  }

  updateImages = e => {
    const { uploadedImages } = this.state,
    files = [...e.target.files],
    inexistentImages = files.filter(image => uploadedImages.indexOf(image) === -1);

    this.setState(prevState => ({
      uploadedImages: [...prevState.uploadedImages, ...inexistentImages]
    }));

    this.props.onChange(e);
  }

  removeImages = image => {
    const { uploadedImages } = this.state,
    imageIndex = uploadedImages.indexOf(image);

    this.setState(prevState => ({
      uploadedImages: prevState.uploadedImages.filter((image, index) => index !== imageIndex)
    }));
  };

  render() {
    const {
      className,
      label,
      id, 
      multiple, 
      name, 
      onBlur
    } = this.props, {
      uploadedImages
    } = this.state;

    return (
      <div className={className}>
        <Typography>
          {label}
        </Typography>
        <StyledPreviewerContainer>
          {uploadedImages.map(image =>
            <ImagePreviewer 
              src={URL.createObjectURL(image)}
              image={image} 
              removeImages={this.removeImages}
              key={uuidv5(image.name, uuidv5.URL)}
            />
          )}
        </StyledPreviewerContainer>
        <FileUploadButton 
          id={id}
          multiple={multiple}
          name={name}
          onChange={this.updateImages}
          onBlur={onBlur}
        />
        <Button>
          Delete all
        </Button>
      </div>
    );
  }
}

ImagePreviewer.js

import React, { Component } from 'react';

import styled from 'styled-components';

import AnimatedImageActions from './AnimatedImageActions';

import { ClickAwayListener } from '@material-ui/core';
import theme from '../../../theme';

const StyledImagePreviewer = styled.div`
  height: 128px;
  position: relative;
  user-select: none;
  cursor: pointer;

  &:not(:last-child) {
    margin-right: ${theme.spacing.unit * 2}px;
  }
`;

const StyledImage = styled.img`
  height: 100%;
`;

export default class ImagePreviewer extends Component { 
  state = {
    actionsOpened: false
  };

  openActions = () => {
    this.setState({
      actionsOpened: true
    });
  };

  closeActions = () => {
    this.setState({
      actionsOpened: false
    });
  };

  render() {
    const {
      actionsOpened
    } = this.state,
    {
      src,
      image,
      removeImages
    } = this.props;

    return (
      <ClickAwayListener onClickAway={this.closeActions}>
        <StyledImagePreviewer onClick={this.openActions}>
          <StyledImage src={src} />
          <AnimatedImageActions 
            actionsOpened={actionsOpened} 
            image={image}
            removeImages={removeImages}
          />
        </StyledImagePreviewer>
      </ClickAwayListener>
    );
  }
}

AnimatedImageActions.js

import React from 'react';

import styled from 'styled-components';

import { Button } from '@material-ui/core';
import { Delete as DeleteIcon } from '@material-ui/icons';
import { fade } from '@material-ui/core/styles/colorManipulator';
import theme from '../../../theme';

import { 
  Motion, 
  spring
} from 'react-motion';

const StyledImageActions = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  color: ${theme.palette.common.white};
  background-color: ${fade(theme.palette.common.black, 0.4)};
  width: 100%;
  height: 100%;
  display: flex;
`;

const StyledImageActionsInner = styled.div`
  margin: auto;
`;

const StyledDeleteIcon = styled(DeleteIcon)`
  margin-right: ${theme.spacing.unit}px;
`;

const AnimatedImageActions = ({ actionsOpened, removeImages, image }) => 
  <Motion
    defaultStyle={{
      scale: 0
    }}
    style={{
      scale: spring(actionsOpened ? 1 : 0, {
        stiffness: 250
      })
    }}
  >
    {({ scale }) =>
      <StyledImageActions style={{
        transform: `scale(${scale})`
      }}>
        <StyledImageActionsInner>
          <Button 
            color="inherit"
            onClick={removeImages(image)}  
          >
            <StyledDeleteIcon />
            Delete
          </Button>
        </StyledImageActionsInner>
      </StyledImageActions>
    }
  </Motion>
;

export default AnimatedImageActions

Any help would be greatly appreciated!

Sam
  • 5,375
  • 2
  • 45
  • 54
Brian Le
  • 2,646
  • 18
  • 28

1 Answers1

3

Could it be that onClick={removeImages(image)} should be onClick={()=>removeImages(image)}?

Otherwise, removeImages is calling setState in AnimatedImageActions's render pass.

Joshua R.
  • 2,282
  • 1
  • 18
  • 21
  • 1
    Works like a charm. Thanh you! I would prefer making that removeImages method returns another function, in other words curry – Brian Le Nov 30 '18 at 00:22
  • Also a fine choice. (And do accept this answer if it solved your problem.) Cheers! – Joshua R. Nov 30 '18 at 00:31
  • I’m on it! Thank once again – Brian Le Nov 30 '18 at 00:32
  • 1
    To prevent event handler functions gets called during render you need to call every event handler function as () => function() which sets the state. So this way the function gets called only when event is triggered. – Hemadri Dasari Nov 30 '18 at 01:23
  • @HemadriDasari that was what I was talking about. Thanks for the clarification tho – Brian Le Nov 30 '18 at 17:22