daneasterman daneasterman - 1 month ago 7
React JSX Question

React: Can't "access" click handler within returning JSX

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.

Answer Source

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.