Pointer Location on a Map

Use MapBox With GatsbyJS
Published: 23-Nov-2019

One of the challenges with GatsbyJS is compiling client code that is heavily dependent on window. Gatsby uses node to compile and serve the website. And window is not available on the server.

So, when we attempt to create widgets using cool tools like MapBox, we can find that everything works great in the development space, where, typically there is a client with window. But when we then push that code to be served, we find that the build fails with errors.

I'm not the first person to have solved this problem by any stretch of the imagination. However, I'd like to think that these code snippets I'm sharing here will help you quickly move forward.

I've found the best way to implement something like MapBox in a Gatsby website is to wrap it in Loadable (react-loadable).

And we're using Material-UI to provide the presentation layer in this case.

import PropTypes from 'prop-types';
import React from 'react';
import Load from 'external-load';
import Loadable from 'react-loadable';
import CircularProgress from '@material-ui/core/CircularProgress';
// Children
import Map from './map';
// Utilities
import getWindowIsDefined from '../../../utilities/get-window-is-defined';
const Loading = () => (<div><CircularProgress /></div>);
const LoadableComponent = Loadable({
loader: () => import('./map'),
loading: Loading,
render(loaded, props) {
let Component = loaded.default;
return <Component {...props}/>
}
});
class MapBoxWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
mounted: false,
};
}
componentDidMount() {
Load.css('https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.css')
.then(() => {
this.setState((prevState) => (
{
...prevState,
css: {
...prevState.css,
map: true,
},
}
));
})
.then(() => {
this.setState((prevState) => (
{
...prevState,
mounted: true,
}
));
})
.catch((error) => {
console.log(error);
});
}
render() {
const { businessName, latitude, longitude, zoom } = this.props;
const { mounted } = this.state;
const isWindowDefined = getWindowIsDefined();
if (latitude === -200 || longitude === -200) {
return null;
}
if (!mounted) {
return null;
}
if (isWindowDefined) {
return (
<LoadableComponent
businessName={businessName}
latitude={latitude}
longitude={longitude}
zoom={zoom}
/>
)
}
return null;
}
}
MapBoxWrapper.defaultProps = {
businessName: '',
latitude: -200,
longitude: -200,
zoom: 6,
};
MapBoxWrapper.propTypes = {
businessName: PropTypes.string,
latitude: PropTypes.number,
longitude: PropTypes.number,
zoom: PropTypes.number,
};
export default MapBoxWrapper;

So, let's run through the code.

Load (external-load) is used to load scripts or CSS on ComponentDidMount. MapBox has a dependency whereby CSS is required to help present the map. I like to make sure the map does not load if the CSS cannot be loaded in the first place, because I'd like the map to look its best.

Material-UI includes a bunch of progress widget options. I'm using the commonly used CircularProgress.

You'll notice that this code requires some code from ./map. Dear reader, you will need to wait until next week to see this section of code.

The getWindowIsDefined is a fairly straightforward boolean which checks whether the environment running has access to window using an if statement that returns true or false based on the test: (typeof (window) === 'undefined').

I found that it was important to put CircularProgress between a <div> block (see Line 14) because this helped Loadable determine it to be renderable element. In other words, on Line 18, if you use loading: <CircularProgress />, the code craps itself.

I'm pretty pleased with myself for the code block within Lines 16-23. What this section does is provide the necessary wrapper for node to ignore MapBox with, and, we can also pass props into MapBox even though MapBox is buried within a higher order component. This is also a huge thanks to the developers of Loadable for building such an awesome tool.

So, Line 17 gives loader something to load. In this case the default export from './map'. Line 18 is what to display while the loader is working to render what we actually want.

On Line 19, render is a function that passes loaded and props (yay) to the loaded component. Which is how Loadable can be taught how to pass required props to our Map. Squeee.

Line 25 defines the class MapBoxWrapper, where state is defined using a mounted value of false.

In componentDidMount on Line 34, the Load.css method is called in an attempt to load the external MapBox css library. If it loads, a css with map set to true is added to state in the first then promise.

One that has happened, the next then promise will set mounted in state to true.

If an error occurs, a console.log is written and state is not touched (mounted remains 'false').

In the render block (Line 60), a few props are passed. These are items that we want to render in the Map. In this case, a business name, a latitude and longitude and the zoom of the map.

The latitude and longitude are not set as required props. But, the default props are set at Line 88 to be -200 each. Which aren't valid numbers. So, if you try to use this MapBox, you need to pass lat and long in order to render the map. If you don't, nothing will appear per Line 66.

Similarly, if it wasn't possible to set mounted to true, Line 70 would also return a null and nothing will render in the client for the MapBox.

If isWindowDefined, Line 74 will return the LoadableComponent. It is here we pass the props that we want to pass down to the wrapped MapBox.

And if isWindowDefined is false, on Line 84 a null will be returned.

Hopefully the rest is straight forward.

Stay tuned to find out what the './map' file looks like and thanks for reading!

Image by Mashiro Momo from Pixabay

