ilrein
ilrein

Reputation: 3923

Async Meteor.call inside React components

I have a React component which holds an instance of google-maps inside of it. I'm using Meteor for the infrastructure, and the issue is that the React component is getting rendered synchronously, but to grab a server side variable, I have to resort to async. How would one handle this situation appropriately? Here's my snippet below:

import { Meteor } from 'meteor/meteor';
import React from 'react';
import { Gmaps, Marker, InfoWindow, Circle } from 'react-gmaps';

// Goals for this:
// - get the location of the user dynamically
// - see other users as markers

const coords = {
  lat: 51.5258541,
  lng: -0.08040660000006028,
};

export default class App extends React.Component {
  componentWillMount() {
    const params = {
      v: '3.exp',
      key: null,
    };

    Meteor.call('googleMapsApiKey', (err, res) => {
      if (!err) {
        params.key = res;
        console.log('params', params);
      }
    });
  }

  onMapCreated(map) {
    map.setOptions({
      disableDefaultUI: true,
    });
  }

  onDragEnd(e) {
    console.log('onDragEnd', e);
  }

  onCloseClick() {
    console.log('onCloseClick');
  }

  onClick(e) {
    console.log('onClick', e);
  }

  onDragStart(e) {
    console.log('onDragStart', e);
  }

  render() {
    return (
      <Gmaps
        width={'800px'}
        height={'600px'}
        lat={coords.lat}
        lng={coords.lng}
        zoom={12}
        loadingMessage={'Be happy'}
        params={params}
        onMapCreated={this.onMapCreated}
      >
        <Marker
          lat={coords.lat}
          lng={coords.lng}
          draggable
          onDragStart={this.onDragStart}
        />
        <InfoWindow
          lat={coords.lat}
          lng={coords.lng}
          content={'Hello, React :)'}
          onCloseClick={this.onCloseClick}
        />
        <Circle
          lat={coords.lat}
          lng={coords.lng}
          radius={500}
          onClick={this.onClick}
        />
      </Gmaps>
    );
  }
}

Here's the telltale console errors that show the sync/async issue:

enter image description here

The render method doesn't seem to wait for Async response to occur. What's the best practise to address this?

Upvotes: 1

Views: 458

Answers (3)

vipcxj
vipcxj

Reputation: 1038

The async work should be put in componentDidMount. And there is a library can help you to create a component with async props. This is the example.

import { Meteor } from 'meteor/meteor';
import React from 'react';
import PropTypes from 'prop-types';
import { Gmaps, Marker, InfoWindow, Circle } from 'react-gmaps';
import { AsyncComponent } from 'react-async-wrapper';

// Goals for this:
// - get the location of the user dynamically
// - see other users as markers

const coords = {
  lat: 51.5258541,
  lng: -0.08040660000006028,
};

class App extends React.Component {

  onMapCreated(map) {
    map.setOptions({
      disableDefaultUI: true,
    });
  }

  onDragEnd(e) {
    console.log('onDragEnd', e);
  }

  onCloseClick() {
    console.log('onCloseClick');
  }

  onClick(e) {
    console.log('onClick', e);
  }

  onDragStart(e) {
    console.log('onDragStart', e);
  }

  render() {
    return (
      <Gmaps
        width={'800px'}
        height={'600px'}
        lat={coords.lat}
        lng={coords.lng}
        zoom={12}
        loadingMessage={'Be happy'}
        params={this.props.params}
        onMapCreated={this.onMapCreated}
      >
        <Marker
          lat={coords.lat}
          lng={coords.lng}
          draggable
          onDragStart={this.onDragStart}
        />
        <InfoWindow
          lat={coords.lat}
          lng={coords.lng}
          content={'Hello, React :)'}
          onCloseClick={this.onCloseClick}
        />
        <Circle
          lat={coords.lat}
          lng={coords.lng}
          radius={500}
          onClick={this.onClick}
        />
      </Gmaps>
    );
  }
}

App.propTypes = {
    params: PropTypes.object
}

App.defaultProps = {
    params: {
        v: '3.exp',
        key: null
    }
}

const AsyncApp = () => (
    <AsyncComponent asyncProps={{
        params: () => new Promise((resolve, reject) => {
            Meteor.call('googleMapsApiKey', (err, res) => {
                if (!err) {
                    resolve({
                        v: '3.exp',
                        key: res,
                    })
                } else {
                    reject(err)
                }
            });
        })
    }}
    >
        <App />
    </AsyncComponent>
)

Upvotes: 0

ngasull
ngasull

Reputation: 4216

When you design a React component, you should think about the multiple states it could have. For instance, your Google maps component has to load before displaying a map, so it has two states: loading and loaded. It depends on your Meteor call's resulting key:

export default class App extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      v: '3.exp',
      key: null
    };

    Meteor.call('googleMapsApiKey', (err, res) => {
      if (!err) {
        this.setState({ key: res })
      }
    });
  }

  ...

  render() {
    if (this.state.key) {
      return (
        <Gmaps
          width={'800px'}
          height={'600px'}
          lat={coords.lat}
          lng={coords.lng}
          zoom={12}
          loadingMessage={'Be happy'}
          params={this.state}
          onMapCreated={this.onMapCreated}
        >
          <Marker
            lat={coords.lat}
            lng={coords.lng}
            draggable
            onDragStart={this.onDragStart}
          />
          <InfoWindow
            lat={coords.lat}
            lng={coords.lng}
            content={'Hello, React :)'}
            onCloseClick={this.onCloseClick}
          />
          <Circle
            lat={coords.lat}
            lng={coords.lng}
            radius={500}
            onClick={this.onClick}
          />
        </Gmaps>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
}

Basically, I just replaced your const params by a state plus added some loading management. Note that in ES6 Components, you must declare this.state in the constructor.

Upvotes: 2

omarjmh
omarjmh

Reputation: 13888

When you use a React.Component, the major difference is that the componentwillmount logic should be put in the class constructor like so:

From the babel blog:

All of the lifecycle methods but one can be defined as you would expect when using the new class syntax. The class' constructor now assumes the role previously filled by componentWillMount:

// The ES5 way
var EmbedModal = React.createClass({
  componentWillMount: function() { … },
});
// The ES6+ way
class EmbedModal extends React.Component {
  constructor(props) {
    super(props);
    // Operations usually carried out in componentWillMount go here
  }
}

Link to blog post

Upvotes: 1

Related Questions