Introduction
I built a website where users could easily create their own maps for whatever purpose they see fit. They are able to add markers to any place in the world, share their maps, and collaborate with other users on maps. This article will cover some of the details of building this site. You can visit and use the site here. Or look at a Video Demo. You can also view the front-end code here.
Using Mapbox
Mapbox is an easy to use, powerful map tool for developers. They have a very generous free tier of api calls so I never had to worry about going over their limit, and I found it a little easier to use than google maps api. To start, you just have to go to their website, create an account, and get an api key.
I was using React for this project, so loading in the map was a little different than doing it with vanilla JS/HTML. First you need to install the mapbox api with npm or yarn. I imported the following to get started with mapbox on my map component.
import mapboxgl from 'mapbox-gl'
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
Mapbox needs a div with an id to attach its map to. Therefore, I had to render the map container before I actually rendered the map. That is why I had the renderMap() function in componentDidMount, since it needed the div to be on the html.
class Map extends React.Component {
componentDidMount() {
this.renderMap()
}
render(){
return(
<>
<div className="map-container">
<div id="map"></div>
</div>
</>
)
}
renderMap() {
mapboxgl.accessToken = process.env.REACT_APP_API_KEY;
const map = new mapboxgl.Map({
container: 'map', // container ID
style: 'mapbox://styles/nicklevenson/ckm82ay4haed317r1gmlt32as', // style URL
center: [-77.0353, 38.8895], // starting position [lng, lat]
zoom: 1 // starting zoom
});
map.addControl(
new MapboxGeocoder({
accessToken: process.env.REACT_APP_API_KEY,
mapboxgl: mapboxgl
})
);
this.setState({map: map})
document.querySelectorAll(".mapboxgl-ctrl-geocoder--input")[0].placeholder = "Search for Places"
}
With the code above, we are rendering the mapbox map, as well as their Geocoder api which allows you to search for places and addressed on the map. You can see where I put my API key to have access to mapbox using the dotenv package for development. Once you have that, you can add a lot of other features that mapbox has to offer. For my project I wanted to be able to add markers to the map.
renderMarkers(){
this.props.markers.forEach(marker => RenderMarker({
marker: marker, map: this.state.map,
handleMarkerSelect: this.props.handleMarkerSelect,
destroyMarker: this.props.destroyMarker,
currentUser: this.props.currentUser,
selectedMap: this.props.selectedMap,
handleRemoveMarker: this.handleRemoveMarker,
handleMarkerAdd: this.handleMarkerAdd
}))
}
To start, I wanted to render markers for all the markers coming in from the database (this.props.markers). The object being passed to the RenderMarker() function is simply a few functions that assisted in handling the redux state and database calls. It also gave the marker information about itself - like title, user, the currentuser, etc...
const coords = [props.marker.lng, props.marker.lat];
const el = document.createElement('div');
el.className = 'marker';
el.style.backgroundImage = `url(${props.marker.user.image})`
const marker = new mapboxgl.Marker(el)
.setLngLat(coords)
.setPopup(new mapboxgl.Popup({ offset: 25 }) // add popups
.setHTML(
`<h3>${props.marker.title}</h3>
<i>By: ${props.marker.user.username}</i>
<br>
<i>Coordinates: [${coords}]</i>
<textarea readonly>${props.marker.info}</textarea>
${props.marker.image ? `<image src=${props.marker.image} alt="marker image" class="marker-image"></image> `: `<br>`}
`
))
.addTo(props.map);
In the renderMarker() function, the code above is what actually renders a marker on the map. You have to create a div for the marker on the html. I made the marker to be the user's profile image. Then, I set a popup for the marker. This is mapbox's easy way to make a marker clickable to show more information. All you do is create the popup, then use mapbox's built in function to set the innerHTML of the popup. In this case, I would add the title, username, description, and image. Lastly, you had to append the marker to the map with the .addTo function. The marker would then appear on the map! Once data was flowing from my database api to the redux state, it was easy to render these markers on the maps.
Rails Api
I won't go too deep into this section, but I wanted to show you the schema for the application to get a better sense of how data was being stored and fetched.
I wanted users to have many maps, and for maps to have many users. This way, people could add collaborators to their maps. Therefore, I needed a joins table (user-maps) to create that many-to-many relationship. I wanted users to have many markers, and for markers to belong to a user. Maps should have many markers and markers should have many maps. This many-to-many relationship (marker_maps) allowed me to give users the ability to add other people's markers to their own maps.
create_table "maps", force: :cascade do |t|
t.string "title"
t.string "description"
t.boolean "public", default: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "marker_maps", force: :cascade do |t|
t.integer "marker_id"
t.integer "map_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "markers", force: :cascade do |t|
t.integer "user_id"
t.string "title"
t.string "info"
t.string "image"
t.decimal "lng", precision: 10, scale: 6
t.decimal "lat", precision: 10, scale: 6
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "user_maps", force: :cascade do |t|
t.integer "user_id"
t.integer "map_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "username"
t.string "email"
t.string "uid"
t.string "provider"
t.string "image", default: "https://icon-library.net//images/no-user-image-icon/no-user-image-icon-27.jpg"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
React + Redux
I wanted to use Redux for state management since this app was going to be fairly complicated when it came to that. It would be nice to have a store for my state that I could access from any component, rather than pass a bunch of props down from components. I also knew I would be making many fetch requests to the backend so I used the middleware Thunk to make those requests work well with Redux. It basically allowed me to make async calls and update the Redux store when it got data, so the app didn't have to constantly wait for database to respond. I set this up in my index.js file like so:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter as Router} from 'react-router-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers/rootReducer.js'
import {composeWithDevTools} from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import 'semantic-ui-css/semantic.min.css'
import 'mapbox-gl/dist/mapbox-gl.css'
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
reportWebVitals();
The rootReducer is a function that combines my reducers into one, and that is being connected to the redux store with the store variable. That variable gets passed to the provider component which connects my app with the redux store as well as dispatch actions.
Here's an example of an action in my application.
export const addMaps = (maps) => ({type: "ADD_MAPS", payload: maps})
export const fetchMaps = () => {
return (dispatch) => {
fetch(`${process.env.REACT_APP_BACKEND_URL}/maps`)
.then(res => res.json())
.then(maps => {
dispatch(addMaps(maps))
})
.catch(function(error) {
alert("Errors getting maps.")
})
}
}
Basically, I am fetching maps from my database and then dispatching them to the store so redux has access to all the maps. This way, I can connect any component to my redux store and access those maps from the database. My application had many more actions like this, including actions to create, edit, and delete maps. You could see how this could get really complicated using React only, but Redux makes it so much easier to contain these complicated actions and data relationships into one place. It allowed me to connect a component to the store and dispatch actions. For example, once I mounted my map component, I could then make the call to fetch its markers so it happens in the background and the user isn't left with a boring loading sign.
Conclusion
This application was complicated to build and I only scratched the surface in this article. This project made me appreciate the functionality that Redux and Thunk brings to a React app. It also was really cool to use the Mapbox api - there are so many different routes to take with it. I hope this article shed some light on how to use Mapbox, as well as show why Redux is useful. Please ask questions in the comments and I hope you check out the project!
Top comments (0)