Async Meteor.call inside React components - javascript

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:
The render method doesn't seem to wait for Async response to occur. What's the best practise to address this?

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.

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

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>
)

Related

Why isn't Google Maps reloading with new coordinates, with the rest of list detail?

I have a list of events, and each event has a click function that displays the details of that event. Each detail holds a google map which takes the latlng coordinates found inside the clicked event object.
My problem is this: when I click on an event for the first time, the detail comes out correctly, with the correct title, location name and map. But then, the second click renders the new detail of the new event with the correct title, location name BUT the map doesn't change.
Why isn't the map being reloaded with the new coordinates?
here is the code for the 'event list' container:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { selectEvent } from '../actions/index.js';
class EventsList extends React.Component {
render() {
var createEventsList = this.props.events.map((event, i) => {
return <li key={i} onClick={() => this.props.selectEvent(event)}>{event.title}</li>
});
return(
<ul>{createEventsList}</ul>
);
}
}
function mapStateToProps(state) {
return {
events: state.events
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ selectEvent: selectEvent }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(EventsList);
the code for the 'event detail' container:
import React from 'react';
import { connect } from 'react-redux';
import GoogleMap from '../components/google_map.js';
class EventDetail extends React.Component{
render() {
if(!this.props.event) {
return <div>Pick an Event</div>
}
const latcoord = parseFloat(this.props.event.locationLat);
const longcoord = parseFloat(this.props.event.locationLong);
return(
<div>
<div>details for:</div>
<div>{ this.props.event.title }</div>
<div>{ this.props.event.location }</div>
<GoogleMap long={longcoord} lat={latcoord}/>
</div>
);
}
}
function mapStateToProps(state) {
return {
event: state.eventDetail
};
}
export default connect(mapStateToProps)(EventDetail);
and here is the code for the Google Map component:
import React from 'react';
export default class GoogleMap extends React.Component{
componentDidMount() {
new google.maps.Map(this.refs.map, {
zoom: 15,
center: {
lat: this.props.lat,
lng: this.props.long
}
});
}
render() {
return(
<div className="google-map" ref="map"></div>
);
}
}
Since componentDidMount gets called only once when the component is mounted, when you click on an event for the second time, the map is not getting updated. To correct this you will need to implement componentDidUpdate() function. Also you should save the map in a state or in this context. You wouldn't want to create a new map always.
componentDidMount() {
this.map = new google.maps.Map(this.refs.map, {
zoom: 15,
center: {
lat: this.props.lat,
lng: this.props.long
}
});
}
componentDidUpdate() {
this.map.panTo(new google.maps.LatLng(this.props.lat, this.props.lng));
}
Hope it helps. :)

React and Google Maps with google-maps-react, cant get to API's methods

Im using google-maps-react, and it works pretty good, but i just cant understand how can i work with Google Maps API's methods inside my component. Now i need to getBounds of rendered map, but cant find any way to do this. Here is the code, any help would be appreciated.
import React from 'react';
import {Map, InfoWindow, Marker, GoogleApiWrapper} from 'google-maps-react';
const GOOGLE_MAPS_JS_API_KEY='AIzaSyB6whuBhj_notrealkey';
class GoogleMap extends React.Component {
constructor() {
this.state = {
zoom: 13
}
this.onMapClicked = this.onMapClicked.bind(this);
this.test = this.test.bind(this);
}
onMapClicked (props) {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null
})
}
}
test(google) {
// Here i tried a lot of ways to get coords somehow
console.log(google.maps.Map.getBounds())
}
render() {
const {google} = this.props;
if (!this.props.loaded) {
return <div>Loading...</div>
}
return (
<Map className='google-map'
google={google}
onClick={this.onMapClicked}
zoom={this.state.zoom}
onReady={() => this.test(google)}
>
</Map>
);
}
}
export default GoogleApiWrapper({
apiKey: (GOOGLE_MAPS_JS_API_KEY)
})(GoogleMap);
Google Maps Api v 3.30.4
You could try and adapt your requirements to the following example here.
From what i can see a reference is returned using the onReady prop.
For example :
import React from 'react';
import {Map, InfoWindow, Marker, GoogleApiWrapper} from 'google-maps-react';
const GOOGLE_MAPS_JS_API_KEY='AIzaSyB6whuBhj_notrealkey';
class GoogleMap extends React.Component {
constructor() {
this.state = {
zoom: 13
}
this.onMapClicked = this.onMapClicked.bind(this);
this.handleMapMount = this.handleMapMount.bind(this);
}
onMapClicked (props) {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null
})
}
}
handleMapMount(mapProps, map) {
this.map = map;
//log map bounds
console.log(this.map.getBounds());
}
render() {
const {google} = this.props;
if (!this.props.loaded) {
return <div>Loading...</div>
}
return (
<Map className='google-map'
google={google}
onClick={this.onMapClicked}
zoom={this.state.zoom}
onReady={this.handleMapMount}
>
</Map>
);
}
}
export default GoogleApiWrapper({
apiKey: (GOOGLE_MAPS_JS_API_KEY)
})(GoogleMap);

