7

Day 1 with React and I'm not seeing a way to get the < button> onClick={this.props.handleClickPlay}>Play < /button> to play audio. If I move it under {audioListNodes} the button works fine. I'd like to have each link play a separate audio file eventually but for right now just playing the same file is a win, but moving the event handler into the list kills it. I'm assuming it's because THIS is no longer referencing AudioList but rather var data? Once I have the button firing how do I ID which button was clicked and change the AudioObject source?

    var data = [
    {voice: "Drew", file:"Drew.mp3", volume: 90},
    {voice: "Chris", file:"Chris.mp3", volume: 85},
    {voice: "Patrick", file:"Patrick.mp3", volume: 85},
    {voice: "Everett", file:"Everett.mp3", volume: 60},
    ];

    var AudioList = React.createClass({
      render: function() {
        var audioListNodes = this.props.data.map(function(el, index) {
          return (
            <div author={el.voice}>
              {el.file}
                <button onClick={this.props.handleClickPlay}>Play</button>
            </div>
          );
        });  
        return (
          <div className="audioList">
            {audioListNodes}
          </div>
        );
      }
    });
    var AudioObject = React.createClass({
        play: function () {
            var audio = this.getDOMNode();
            audio.load();
            audio.play();
        },
        render: function() {
            return (
                <audio preload="auto" controls="true" autoPlay="">
                    <source src='female/DeDe Splaingard.mp3'></source>
                    Your browser does not support audio.
                </audio>
            );
        } 
    });
    var App = React.createClass({
        handleClickPlay: function() {
            this.refs.audioObject.play()
        },
        render: function() {
            return (
                <div>
                    <AudioObject ref="audioObject" />
                    <AudioList data={this.props.data} handleClickPlay={this.handleClickPlay} />
                </div>
            );
        } 
    }); 
    ReactDOM.render(
      <App data={data} />,
      document.getElementById('content')
    );
Adam L
  • 177
  • 1
  • 1
  • 9
  • 1
    Possible duplicate of ["this" is undefined inside map function Reactjs](http://stackoverflow.com/questions/30148827/this-is-undefined-inside-map-function-reactjs) – Dan Prince Jan 22 '16 at 02:42

2 Answers2

15

You have to bind(this) to the anonymous function since the context inside of .map changes:

var audioListNodes = this.props.data.map(function(el, index) {
  return (
    <div author={el.voice}>
      {el.file}
      <button onClick={this.props.handleClickPlay}>Play</button>
    </div>
  );
}.bind(this));

Another option is to start using ES6 arrow functions, which lexically pass this:

var audioListNodes = this.props.data.map((el, index) => {
  return (
    <div author={el.voice}>
      {el.file}
      <button onClick={this.props.handleClickPlay}>Play</button>
    </div>
  );
});

As @Henrik Andersson mentioned in the comments, you can also pass this directly to map.

var audioListNodes = this.props.data.map(function(el, index) {
  return (
    <div author={el.voice}>
      {el.file}
      <button onClick={this.props.handleClickPlay}>Play</button>
    </div>
  );
}, this);
Matthew Herbst
  • 29,477
  • 23
  • 85
  • 128
  • 2
    `.map()` also takes a second argument, `this` which will set the context right. – Henrik Andersson Jan 22 '16 at 04:34
  • 1
    @HenrikAndersson who knew! Just modified my answer to include that, thanks. – Matthew Herbst Jan 22 '16 at 04:59
  • Perfect! Works like a charm now. The several tutorials I read so far didn't have anything structured how I have this with an event inside a loop so wasn't even sure if I was doing this the "React Way". Thanks again – Adam L Jan 22 '16 at 17:40
  • Ok another question. If the AudioList component holds the buttons (events) and React data only flows one way from App --> AudioList then how can I ID what button was clicked and send that to the AudioObject component so the users click changes the audio played? Does all the logic need to live in App, that doesn't seem right. Seems like i'd need to pass state from AudioList to App to AudioObject? OMG I thought this was going to be a simple React example app. – Adam L Jan 22 '16 at 20:43
  • @AdamL tl;dr: pass the `onClick` handler from `App` to `AudioList` via `AudioList`'s props. That way you can call it in `AudioList` and it will change the state of `App` (causing both to re-render). Longer explanation: ask a new question :) – Matthew Herbst Jan 22 '16 at 20:47
  • In `onClick={this.props.handleClickPlay}`, shouldn't it be `onClick={this.handleClickPlay}`? The former didn't work for me, and it makes sense (i.e., `handleClickPlay` now part of local context). Please double check. – user1322092 Nov 26 '16 at 18:41
  • @user1322092 `this.props.handleClickPlay` is correct. `AudioList` is its own component, and `handleClickPlay` is defined on the parent `App` component. You can see in `App`'s `render` that `handleClickPlay` is passed as a prop to `AudioList`. – Matthew Herbst Nov 26 '16 at 20:32
  • @MatthewHerbst, thanks!! I missed that AudioList was its own component. – user1322092 Nov 27 '16 at 17:58
4

You should add this to .map(), because inside of .map() this refers to window or undefined when in strict mode.

var audioListNodes = this.props.data.map(function(el, index) {
      return (
        <div author={el.voice}>
          {el.file}
            <button onClick={this.props.handleClickPlay}>Play</button>
        </div>
      );
}, this);
 ! here !