0

I am building a React based project for study purposes. I am stuck on making a table component, that renders itself and then sends ajax request to mbaas backend to get all book entries and fill each on a new row. Here is what I've come up so far. Please forgive the large chunk of code, but since I don't yet fully understand the interactions between methods, state and render() here is the whole class:

class BooksTable extends Component {
    constructor(props) {
        super(props);
        this.state = {
            books: []
        };

        this.updateState = this.updateState.bind(this);
        this.initBooks = this.initBooks.bind(this);
    }

    componentDidMount() {
        let method = `GET`;
        let url = consts.serviceUrl + `/appdata/${consts.appKey}/${consts.collection}`;
        let headers = {
            "Authorization": `Kinvey ${sessionStorage.getItem(`authToken`)}`,
            "Content-Type": `application/json`
        };

        let request = {method, url, headers};
        $.ajax(request)
            .then(this.initBooks)
            .catch(() => renderError(`Unable to connect. Try again later.`));
    }

    deleteBook(id) {
        let method = `DELETE`;
        let url = consts.serviceUrl + `/appdata/${consts.appKey}/${consts.collection}/${id}`;
        let headers = {
            "Authorization": `Kinvey ${sessionStorage.getItem(`authToken`)}`,
            "Content-Type": `application/json`
        };

        let request = {method, url, headers};
        $.ajax(request)
            .then(() => this.updateState(id))
            .catch(() => renderError(`Unable to delete, something went wrong.`));
    }

    updateState(id) {
        for (let entry of this.state.books.length) {
            if (entry.id === id) {
                // Pretty sure this will not work, but that is what I've figured out so far.
                this.state.books.splice(entry);
            }
        }
    }

    initBooks(response) {
        console.log(`#1:${this.state.books});
        console.log(`#2:${this});
        for (let entry of response) {
            this.setState({
                books: this.state.books.concat([{
                    id: entry._id,
                    name: entry.name,
                    author: entry.author,
                    description: entry.description,
                    price: Number(entry.name),
                    publisher: entry.publisher
                }])
            }, () => {
                console.log(`#3${this.state.books}`);
                console.log(`#4${this}`);
            });
        }
    }

    render() {
        return (
            <div id="content">
                <h2>Books</h2>
                <table id="books-list">
                    <tbody>
                        <tr>
                            <th>Title</th>
                            <th>Author</th>
                            <th>Description</th>
                            <th>Actions</th>
                        </tr>
                        {this.state.books.map(x =>
                            <BookRow
                                key={x.id}
                                name={x.name}
                                author={x.author}
                                description={x.description}
                                price={x.price}
                                publisher={x.publisher} />)}
                    </tbody>
                </table>
            </div>
        );
    }
}

Now the BookRow is not very interesting, only the onClick part is relevant. It looks like this:

<a href="#" onClick={() => this.deleteBook(this.props.id)}>{owner? `[Delete]` : ``}</a>

The Link should not be visible if the logged in user is not publisher of the book. onClick calls deleteBook(id) which is method from BookTable. On successful ajax it should remove the book from state.books (array) and render.

I am particularly confused about the initBooks method. I've added logs before the loop that populates the state and as callbacks for when the state is updated. Results from log#1 and log#3 are identical, same for logs#2#4. Also if I expand log#2 (before setState) or log#4, both of those show state = [1]. How does this make sense? Furthermore if you take a look at logs#1#3 - they print [ ]. I am probably missing some internal component interaction, but I really cant figure out what.

Thanks.

Alex
  • 715
  • 1
  • 8
  • 29
  • setState actions are asynchronous, so changes might not be reflected in this.state right away. – Radio- Nov 27 '16 at 17:16

2 Answers2

1

The setState doesn't immediately update the state. So in the second iteration of your for loop, you wont be getting the new state. So make your new book list first and then set it once the new list is prepared.

Try this:

initBooks(response) {
    console.log(this.state.books, "new books not set yet")
    let newBooks = []
    for (let entry of response) {
     newBooks.push({
                id: entry._id,
                name: entry.name,
                author: entry.author,
                description: entry.description,
                price: Number(entry.name),
                publisher: entry.publisher
            })
    }
    this.setState({books: [...this.state.books, newBooks]}, () => {
        console.log(this.state.books, "new books set in the state")
    })
} 
Swapnil
  • 2,573
  • 19
  • 30
  • Thanks for answering. @free-soul, thanks as well. I'll give it a try. The thing is that in my current testing the loop goes only once, because I have only one test record. Setting state in loop is stupid, I should have thought of that, but it still feels strange.. Let me try it and get back at you. – Alex Nov 27 '16 at 20:59
  • If the loop goes only once then it should work. I copied your code and tried it. It worked for me – Swapnil Nov 27 '16 at 21:23
  • As I thought - while your point to move the setState out of loop is good, it does not help my issue. Running your code - it doesn't print 'new books set in the state'. I only get warning about key property. I still cannot explain how the logs#1#2 print different results, since they are logging the same object. Why does the printed object show updated state of length 1 and containing the object received from server response. I have't even initialized the temporary array holder yet.. – Alex Nov 27 '16 at 21:27
  • I think logs#1#2 print different results because chrome debugger does a lazy evaluation. There is a very good discussion over [here](http://stackoverflow.com/questions/4057440/is-chromes-javascript-console-lazy-about-evaluating-arrays) – Swapnil Nov 27 '16 at 21:44
  • Also, is it possible for you to share the source code? I might be able to solve your problem if I have the full code – Swapnil Nov 27 '16 at 21:45
  • Making a git repo. Will provide link in a moment. – Alex Nov 27 '16 at 21:54
  • There you go: https://github.com/achobanov/05_24-11-2016_reactjs_server-side-transpilation – Alex Nov 27 '16 at 21:58
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/129172/discussion-between-swapnil-and-alex). – Swapnil Nov 27 '16 at 22:07
1

try this:

initBooks(response = {}) {
    const books = Object.keys(response);

    if (books.length > 0) {
        this.setState((prevState) => {
            const newBooks = books.reduce((acc, key) => {
                const entry = response[key];

                return [ ...acc, {
                    id: entry._id,
                    name: entry.name,
                    author: entry.author,
                    description: entry.description,
                    price: Number(entry.name),
                    publisher: entry.publisher
                }];
            }, prevState.books);

            return { books: newBooks };
        });
    }
}

What I did here?

  • setState only if needed (ie only if there is data from API response)
  • avoids state mutation, no setState inside loop
  • using a function ((prevState, props) => newState) to ensure atomic update for reliability.

Points to ponder:

  • Don't mutate your state
  • Avoid setState inside a loop; instead prepare the new state object and do a one-time setState)

  • If you want to access the previous state while calling setState, it is advised to do it like this:

    this.setState((prevState, props) => {
      return { counter: prevState.counter + props.increment };
    });
    

    setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

yadhu
  • 15,423
  • 7
  • 32
  • 49