Pointer Location on a Map

Use MapBox With GatsbyJS
Published: 23-Nov-2019

One of the challenges with GatsbyJS is compiling client code that is heavily dependent on window. Gatsby uses node to compile and serve the website. And window is not available on the server.

So, when we attempt to create widgets using cool tools like MapBox, we can find that everything works great in the development space, where, typically there is a client with window. But when we then push that code to be served, we find that the build fails with errors.

I'm not the first person to have solved this problem by any stretch of the imagination. However, I'd like to think that these code snippets I'm sharing here will help you quickly move forward.

I've found the best way to implement something like MapBox in a Gatsby website is to wrap it in Loadable (react-loadable).

And we're using Material-UI to provide the presentation layer in this case.

import PropTypes from 'prop-types';
import React from 'react';
import Load from 'external-load';
import Loadable from 'react-loadable';
import CircularProgress from '@material-ui/core/CircularProgress';
// Children
import Map from './map';
// Utilities
import getWindowIsDefined from '../../../utilities/get-window-is-defined';
const Loading = () => (<div><CircularProgress /></div>);
const LoadableComponent = Loadable({
loader: () => import('./map'),
loading: Loading,
render(loaded, props) {
let Component = loaded.default;
return <Component {...props}/>
}
});
class MapBoxWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
mounted: false,
};
}
componentDidMount() {
Load.css('https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.css')
.then(() => {
this.setState((prevState) => (
{
...prevState,
css: {
...prevState.css,
map: true,
},
}
));
})
.then(() => {
this.setState((prevState) => (
{
...prevState,
mounted: true,
}
));
})
.catch((error) => {
console.log(error);
});
}
render() {
const { businessName, latitude, longitude, zoom } = this.props;
const { mounted } = this.state;
const isWindowDefined = getWindowIsDefined();
if (latitude === -200 || longitude === -200) {
return null;
}
if (!mounted) {
return null;
}
if (isWindowDefined) {
return (
<LoadableComponent
businessName={businessName}
latitude={latitude}
longitude={longitude}
zoom={zoom}
/>
)
}
return null;
}
}
MapBoxWrapper.defaultProps = {
businessName: '',
latitude: -200,
longitude: -200,
zoom: 6,
};
MapBoxWrapper.propTypes = {
businessName: PropTypes.string,
latitude: PropTypes.number,
longitude: PropTypes.number,
zoom: PropTypes.number,
};
export default MapBoxWrapper;

So, let's run through the code.

Load (external-load) is used to load scripts or CSS on ComponentDidMount. MapBox has a dependency whereby CSS is required to help present the map. I like to make sure the map does not load if the CSS cannot be loaded in the first place, because I'd like the map to look its best.

Material-UI includes a bunch of progress widget options. I'm using the commonly used CircularProgress.

You'll notice that this code requires some code from ./map. Dear reader, you will need to wait until next week to see this section of code.

The getWindowIsDefined is a fairly straightforward boolean which checks whether the environment running has access to window using an if statement that returns true or false based on the test: (typeof (window) === 'undefined').

I found that it was important to put CircularProgress between a <div> block (see Line 14) because this helped Loadable determine it to be renderable element. In other words, on Line 18, if you use loading: <CircularProgress />, the code craps itself.

I'm pretty pleased with myself for the code block within Lines 16-23. What this section does is provide the necessary wrapper for node to ignore MapBox with, and, we can also pass props into MapBox even though MapBox is buried within a higher order component. This is also a huge thanks to the developers of Loadable for building such an awesome tool.

So, Line 17 gives loader something to load. In this case the default export from './map'. Line 18 is what to display while the loader is working to render what we actually want.

On Line 19, render is a function that passes loaded and props (yay) to the loaded component. Which is how Loadable can be taught how to pass required props to our Map. Squeee.

Line 25 defines the class MapBoxWrapper, where state is defined using a mounted value of false.

In componentDidMount on Line 34, the Load.css method is called in an attempt to load the external MapBox css library. If it loads, a css with map set to true is added to state in the first then promise.

One that has happened, the next then promise will set mounted in state to true.

If an error occurs, a console.log is written and state is not touched (mounted remains 'false').

In the render block (Line 60), a few props are passed. These are items that we want to render in the Map. In this case, a business name, a latitude and longitude and the zoom of the map.

The latitude and longitude are not set as required props. But, the default props are set at Line 88 to be -200 each. Which aren't valid numbers. So, if you try to use this MapBox, you need to pass lat and long in order to render the map. If you don't, nothing will appear per Line 66.

Similarly, if it wasn't possible to set mounted to true, Line 70 would also return a null and nothing will render in the client for the MapBox.

If isWindowDefined, Line 74 will return the LoadableComponent. It is here we pass the props that we want to pass down to the wrapped MapBox.

And if isWindowDefined is false, on Line 84 a null will be returned.

Hopefully the rest is straight forward.

Stay tuned to find out what the './map' file looks like and thanks for reading!

Image by Mashiro Momo from Pixabay