0

I have just graduated a bootcamp and working on my first project in React. I have a fetch request that renders back an array of objects from my Rails DB. They are books. And each book has specific reviews. I therefore have all my books rendered in my array as per below and i have another fetch request for the individual book that is clicked on so it opens up more details about it like author, description, etc along with its reviews. Everything is great up until the point when i click on another book. Then the reviews from my last clicked book will get rendered in all the books that have been opened before it. I do appreciate is the state that is correctly rendered but i am wondering if there is a way of persisting each state per individual book? I am using Rails 5.1.4 with WebPacker not react-rails, this is my code below: Thank you so very much for your kind assistance!

var PropTypes = require ('prop-types')
var Reviews = require ('./reviews.jsx')
var ReactStars = require ('react-stars')
var BuyButton = require ('images/buyButton.jpg')
var MainBasket = require('./mainBasket.jsx');

var reviewsArray = [];

class EachBook extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      reviews: reviewsArray
    }
  }

  getReview(bookId){

    fetch(`http://localhost:3000/books/${bookId}/reviews`,{
      method: 'POST',
      mode: 'cors',
      body: JSON.stringify({comment: this.refs.comment.value}),
      headers: new Headers({
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'X-CSRF-Token' : this.props.parent.props.parent.props.csrf
      }),
      credentials: 'same-origin'
    }).then(response => response.json())
    .catch(error => alert("There is something wrong with this request"))
    .then( response => {
     this.state.reviews.push(response);
     this.setState(this.state)
    })
  }
render(){
    return(
      <div>

       <MainBasket items={this.state.items}  total={this.state.total} />

        <div className="container"> 
          {this.props.singleBook.map(indBook =>   
            <div className="indBook" key={indBook.id}>
              <h1>{indBook.title}</h1> <br />
              <h2>{"Author: " + indBook.author}</h2> <br />
              <h4>{"Genre: " + indBook.genre}</h4>
              <h4>{"Price: " + indBook.price}£</h4>
              <img src={indBook.image.url} />
              <div className="button"><img src={BuyButton} onClick={function(){this.addItemToBasket(indBook.id)}.bind(this)}/></div>
              <div className="description">{indBook.description}</div>

              <h3> Reviews for {indBook.title} :</h3>
              <Reviews allReviewsTogether={reviewsArray} />
              <div> 
                {this.props.singleBookReviews.map(eachReview => 
                  <div key={"eachReview-" + eachReview.id}>
                    <h5> Left by {eachReview.user.name} on {eachReview.created_at.slice(0,10)}</h5>
                    <div>{eachReview.comment}</div>
                  </div>
                )}
                <h4>Leave a new review</h4> 
                  <textarea ref="comment" type="text" placeholder='Tell us your thoughts '></textarea>
                  <button onClick={ function(){this.getReview(indBook.id)}.bind(this) } >Submit</button>       
              </div>

            </div>
          )}   
        </div> 
      </div>   
    )
  }          
}
module.exports = EachBook;


var React = require ('react')
var ReactDOM = require ('react-dom')
var PropTypes = require ('prop-types')


class Reviews extends React.Component {
  constructor(props){
    super(props);
  }

  render(){
    return(
      <div className="bookReview">
        {this.props.allReviewsTogether.map( review =>
          <div key={"review-" + review.id}>
            {review.comment}
          </div>
        )}
      </div>
    )
  }
}
module.exports = Reviews;

Please note the screenshot in the link below: my first book Self Reliance which has reviews gets rendered ok i can see them but when i click on the second one Huckleberry Finn which does not have any reviews yet, it updated the Self Reliance one with no reviews as well. I cannot post a second link with the first image as well unfortunately i am only allowed to one picture. Many thanks again!

books component which renders each book:

import React, { Component } from 'react';
import EachBook from './eachBook';


class Books extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      individualBook: [],
      bookReviews: []
    }  
  }

  bookDetails(bookId){
    fetch(`http://localhost:3000/books/${bookId}`)
    .then(response => response.json())
    .then(book => {
      this.state.individualBook.push(book);
      this.setState(this.state);
    })
  }  



  render(){
    return(
      <div className="container">
        <EachBook singleBook={this.state.individualBook} parent={this}  />
        <div className="row">
          {this.props.allBooks.map(book => 
            <div className="col-sm" key={book.id} id={"bookId-" + book.id}>
              <h1><a href="">{book.title}</a></h1>
              <img src={book.image.url} className="image-style" />
              <button onClick={function(){this.bookDetails(book.id)}.bind(this)}> Read More </button>
            </div>
          )}
        </div>
      </div>
    )
  }
}

export default Books;
Corina Feraru
  • 39
  • 2
  • 8
  • Could you please show us your EachBook's render method? For surely solution for this. – thanhnha1103 Feb 08 '18 at 04:43
  • @thanhnha1103 thank you very much i have just added the rest of the code above – Corina Feraru Feb 08 '18 at 11:12
  • you said that `Everything is great up until the point when i click on another book`. I think the click action that you mean is not `BuyButton` nor `Submit Review Button`, right? They're not the root cause for this issue. Could you please explain or show us which one is the click action on booking? – thanhnha1103 Feb 09 '18 at 03:08
  • @thanhnha1103 many thanks for your prompt reply that's correct indeed the button that opens another book's details is in my books component i will paste the code above. so in my books i have a button "read more" that opens each book component with its props title, author, genre, description, image url and the nested component reviews. So even if the fetch request for the reviews of the specific book is correctly executed, when i open another book its reviews overwrite the ones above it (the first initial book opened) thank you so much for your help! – Corina Feraru Feb 10 '18 at 21:11
  • I see your code show a list of books (allBooks), then when you click on a book, you add it to an array (individualBook) and pass it as props to EachBook to render. But problem is your EachBook just have only one array to store all the reviews of books you have clicked. The components structure is not good. I will make some change in answer section. Hope it helps. – thanhnha1103 Feb 11 '18 at 07:33

