Search code examples
javascriptreactjsparse-server

Can't get React component to re-render after state has been updated with returned data from Parse Server


I’m having trouble rendering a component with data from state. I’m pulling data from Parse Server and then updating state with that data. However, my component that uses the data (<MenuItem /> in the code below) was rendering before the data became available.

I tried the method described here: How to wait for AJAX response and only after that render the component?

But I couldn't get the component to recognize state being updated and then re-rendered.

Any ideas? Code is below in full. Also attached is a screenshot showing the successful console log and the existence of the data in state.

(It should be noted that I am new to React so a lot of the code will likely not be optimal.)

import React from "react";
import Parse from 'parse';

import MenuItem from './MenuItem';

class MenuItems extends React.Component {
  constructor() {
    super();

    this.loadMenuItems = this.loadMenuItems.bind(this);

    this.state = {
      menuItems: {},
      order: {}
    };
  }

  loadMenuItems() {
    /* 0a. Get copy of State? */
    const menuItems = {...this.state.menuItems};

    /* 0. Get User */
    // TODO: Get this from props
    var user = Parse.User.current();

    /* 1. Get Menu Items for this user */
    var MenuItemTest = Parse.Object.extend('MenuItemTest');
    var query = new Parse.Query(MenuItemTest);

    query.equalTo('user', user);
    query.find({
      success: function(returnedMenuItems) {
        console.log("Successfully retrieved " + returnedMenuItems.length + " menu items.");

        for (var i = 0; i < returnedMenuItems.length; i++) {
          var object = returnedMenuItems[i];

          const menuItem = {
            name: object.get('name'),
            price: object.get('price'),
            description: object.get('description'),
          }

          menuItems[`menu-item-${object.id}`] = menuItem;
        }
      },
      error: function(error) {
        console.log("Error: " + error.code + " " + error.message);
      }
    });

    this.setState({ menuItems });
  }

  componentWillReceiveProps(nextProps) {
    console.log(nextProps);
    this.loadMenuItems();
  }

  componentWillMount() {
    this.loadMenuItems();
  }

  render() {
    // Example from:
    // https://stackoverflow.com/questions/31723095/how-to-wait-for-ajax-response-and-only-after-that-render-the-component

    if (!this.state.response) {
      return <div>Loading...</div>;
    }

    if (this.state.response.length === 0) {
      return <div>No menu items yet</div>;
    }

    return (
      <div className="menu-items">
        {
          Object
            .keys(this.state.menuItems)
            .map(key => <MenuItem key={key} details={this.state.menuItems[key]} />)
        }
      </div>
    )
  }
}

export default MenuItems;

Screenshot showing state with items

Successful console log

UPDATED CODE to coincide with comment thread.

import React from "react";
import Parse from 'parse';

import MenuItem from './MenuItem';

class MenuItems extends React.Component {
  constructor() {
    super();

    this.loadMenuItems = this.loadMenuItems.bind(this);
  }

  loadMenuItems() {
    const menuItems = {};

    /* 0. Get User */
    var user = Parse.User.current();

    /* 1. Get Menu Items for this user */
    var MenuItemTest = Parse.Object.extend('MenuItemTest');
    var query = new Parse.Query(MenuItemTest);

    query.equalTo('user', user);
    query.find({
      success: function(returnedMenuItems) {
        console.log("Successfully retrieved " + returnedMenuItems.length + " menu items.");

        for (var i = 0; i < returnedMenuItems.length; i++) {
          var object = returnedMenuItems[i];

          const menuItem = {
            name: object.get('name'),
            price: object.get('price'),
            label: object.get('label'),
            shorthand: object.get('shorthand'),
            description: object.get('description')
          }

          menuItems[`menu-item-${object.id}`] = menuItem;
        }
      },
      error: function(error) {
        console.log("Error: " + error.code + " " + error.message);
      }
    });

    this.setState({ menuItems });
  }

  componentWillReceiveProps(nextProps) {
    console.log('I ran');
    console.log(nextProps);
    this.loadMenuItems();
  }

  componentWillMount() {
    this.loadMenuItems();
  }

  render() {
    // Handle case where the response is not here yet
    if (!this.state.menuItems) {
      console.log('No state');
      return <div>Loading...</div>;
    }

    // Gives you the opportunity to handle the case where the ajax request
    // completed but the result array is empty
    if (this.state.menuItems.length === 0) {
      console.log('Zero items');
      return <div>No menu items yet</div>;
    }

    if (this.state.menuItems) {
      return (
        <div className="menu-items">
          { console.log(this.state.menuItems) }
          { console.log(Object.keys(this.state.menuItems)) }
        </div>
      )
    }
  }
}

