Search code examples
javascriptreactjsreactstrap

Changing the state from reusable component


i'm trying to to add data to the main App through an reusable component, the problem is that when i use setState, nothing is happening.

In the main app, i m calling the BookModal and passing the

state:

    state= {
    books: [
      {id: uuidv4(),name: 'The Kingkiller Chronicle', isbm:'5435-54',quantity: '4', price:'14.99', isBorrowed: false, remainingDays: 14},
      {id: uuidv4(),name: 'Jane Eyre', isbm:'643543-21',quantity: '2', price:'19.99', isBorrowed: false, remainingDays: -3}
    ],
    newBookModal: false,
    editBookModal: false,
    newBookData: {
      id: '',
      name: '',
      isbm: '',
      quantity:'',
      price: '',
      isBorrowed: false,
      remainingDays: 14
    },
    editBookData: {
      id: '',
      name: '',
      isbm: '',
      quantity:'',
      price: ''
    }
  }

And here i render the component:

<BookModal booksData={this.state}/>

In the modal:

    import { Component } from 'react';
import React from 'react';
import {
  Button,
  Modal,
  ModalHeader,
  ModalBody,
  FormGroup,
  Label,
  Input,
  ModalFooter
 } from 'reactstrap';
 import { v4 as uuidv4 } from 'uuid';

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

        this.state = {
            newBookData: {
                id: '',
                name: '',
                isbm: '',
                quantity:'',
                price: '',
                isBorrowed: false,
                remainingDays: 14
              },
              newBookModal: false,
        }

        this.openNewBookModal  = this.openNewBookModal.bind(this);
        this.addBook = this.addBook.bind(this)
    }
     async openNewBookModal () {
         console.log(this.state.newBookModal);
        this.setState({
          newBookModal: !this.props.booksData.newBookModal//the opposite of the state
        });
      };

       addBook  () {
        let { books } = this.props.booksData;
        
        books.push(this.props.booksData.newBookData);
    
        await this.setState({ books, newBookModal: false, newBookData: {
          id: uuidv4(),
          name: '',
          isbm: '',
          quantity:'',
          price: '',
          isBorrowed: false
        }});
      }
render() {
  return(
    <Modal isOpen={this.props.booksData.newBookModal} toggle={this.openNewBookModal}>
    <ModalHeader toggle={this.openNewBookModal}>Add a new book</ModalHeader>
    <ModalBody>
      <FormGroup>
        <Label for="title">Title</Label>
        <Input id="title" value={this.props.booksData.newBookData.name}  onChange={(e) => {
          let { newBookData } = this.props.booksData;
          newBookData.name = e.target.value;
  
          this.setState({ newBookData });
        }} />
      </FormGroup>
  
      <FormGroup>
        <Label for="isbm">ISBM</Label>
        <Input id="isbm" value={this.props.booksData.newBookData.isbm}  onChange={(e) => {
          let { newBookData } = this.props.booksData;
  
          if (e.target.value === '' || e.target.value.match(/^\d{1,}(\-\d{0,2})?$/)) {
            newBookData.isbm = e.target.value;
          }
  
          this.setState({ newBookData });
        }} />
      </FormGroup>
  
      <FormGroup>
        <Label for="quantity">Quantity</Label>
        <Input id="quantity" value={this.props.booksData.newBookData.quantity}  onChange={(e) => {
          let { newBookData } = this.props.booksData;
  
          if (e.target.value === '' || e.target.value.match(/^\d{1,9}?$/)) {
            newBookData.quantity = e.target.value;
          }
  
          this.setState({ newBookData });
        }} />
      </FormGroup>
  
      <FormGroup>
        <Label for="price">Price</Label>
        <Input id="price" value={this.props.booksData.newBookData.price}  onChange={(e) => {
          let { newBookData } = this.props.booksData;
          if (e.target.value === '' || e.target.value.match(/^\d{1,}(\.\d{0,2})?$/)) {
            newBookData.price = e.target.value;
          }
  
          this.setState({ newBookData });
        }} />
      </FormGroup>
    </ModalBody>
    <ModalFooter>
      <Button color="primary" onClick={this.addBook}>Add Book</Button>{' '}
      <Button color="secondary" onClick={this.openNewBookModal}>Cancel</Button>
    </ModalFooter>
  </Modal>
       )
    }
}

export default BookModal;

The problem seems to be in addBook, because it is not aware of the state from the main App component, how can i make this work, so i can enter the books.


Solution

  • This code has some problems, but I think the main one is conceptual. One of the things that React encourages you to do is to find the least amount of state possible.

    In your app, the App component appears to handle the application's state. It tracks the data that you'll need to display in various ways, namely the list of books. Then, as you should, you pass this data, as props, down to a child component that will handle displaying this data, in this case a modal.

    Where you go wrong is what you do next. The <BookModal /> component should only care about displaying the props it's given, and yet you spend a lot of code basically storing the props in BookModal's state. Why? BookModal has everything it needs in the props it was passed.

    "But," you'll say, "the modal has a form that the user will use to add a new book. How will the child component, BookModal, pass that data to the parent, App?" The answer is that it won't! App tracks the state, so App should expose a function to its children that can add a book to the state. How do you get this to the child? Pass it as a prop! The only state BookModal needs is that which will allow it to control the form components.

    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {books: []};
      }
    
      addBook(bookToAdd) {
        // instead of using push, which mutates the state (generally a bad idea), we'll make a copy of the state books array using the ES6 spread operator and add the new book
        this.setState({ books: [...this.state.books, bookToAdd ] });
      }
    
      render() {
        return (
          {/* you don't even really need to pass the app state down to this component if all it does is render a form, but we'll leave it for now */}
          <BookModal data={this.state} addBook={this.addBook} />
        );
      }
    }
    
    class BookModal extends React.Component {
      constructor(props) {
        super(props);
        this.state = { 
          title: '',
          isbn: '',
          quantity: '0',
          price: '0',
        }
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleSubmit() {
        // call the function we have been passed as a prop with the new book and let the App component handle the change in our application state
        const bookToAdd = { 
          title: this.state.title,
          isbn: this.state.isbn,
          quantity: this.state.quantity,
          price: this.state.price,
        }
        this.props.addBook(bookToAdd);
      }
      
      render() {
        return (
          <Modal>
            <FormGroup>
              <Label for="title">Title</Label>
              <Input id="title" value={this.state.title} onChange={(e) => this.setState({title: e.target.value})} />
            </FormGroup>
            <FormGroup>
              <Label for="isbn">ISBN</Label>
              <Input id="isbn" value={this.state.isbn} onChange={(e) => this.setState({isbn: e.target.value})}/>
            </FormGroup>
            <FormGroup>
              <Label for="quantity">Quantity</Label>
              <Input id="quantity" value={this.state.quantity} onChange={(e) => this.setState({quantity: e.target.value})}/>
            </FormGroup>
            <FormGroup>
              <Label for="isbn">Price</Label>
              <Input id="price" value={this.state.price} onChange={(e) => this.setState({isbn: e.target.value})}/>
            </FormGroup>
            <Button color="primary" onClick={this.handleSubmit}>Add Book</Button>
          </Modal>
        );
      }
    }
    

    EDIT: I haven't worked with class components in a while, so apologies if there are minor errors here (can't believe we used to bind!). Hopefully, my main point gets across.