2 Answers2

0

this.setState({reviews: response}) You don’t want to mutate state like you are doing. Anytime you want to update the state make sure that you are using setState.

Brian McCall
  • 1,831
  • 1
  • 18
  • 33
  • many thanks Brian it does make more sense indeed to just set the state like this instead of pushing into the array and setting it afterwards. i deleted this.state.reviews.push(response) and i just have this.setState({reviews: response}) and now reviews is just an empty array. but it still changes the state to all the books when i click on a second book. Is it because i render the state like this ? Thank you very much for your help! – Corina Feraru Feb 08 '18 at 11:50
0

In this, I keep your allBooks list

When we click on a book. It will render only selected book.

When we submit a review it will get all reviews of current selected book.

The thing you have to do to make this better is load by default selectedBook's reviews when you click "Read more". (*)

import React, { Component } from 'react';
import EachBook from './eachBook';

class Books extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      // prev, you named individualBook but it is array of selected books
      // individualBook: [],
      // let's rename individualBook to selectedBook and make it a be a single selected book
      selectedBook: undefined
    }
  }

  bookDetails(bookId){
    fetch(`http://localhost:3000/books/${bookId}`)
    .then(response => response.json())
    .then(book => {
      this.setState({selectedBook: book});
    })
    // you can do (*) - request selectedBook's reviews here and pass it to singleBook too
  }

  render(){
    // just render EachBook if selectedBook already set
    // you can put the logic "selectedBook != undefined" here or movev it into EachBook component
    return(
      <div className="container">
        {selectedBook != undefined &&
          <EachBook singleBook={this.state.selectedBook} parent={this} />
        }
        <div className="row">
          {this.props.allBooks.map(book =>
            <div className="col-sm" key={book.id} id={"bookId-" + book.id}>
              <h1><a href="">{book.title}</a></h1>
              <img src={book.image.url} className="image-style" />
              <button onClick={function(){this.bookDetails(book.id)}.bind(this)}>Read More</button>
            </div>
          )}
        </div>
      </div>
    )
  }
}

export default Books;

Then your EachBook and Reviews:

var PropTypes = require ('prop-types')
var Reviews = require ('./reviews.jsx')
var ReactStars = require ('react-stars')
var BuyButton = require ('images/buyButton.jpg')
var MainBasket = require('./mainBasket.jsx');

class EachBook extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      reviews: []
    }
  }

  getReview(bookId){
    fetch(`http://localhost:3000/books/${bookId}/reviews`,{
      method: 'POST',
      mode: 'cors',
      body: JSON.stringify({comment: this.refs.comment.value}),
      headers: new Headers({
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'X-CSRF-Token' : this.props.parent.props.parent.props.csrf
      }),
      credentials: 'same-origin'
    }).then(response => response.json())
    .catch(error => alert("There is something wrong with this request"))
    .then( response => {
      this.setState({reviews: response})
    })
  }

  render(){
    let indBook = this.props.selectedBook
    return(
      <div>
       <MainBasket items={this.state.items}  total={this.state.total} />
        <div className="container">
          <div className="indBook" key={indBook.id}>
            <h1>{indBook.title}</h1> <br />
            <h2>{"Author: " + indBook.author}</h2> <br />
            <h4>{"Genre: " + indBook.genre}</h4>
            <h4>{"Price: " + indBook.price}£</h4>
            <img src={indBook.image.url} />
            <div className="button">
              <img src={BuyButton} onClick={function(){this.addItemToBasket(indBook.id)}.bind(this)}/>
            </div>
            <div className="description">{indBook.description}</div>
            <h3> Reviews for {indBook.title} :</h3>
            <Reviews allReviewsTogether={reviews} />
            <div>
              <h4>Leave a new review</h4>
                <textarea ref="comment" type="text" placeholder='Tell us your thoughts'></textarea>
                <button onClick={ function(){this.getReview(indBook.id)}.bind(this) }>
                  Submit
                </button>
            </div>
          </div>
        </div>
      </div>
    )
  }
}
module.exports = EachBook;

var React = require ('react')
var ReactDOM = require ('react-dom')
var PropTypes = require ('prop-types')

class Reviews extends React.Component {
  constructor(props){
    super(props);
  }

  render(){
    return(
      <div className="bookReview">
        {this.props.allReviewsTogether.map( review =>
          <div key={"review-" + review.id}>
            {review.comment}
          </div>
        )}
      </div>
    )
  }
}
module.exports = Reviews;

If this show selected book and it's reviews is not your purpose and you want to show all selected books with, their reviews. Let me know.

thanhnha1103
  • 1,037
  • 1
  • 8
  • 18
  • thank you so much you enlightened me. i cannot use the undefined approach because i need an array so i can iterate over it with map to render the elemenets of my object and the individualBook renders only one book indeed, but i moved my reviews from eachBook into Books so a fetch request would give me both the each books details and its reviews which is fantastic this is exactly what i wanted.Now when i open a second book it would have the correct reviews and the previous ones would be blank as the state moves to the current opened book. Thanks a lot again wish you all the best! – Corina Feraru Feb 11 '18 at 11:46
  • Awesome! Thank you too. – thanhnha1103 Feb 12 '18 at 03:12