1

I am creating a demo application from the course. I have defined my component in following way.

const Book = (props) => {
  console.log(">>>>>>>>>>>");
  console.log(props.onAnswerSelected);
  return (
    <div className="answer" onClick={props.onAnswerSelected}>
      <h4>{props.title}</h4>
    </div>
  )
}

This component renders the book name. On clicking the book name it should pass the book title to the method onAnswerSelected. Book is composed inside another component Turn.

export default class Turn extends Component {

  render() {

    console.log("Turn props >>> ", this.props);
    return (
      <div className="row turn" style={{backgroundColor: this.highlightTheBackgroundColor(this.props.highlightmapping)}}>
        <div className="col-4 offset-1">
            <img src={this.props.author.imageUrl} className="authorimage" alt="Author"></img>
        </div>
        <div className="col-6">
          {this.props.books.map((title) => <Book title={title} key={title} onAnswerSelected={this.props.onAnswerSelected}></Book>)}
        </div>
      </div>
    )
  }

  highlightTheBackgroundColor(highlightmapping) {
    if(!highlightmapping) {
      highlightmapping = 'none';
    }
    let colors = {
      'none' : "none", 
      'correct' : "green",
      'wrong' : "orange"
    }
    console.log("Input mapping .>>> ", highlightmapping);
    console.log("Required highlight color >> ", colors[highlightmapping]);
    return colors[highlightmapping];
  }
}

This is used inside another component called AuthorQuiz.

    class AuthorQuiz extends Component {
      render() {
        console.log("TURN DATA FOUND >>> ", this.props.turnData);

        return (
          <div className="container-fluid">

            <Hero />
            <Turn {...this.props.turnData} highlightmapping={this.props.highlightmapping} onAnswerSelected={this.props.onAnswerSelected}/>
            <Continue /> 
            <Footer />

            <PreventDefaultComponent />

            {/* <Button label="Click me!"/>
            <ButtonExntended label="Click Another"/>
            <ClickCounterComponentWithState></ClickCounterComponentWithState> */}
          </div>
        );
      }
    }

The function is defined in index.js file which is as follows:


const state = {
    turnData:  getTurnData(Authors),
    allBooks: getAllBooks(Authors),
    highlight: 'none'
}

function getAllBooks(Authors) {
    const allBooks = Authors.reduce(function (p,c, i) {
        return p.concat(c.books);
    }, []);
    return allBooks;
}

function getTurnData(Authors) {
    let allBooks = getAllBooks(Authors);
    console.log("all Books >>>> ", allBooks);
    const fourRandomBooks = shuffle(allBooks).slice(0,4);
    const answer = sample(fourRandomBooks);

    return {books: fourRandomBooks,
        author: Authors.find((author) => author.books.some((title) => title === answer))
    };
}

function onAnswerSelected(event, answer) {
    console.log("On Answer Selected called ... ", answer);
    console.log("Logging the event >>> ", event.target.text);
    const isCorrect = state.allBooks.some((book) => {
        return book === answer;
    });
    state.highlight = isCorrect? "correct" : "wrong";
    // render();
}

// function render() {
    ReactDOM.render(<AuthorQuiz {...state} onAnswerSelected={onAnswerSelected}/>, document.getElementById('root'));
// }

// render();

serviceWorker.unregister();

for the sake of shortening the question I have omitted the imports. All I get is in the method is event only, I can't find the answer in it.

I am not sure what I am doing wrong. If someone can help. Thanks.

P.S I have refered to other questions from there I got the idea of passing an event but when I do event.target.dataset/text/value I get undefined, I think there has to be a react way or better way to pass the value in it.

Update : 1 This part doesn't work, I get entire h4 or div (depending on where I click in the console). further more when I try to pass the value it still doesn't give out the value in the function, all i get is events inside the function.

the output of on click is as follows:

index.js:35 On Answer Selected called ...  <h4>​Roughing it​</h4>​
index.js:35 On Answer Selected called ...  <div class=​"answer" title=​"Roughing it" value=​"Roughing it">​…​</div>​<h4>​Roughing it​</h4>​</div>​
Bilbo Baggins
  • 2,899
  • 10
  • 52
  • 77

2 Answers2

1