React how to properly call a component function inside render()

I'm building a sidebar menu skeleton using ReactJs and need to understand the way to call a function inside ReactJs render() function.
The code is below:
import React from 'react';
var menuData = require("./data/admin.menu.json");
class SidebarMenu extends React.Component {
constructor(props) {
super(props);
this.state = { expanded: true };
this.buildItem = this.buildItem.bind(this);
};
buildItem(title, ref, icon) {
return (
<div className={"item" + this.props.key}>
<a href={ref}>{title}<i className={"fa " + icon} /></a>
</div>
);
};
render() {
return (
<div>
{
menuData.forEach(function (item) {
this.buildItem(item.title, item.ref, item.icon);
if (item.hasOwnProperty("submenu")) {
item.submenu.forEach(function (subitem) {
this.buildItem(subitem.title, subitem.ref, subitem.icon);
});
}
})
}
</div>
);
};
}
export default SidebarMenu;
The given code shows the following error:
Uncaught TypeError: Cannot read property 'buildItem' of undefined
How to properly call a function that will render data inside the ReactJs function ?
The this referenced when you try to call this.buildItem() refers to the anonymous function's context, not your React component.
By using Arrow Functions instead of functions defined using the function keyword inside the render() method, you can use this to reference the React component and its methods as desired.
Alternatively, you can use (function () { ... }).bind(this) to achieve the same result. But this is more tedious and the use of arrow functions is preferred.
Below is one solution, using fat arrow, AKA arrow functions:
import React from 'react';
var menuData = require("./data/admin.menu.json");
class SidebarMenu extends React.Component {
constructor(props)
{
super(props);
this.state = { expanded: true };
this.buildItem = this.buildItem.bind(this);
};
buildItem(title, ref, icon) {
return (
<div className={"item" + this.props.key}>
<a href={ref}>{title}<i className={"fa " + item.icon}/></a>
</div>
);
};
render() {
return (
<div>
{
menuData.forEach(item => {
this.buildItem(item.title, item.ref, item.icon);
if (item.hasOwnProperty("submenu"))
{
item.submenu.forEach(subitem => {
this.buildItem(subitem.title, subitem.ref, subitem.icon);
});
}
})
}
</div>
);
};
}
export default SidebarMenu;
Another solution would be:
render() {
return (
<div>
{
menuData.forEach(function (item) {
this.buildItem(item.title, item.ref, item.icon);
if (item.hasOwnProperty("submenu"))
{
item.submenu.forEach(function (subitem) {
this.buildItem(subitem.title, subitem.ref, subitem.icon);
}.bind(this));
}
}.bind(this))
}
</div>
);
};
}
But, IMO, the best solution would be to refactor the code using a component:
import React, {PropTypes, Component} from 'react';
const menuData = require('./data/admin.menu.json');
function MenuItem({key, ref, title, icon, submenu}) {
return (
<div className={`item${key}`}>
<a href={ref}>{title}<i className={`fa ${icon}`}/></a>
if (submenu) {
submenu.map((subitem) => <MenuItem {...subitem} />)
}
</div>
);
}
MenuItem.propTypes = {
key: PropTypes.string,
title: PropTypes.string,
ref: PropTypes.string,
icon: PropTypes.string,
submenu: PropTypes.array,
};
class SidebarMenu extends Component {
constructor(props) {
super(props);
this.state = {
expanded: true,
};
}
render() {
return (
<div>
{
menuData.map((subitem) => <MenuItem {...subitem} />)
}
</div>
);
}
}
export default SidebarMenu;
You can add this line:
render() {
let that = this
return (
and then instead of this.buildItem use that.buildItem or you may need that.buildItem.bind(that)

setState in componentDidMount() not working in geolocation callback

I'm new to react-native and I'm trying to use geolocation to update the state.
In componentDidMount() I can call this.setState({lat: 1}); and it works. However if I call this.setState({lat: 1}); from within the geolocation.getCurrentPosition() callback the app crashes. I'm testing on a physical device.
I have stripped back everything to isolate the problem. Any help is greatly appreciated.
Here is my code:
import React, { Component } from 'react';
import { AppRegistry,ScrollView, ListView, Text, View } from 'react-native';
class TestProject extends Component {
constructor(props) {
super(props);
this.state = {
lat: 0
};
}
componentDidMount() {
navigator.geolocation.getCurrentPosition(function(position) {
this.setState({lat: 1}); // this causes a crash - without this line the app does not crash.
},
(error) => alert(error.message)
);
this.setState({lat: 1}); // this works
}
render() {
return (
<View>
<Text>
{this.state.lat}
</Text>
</View>
);
}
}
AppRegistry.registerComponent('TestProject', () => TestProject);
Thanks to #Phil I just had to set self = this:
let self = this;
navigator.geolocation.getCurrentPosition(function(position) { self.setState({ lat: 1 }); }, (error) => alert(error.message) );

Why is the Meteor collection not populating on the first render()?

I am a bit confused as to how to get this to work. I have three components - ReportsWrapper, ReportsNearby, and ReportSingle:
1.) ReportsWrapper gets the device location data then calls ReportsNearby passing the location as a prop
2.) ReportsNearby then uses that location to populate a "reports" collection from the mongodb
3.) Each record from "reports" is rendered via a ReportSingle component
Problem is that the "reports" collection does not populate the page at first load. The page loads, no data (data from mongodb) is shown on the client.
If I navigate to a different page (like /about or /settings) then back to ReportsWrapper, the collection seems to populate as expected and a ReportSingle is displayed for each record. Once I refresh on ReportsWrapper the collection is once again empty.
Any ideas?
ReportsWrapper.jsx
import React, {Component} from "react";
import TrackerReact from "meteor/ultimatejs:tracker-react";
import ReportsNearby from "./ReportsNearby.jsx";
export default class ReportsWrapper extends TrackerReact(Component) {
render() {
if (Geolocation.latLng() == null) {
return (
<span>Determining location...</span>
)
}
return (
<div>
<h1>Reports</h1>
<ReportsNearby latLng={Geolocation.latLng()} />
</div>
)
}
}
ReportsNearby.jsx
import React, {Component} from "react";
import TrackerReact from "meteor/ultimatejs:tracker-react";
import ReportSingle from "./ReportSingle.jsx";
import NearbyMap from "../maps/NearbyMap.jsx"
export default class ReportsNearby extends TrackerReact(Component) {
constructor(props) {
super(props);
this.state = {
subscription: {
reports: Meteor.subscribe("nearbyReports", 5, props.latLng)
}
}
}
componentWillUnmount() {
this.state.subscription.reports.stop();
}
_reports() {
return Reports.find().fetch();
}
render() {
console.log(this._reports()); // this is empty - why???
return (
<div>
<ul>
{this._reports().map((report, index)=> {
return <ReportSingle key={report._id}
report={report}
})}
</ul>
</div>
)
}
}
ReportSingle.jsx
import React, {Component} from 'react';
export default class ReportSingle extends Component {
render() {
return (
<li>
<a href={`/report/${this.props.report._id}`}>
{this.props.report.category}<br/>
{this.props.report.description}<br/>
{this.props.report.status}
</a>
</li>
)
}
}
routes.jsx
import React from "react";
import {mount} from "react-mounter";
import {MainLayout} from "./layouts/MainLayout.jsx";
import ReportsWrapper from "./reports/ReportsWrapper.jsx";
import Settings from "./Settings.jsx";
import About from "./About.jsx";
FlowRouter.route('/', {
action() {
mount(MainLayout, {
content: <ReportsWrapper />
})
}
});
FlowRouter.route('/settings', {
action() {
mount(MainLayout, {
content: <Settings />
})
}
});
FlowRouter.route('/about', {
action() {
mount(MainLayout, {
content: <About />
})
}
});
publish.jsx
Meteor.publish("nearbyReports", function (limit, latLng) {
Reports._ensureIndex({'lngLat': '2dsphere'});
return Reports.find({
lngLat: {
$near: {
$geometry: {
type: "Point",
coordinates: [latLng.lng, latLng.lat]
},
$minDistance: 0,
$maxDistance: 3218.69 // this is 2 miles in meters
}
}
}, {
sort: {createdAt: -1},
limit: limit,
skip: 0
});
});
I ended up getting this working by adding the following code to ReportsWrapper.jsx. I think that ReportsNearby was not getting the latLng prop as expected, even though I had console logged it to verify.
tl;dr Use a timer interval to make sure your app has received the location.
constructor () {
super();
this.state = {
handle: null,
latLng: Geolocation.latLng()
}
}
componentDidMount() {
if (this.state.latLng != null) {
return
}
// Attempt to reload the location if it was not found. do this because the device
// location is not immediately available on app start
this.state.handle = Meteor.setInterval(()=> {
if (Geolocation.latLng() != null) {
this.setState({latLng: Geolocation.latLng()}); // Setting the state will trigger a re-render
Meteor.clearInterval(this.state.handle); // Stop the interval function
}
}, 500)
}
componentWillUnmount() {
// Make sure interval is stopped
Meteor.clearInterval(this.state.handle);
}

Categories