React renders before Meteor.call finishes

I have a situation where React renders before data fetched from Oracle database using Meteor.call is ready, and I can’t figure out how to solve it.

The below handleOnDrop method handles dropped files and works for files that don’t require a Meteor.call as determined in parserHelper:

  /**
   * Event handler for files dropped on Dropzone.
   * @param {Array} files List of File objects from a dropzone.
   */
  handleOnDrop(files) {
    if (files.length === 0) {
      return;
    }

    const droppedFile = files[0];
    const reader = new FileReader();

    reader.onload = (event) => {
      const parserHelper = new ParserHelper(this.state.plateData, event, droppedFile);

      this.setState({
        droppedFile: droppedFile.name,
        plateBarcode: parserHelper.plateBarcode,
        plateData: parserHelper.plateData,
        fileType: parserHelper.fileType,
        types: parserHelper.types,
        compounds: parserHelper.compounds,
        statusData: parserHelper.statusData,
        selectedStatus: parserHelper.selectedStatus,
        tableData: parserHelper.tableData,
        columns: parserHelper.columns,
        parsed: parserHelper.parsed,
      });
    };

    reader.readAsText(droppedFile);
  }

The parserHelper constructor calls this method for certain dropped files where we need data from Oracle (the database returns data nicely):

  parseEnvision() {
    const parser = new ParseEnvision(this._event.target.result);
    const newPlate = parser.plate;
    const plateBarcode = parser.plateBarcode;

    Meteor.call('getPlate', plateBarcode, function(error, plateRecord) {
      if (error) {
        throw new Error(`getPlate failed for barcode: ${plateBarcode}`);
      }

      const plate = new Plate({
        rowSize: plateRecord._rowSize,
        columnSize: plateRecord._columnSize,
        wells: plateRecord._wells,
      });

      newPlate.merge(plate);

      const plateData = this._plateData
        ? this._plateData.merge(newPlate)
        : newPlate;

      const helper = new PlateHelper(plateData);

      this._plateBarcode = plateBarcode;
      this._plateData = plateData;
      this._types = plateData.types;
      this._compounds = helper.getUniqueCompounds();
      this._statusData = prepareStatusData(plateData.types);
      this._parsed = true;
    }.bind(this));
  }

The render methods starts with:

  render() {
    if (this.state.droppedFile && !this.state.parsed) {
      return <div>Loading ...</div>;
    }

    return (
...

And never gets beyond Loading ... because this.state.parsed is false. But only so for dropped files that invokes the Meteor.call - it renders nicely in other cases.

As Meteor.call is async, this works just as it should. You are calling setState before the meteor call has time to finish.

I’d recommend you don’t do unneccesary work in the constructor. You call the parseEnvision method manually after creating it, and pass along a callback which calls the setState. In parseEnvision, you trigger that callback inside the meteor.call callback, after all the data has been set (after this._parsed = true)

so

const parserHelper = new ParserHelper(this.state.plateData, event, droppedFile);
parserHelper.parseEnvision( parsedData => {
      this.setState({
        droppedFile: droppedFile.name,
        plateBarcode: parsedData.plateBarcode,
        plateData: parsedData.plateData,
        fileType: parsedData.fileType,
        types: parsedData.types,
        compounds: parsedData.compounds,
        statusData: parsedData.statusData,
        selectedStatus: parsedData.selectedStatus,
        tableData: parsedData.tableData,
        columns: parsedData.columns,
        parsed: parsedData.parsed,
      });
})
parseEnvision(done) {
    const parser = new ParseEnvision(this._event.target.result);
    const newPlate = parser.plate;
    const plateBarcode = parser.plateBarcode;

    Meteor.call('getPlate', plateBarcode, (error, plateRecord) => {
      // ...
     // ....
      this._parsed = true;
      done(this);
    });
  }

Another way to do it, is to just call the meteor method inside the onload function, and pass the plateRecord into the constructor for ParserHelper

Thanks, that did the trick!