export default MenuItems;

Solution

  • I found the solution. Though in trying to solve my problem I moved some code around to set the state on the parent component and pass it to MenuItems as props. But why it wasn't working is still the same.

    It has to do with loadMenuItems():

    loadMenuItems() {
      const menuItems = {};
    
      /* 0. Get User */
      var user = Parse.User.current();
    
      /* 1. Get Menu Items for this user */
      var MenuItemTest = Parse.Object.extend('MenuItemTest');
      var query = new Parse.Query(MenuItemTest);
    
      query.equalTo('user', user);
      query.find({
        success: function(returnedMenuItems) {
          console.log("Successfully retrieved " + returnedMenuItems.length + " menu items.");
    
          for (var i = 0; i < returnedMenuItems.length; i++) {
            var object = returnedMenuItems[i];
    
            const menuItem = {
              name: object.get('name'),
              price: object.get('price'),
              label: object.get('label'),
              shorthand: object.get('shorthand'),
              description: object.get('description')
            }
    
            menuItems[`menu-item-${object.id}`] = menuItem;
          }
        },
        error: function(error) {
          console.log("Error: " + error.code + " " + error.message);
        }
      });
    
      this.setState({ menuItems });
    }
    

    What was happening was this.setState({ menuItems }) was running before the success callback was executed. So the state was always empty, but by the time I inspected everything in console it had returned and looped through menuItems[menu-item-${object.id}] = menuItem; and everything was there.

    So what I did was move this.setState({ menuItems }); in the success callback and then added .bind(this) so I could set state there:

    success: function(returnedMenuItems) {
      ...
    }.bind(this),
    

    And I believe the components were never updating because although state was set with the menuItems[menu-item-${object.id}] = menuItem; loop this.setState() was never called again and this is what triggers the component refresh. I could be off on some of this, but I think this is what was happening.

    I've posted updated code below for future reference.

    Parent Component:

    import React from "react";
    import Parse from 'parse';
    
    import MenuItems from './MenuItems';
    
    
    class ManageMenu extends React.Component {
      constructor() {
        super();
    
        this.loadMenuItems = this.loadMenuItems.bind(this);
      }
    
      loadMenuItems() {
        const menuItems = {};
    
        /* 0. Get User */
        var user = Parse.User.current();
    
        /* 1. Get Menu Items for this user */
        var MenuItemTest = Parse.Object.extend('MenuItemTest');
        var query = new Parse.Query(MenuItemTest);
    
        query.equalTo('user', user);
        query.find({
          success: function(returnedMenuItems) {
            console.log("Successfully retrieved " + returnedMenuItems.length + " menu items.");
    
            for (var i = 0; i < returnedMenuItems.length; i++) {
              var object = returnedMenuItems[i];
    
              const menuItem = {
                name: object.get('name'),
                price: object.get('price'),
                label: object.get('label'),
                shorthand: object.get('shorthand'),
                description: object.get('description')
              }
    
              menuItems[`menu-item-${object.id}`] = menuItem;
            }
    
            this.setState({ menuItems });
          }.bind(this),
          error: function(error) {
            console.log("Error: " + error.code + " " + error.message);
          }
        });
      }
    
      componentWillMount() {
        this.loadMenuItems();
      }
    
      render() {
        // Handle case where the response is not here yet
        // console.log(this.state);
        if (!this.state) {
          console.log('No state');
          // Note that you can return false it you want nothing to be put in the dom
          // This is also your chance to render a spinner or something...
          return <div>Loading...</div>;
        }
    
        if (this.state.menuItems) {
          return (
            <div className="manage-menu">
              <div className="mt3 border-top">
                <h3 className="mt1 bold s2">Menu Items</h3>
    
                <div className="mt1">
                  <MenuItems menuItems={this.state.menuItems} />
                </div>
              </div>
            </div>
          )
        }
      }
    }
    
    export default ManageMenu;
    

    MenuItems Component:

    import React from "react";
    
    import MenuItem from './MenuItem';
    
    class MenuItems extends React.Component {
      render() {
        // Same as: const menuItems = this.props.menuItems;
        const { menuItems } = this.props;
    
        // No menu items yet
        if (menuItems.length === 0) {
          console.log('Zero items');
          return <div>No menu items yet</div>;
        }
    
        if (menuItems) {
          return (
            <div className="menu-items">
              {
                Object
                  .keys(menuItems)
                  .map(key => <MenuItem key={key} details={menuItems[key]} />)
              }
            </div>
          )
        }
      }
    }
    
    export default MenuItems;