Nowhere in your example, you have passed the book title to the onAnswerSelected function. You can pass it from the Books component like

const Book = (props) => {
  console.log(">>>>>>>>>>>");
  console.log(props.onAnswerSelected);
  return (
    <div className="answer" onClick={(e) => props.onAnswerSelected(e, props.title)}>
      <h4>{props.title}</h4>
    </div>
  )
}

and using it in onAnswerSelected method from answer argument value

Also the reason that event.target.text/dataset/value return undefined is because you haven't set those attributes on the div element in Book and also because they aren't the defined attributes on div and hence you need to access them using getAttribute once you define them and using it from the currentTarget since it has the handler defined.

Another possible solution is

const Book = (props) => {
  console.log(">>>>>>>>>>>");
  console.log(props.onAnswerSelected);
  return (
    <div className="answer" text={props.title} onClick={props.onAnswerSelected}>
      <h4>{props.title}</h4>
    </div>
  )
}

function onAnswerSelected(event) {
    console.log("On Answer Selected called ... ", answer);
    console.log("Logging the event >>> ", event.currentTarget.getAttribute('text'));
    const answer = event.currentTarget.getAttribute('text')
    const isCorrect = state.allBooks.some((book) => {
        return book === answer;
    });
    state.highlight = isCorrect? "correct" : "wrong";
    // render();
}
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Hi, thanks for answering I don't want to use the inline function as it generates multiple unnecessary render calls. And I also don't want to do the getAttribute() thing, what other improvements/changes I need to do to solve the problem. – Bilbo Baggins Nov 26 '18 at 07:18
  • Why wouldn't you want to use getAttribute. Its usage is perfectly fine. However , you can use the `title` attribute which is a defined attribute in div along with `id` and with these you won't require getAttribute – Shubham Khatri Nov 26 '18 at 07:19
  • Check this answer on how to avoid inline arrow functions https://stackoverflow.com/questions/45053622/how-to-avoid-binding-inside-render-method/45053753#45053753 – Shubham Khatri Nov 26 '18 at 07:24
  • Thanks for your answer. I am getting different behaviour, I have updated my question. – Bilbo Baggins Nov 26 '18 at 10:04
  • 1
    If you read my answer carefully, you would notice that I have used currentTarget instead of `event.target` , currentTarget gives the element on which the handler is defined and not the element clicked – Shubham Khatri Nov 26 '18 at 10:06
  • Thanks, is it perfectly fine to do so? I am new to react and hence I am asking, if you could guide me to react best practices or some online links ? It would be very helpful. is it a good practice to define the methods within class where the element is defined? As @Fawzi mentioned – Bilbo Baggins Nov 26 '18 at 10:11
  • Fawzi has converted Book into a class component to define method in it, if you follow the link that I shared in my above comment, you will see that as one approach. However, converting a functional component to a class component just to avoid arrow function is render is more overhead then using the arrow functions inline. React has now proposed a way(Hooks) to use state and lifecycle functionalities in functional components to avoid classes. I would say React docs are the best place to start with React. They suggest all the best practices along with the donts – Shubham Khatri Nov 26 '18 at 10:17
  • Glad to have helped – Shubham Khatri Nov 26 '18 at 10:20
1

Try calling the function with the data you need in the parents.

{()=>{ props.onAnswerSelected(props.title//or id)}}

if you don't want to use the inline function try this

class Book extends Component {
  handleClick = () => {
    this.props.onAnswerSelected(this.props.title);
  }

  render() {
    return (
      <div className="answer" onClick={this.handleClick}>
      <h4>{props.title}</h4>
    </div>
    );
  }
}

this will then re-render only when the props change (because the handler reference now never changes):

c-chavez
  • 7,237
  • 5
  • 35
  • 49
Fawzi
  • 359
  • 2
  • 12
  • I don't want to use the inline function as it generates multiple renders. – Bilbo Baggins Nov 26 '18 at 07:16
  • I haven't defined my handleClick in the same component, I am not sure whether this will impact it or not but it is still not working all I get is

    tag or a
    tag that depends on where I click.

    – Bilbo Baggins Nov 26 '18 at 10:01
  • you need to define it in the same component so that you can send the parameter. – Fawzi Nov 26 '18 at 10:04