1

I am learning React by trying to create a simple Tweetbox "uploading" multiple images using the browser HTML5 File API.

See screenshot below:

enter image description here

As it recommends in the official documentation I am using the inbuilt map function to loop over the uploaded image urls in an array.

I am enclosing this in a variable so that it can be reused in the JSX below within the wrapper div which I only want to show if an image has been uploaded.

The problem is that due to some sort of scope problem it can't "see" or "access" my _removePhoto function and click handler in this code snippet:

_removePhoto(event) { 
    let array = this.state.imagePreviewUrls;
    let index = array.indexOf(event.target.result)
      array.splice(index, 1);
      this.setState({imagePreviewUrls: array });
      // this.setState( {imagePreviewUrls: false} );
  }

  render() {  

    let {imagePreviewUrls} = this.state;
    let {thereIsImage} = this.state;
    // var {thereIsImage} = this.state;
    // onClick={this._removePhoto}

    var imageList = imagePreviewUrls.map(function(value, index) {
        return (
          <figure className="ma0 relative flex items-center justify-center">
              <button onClick={this._removePhoto} className="button-reset pointer dim bn bg-black h2 w2 br-100 white flex items-center justify-center absolute absolute--fill-l center"><i className="material-icons f5">close</i>
              </button>
              <img key={index} src={value} className="h3 w3" />
          </figure>
          )
    })

This gives the error:

Uncaught TypeError: Cannot read property '_removePhoto' of undefined
    at bundle.js:19085
    at Array.map (<anonymous>)

(The _removePhoto logic works if I don't enclose the map function in the imageList variable in this way).

Also if I remove the onClick event handler, it shows the image preview, but there is another error in the console saying that I need to include "index". However, I have already included this as per the documentation's instructions.

Here is my full code below:

import React from 'react'
import ReactDOM from 'react-dom'
// This library didn't help me solve the problem
// const classNames = require('classnames');

class TweetBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
          text: "", 
          files: [],
          imagePreviewUrls: [],
          thereIsImage: false
    };

    this._handleTextChange = this._handleTextChange.bind(this);
    this._triggerFileDialogue = this._triggerFileDialogue.bind(this);
    this._handlePhotoUpload = this._handlePhotoUpload.bind(this);
    this._removePhoto = this._removePhoto.bind(this);
  } 


  _removePhoto(event) { 
    let array = this.state.imagePreviewUrls;
    let index = array.indexOf(event.target.result)
      array.splice(index, 1);
      this.setState({imagePreviewUrls: array });
      // this.setState( {imagePreviewUrls: false} );
  }

  render() {  

    let {imagePreviewUrls} = this.state;
    let {thereIsImage} = this.state;
    // var {thereIsImage} = this.state;
    // onClick={this._removePhoto}

    var imageList = imagePreviewUrls.map(function(value, index) {
        return (
          <figure className="ma0 relative flex items-center justify-center">
              <button onClick={this._removePhoto} className="button-reset pointer dim bn bg-black h2 w2 br-100 white flex items-center justify-center absolute absolute--fill-l center"><i className="material-icons f5">close</i>
              </button>
              <img key={index} src={value} className="h3 w3" />
          </figure>
          )
    })

    return (
      <div>

        {/* TITLE SECTION */}
          <div className="pv2 tc bb b--black-10">
            <h1 className="ma0 f5 normal">Create New Report</h1>
          </div>

        <div className="bg-near-white pa3">

        <textarea onChange={this._handleTextChange} placeholder="Write your report here" name="tweet" rows="4" className="w-100 br2 ba b--black-10 pa2"></textarea>

        { imagePreviewUrls.length > 0 &&
        <div className="bg-black-10 pa2 flex"> 
          {imageList}          
        </div>
      } 

        <input className="hide" ref={ (input) => { this.fileInput = input; } } onChange={ this._handlePhotoUpload } type="file"></input>

        <div className="mt3 flex justify-between">
            <button onClick={ this._triggerFileDialogue } className="button-reset flex items-center br2 bn bg-transparent blue hover-bg-black-10 pointer">
              <i className="material-icons f3">photo_camera</i>
            </button>

          <div className="flex items-center">
            <button disabled={ this.state.text.length === 0 } className="button-reset bg-blue bn white f6 fw5 pv2 ph3 br2 dim">Message</button>
          </div>
        </div>

        {/* End "near-white" subcontainer */}
        </div> 
      {/* End "b--black-10" parent container */}
      </div> 
    ); 

} // End Render    

  _handleTextChange(event) {
      this.setState( { text: event.target.value } );
      // For debugging:
      // console.log( { text: event.target.value })
    }

  _triggerFileDialogue(event) {
    this.fileInput.click();
  }

  _handlePhotoUpload(event) {
    // var self = this;
    event.preventDefault();

    let files = event.target.files;

    for (let i = 0; i < files.length; i++) {
      let reader = new FileReader();      

      reader.onloadend = (evt) => {

        var imageString = evt.target.result;
        let updatedImages = this.state.imagePreviewUrls.concat(imageString);

        this.setState({
          imagePreviewUrls: updatedImages,
          files: files[i],
          thereIsImage: true
        });

      }
      reader.readAsDataURL(files[i]);
      console.log(files[i]);
    }

  }

} // End Master React Class


ReactDOM.render( <TweetBox />, document.getElementById("app") );

Many thanks in advance, I've googled lots of things on Stack Overflow but can't find a solution to this! The code above should be reproducible.

neo5_50
  • 466
  • 1
  • 5
  • 19
  • Possible duplicate of [React: How to render same component with same onClick and different drops](https://stackoverflow.com/questions/45838845/react-how-to-render-same-component-with-same-onclick-and-different-drops) – Murat Karagöz Aug 23 '17 at 14:59
  • 1
    Your `map` function misses the binding. – Murat Karagöz Aug 23 '17 at 14:59
  • 1
    The inline function notation must also be used if you want to access the `this` of the outside scope. In your code, you are still using the `function` keyword instead of the fat arrow ( `=>` ) – JD Hernandez Aug 23 '17 at 15:02

1 Answers1

3

Instead of using anonymous function on the map you should use arrow function (which are auto bind to the current context):

var imageList = imagePreviewUrls.map((value, index) => {
        return (
          <figure className="ma0 relative flex items-center justify-center">
              <button onClick={this._removePhoto} className="button-reset pointer dim bn bg-black h2 w2 br-100 white flex items-center justify-center absolute absolute--fill-l center"><i className="material-icons f5">close</i>
              </button>
              <img key={index} src={value} className="h3 w3" />
          </figure>
          )
    })

When using arrow function, the this is automatically referenced to the current this you have in the relevant context.

Dekel
  • 60,707
  • 10
  • 101
  • 129
  • thanks very much Dekel. This solved problem (have upvoted but need to wait 8 mins to accept). Didn't know this about arrow syntax auto-binding! But it still doesn't "see" the "index" . Error message: "Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `TweetBox`" Anything I can do about this? – neo5_50 Aug 23 '17 at 15:06
  • 1
    `
    ` :) Note that the `key` is relevant for every root-element in an array. In your case the `root` element is the `
    ` and not the ``
    – Dekel Aug 23 '17 at 15:09