Airbnb Map Clone Tutorial – React with Ruby on Rails

Honestly, I’ve always wanted to create something like the Airbnb map. It looks really great, when you drag and drop on it, it reloads the data and shows all available apartments on the map with prices. It’s a great example of using a front-end framework to build a map and creating a separate back-end app which returns only data passed to the front-end. In this tutorial, I’ll show how to create a map like the Airbnb map.

So once again, to sum it up – which features does it have? Well, when it initializes, it fetches all available apartments which are on a map from the back-end. Also, when you change the location of the map, like dragging, dropping, or zooming it, all places are reloaded from the back-end. Moreover, when you click on a marker on the map, it shows basic info about an apartment. It looks like:

Yeah, we will create the map which looks like the map above, with the same features. We won’t focus on the design, we will keep it simple.

We will build two applications, one will be responsible for the front-end part – React app. The second application – our API, back-end, will be written in Rails 5 API.

Everything will be connected together, so the front-end application will fetch data from the back-end application. It’s much easier to work on to different applications, everything is separated. Later there is not one huge application which you need to maintain. Back-end and front-end are independent, which is really great. You don’t need to care about their dependencies or install front-end libraries via bundler or rails-assets.

Back-end

We will separate our application to a back-end and front-end. The back-end will be responsible for providing data via API to the front-end, that’s all – nothing more, only one responsibility 🙂

One more important thing about the code snippets – when you notice the three dots (…), it means that there is a code from the previous snippet, which you don’t need to change, I’m not adding it to make a snippet clear.

Let’s generate new Rails 5 API application without views, we just need JSONs:

What do we need?

The first thing – do we know what we need? We need only a simple model which keeps data connected to a flat/apartment like on Airbnb, so price, home name, description and of course longitude and longitude (we need to visualize them on a map). Ok, so let’s create a model called a “place”.

We have a model which is not complete yet. We also need routes and a controller! Let’s start with routes. We just need an index action, nothing more. Oh wait, we also need an api namespace inside our routes:

Rails.application.routes.draw do
scope :api do
resources :places, only: [:index]
end
end

The search method

Now let’s update our model, what we need here is a method which gets data when passed longitude and longitude. We won’t search by a place name or something like this. Google Maps provides in their API something like minimum and maximum longitude and latitude:

The search method returns all places between passed params, so between min_lat, max_lat, min_lng and max_lng.

We limit returned results to 100. Why do we limit them? Well, imagine a lot of records on a small piece of a map, it would be really unreadable. It’s not about performance, we can add indexes to these fields, cache them somehow or even use a tool designed and built for really efficient search engines, like Solr or ElasticSearch.

When we zoom the map, we will render all records located near a center point. Check out how it works on Airbnb, they use an algorithm to filter their apartments on a page load, later, when you zoom the map, they render the rest.

Now let’s add a controller (places_controller.rb) which will return a JSON file based on passed params:

Nothing complicated here, we use strong params to pass only these, which we really want to pass to the search method. Remember to use the symbolize_keys method in order to pass a formatted hash, like { a: 1, b: 2 }, not { “a” => 1, “b” => 2 } – with the second hash our method will have problems reading it.

Ok, we have everything ready but were missing data! Yeah, we need to return something to the front-end! Let’s add some records using Rails’ seeds. To generate random names we gonna use the faker gem. Also, all places will be created around one main point, +-5 degrees.

Running Puma

Great, our back-end is ready. One more thing, please use Rails server on a different port than 3000, we will use it with React. You can use for example 4000 port. Remember, that we use Puma, so you need to add a PORT variable before the rails server command. Using the -p attribute in the rails server command won’t work.

Front-end

Let’s start with installing the create-react-app tool which creates a scaffold app for us, it already includes React and the folder structure:

$ npm install -g create-react-app

Ok, now generate a new, fresh app:

$ create-react-app airbnb_map_front-end
$ cd airbnb_map_front-end

You can start it by running:

$ npm start

A new browser tab/window should be opened. It’s auto-reloading our app, so there is no need to rerun the server – only when you change something in the package.json file, I recommend you rerun it to reload everything once again.

First component

Let’s make some small cleanups to our app. We will remove unneeded code to make the start files thinner. Start with cleaning the App.js:

Google Map library

Our first component is ready, let’s modify it and display an empty Google Map.

First of all, we need to install the Google Maps library inside our project. How can we do it? Using npm, it’s called react-google-maps. One important thing here, please go with 7.3.0 version, in the newest one I had some problems with the custom marker.

$ npm install --save react-google-maps@7.3.0

Great, we have already installed the library with customs components. Now, we need to include the official Google Maps source code.

Awesome! As I mentioned in the beginning, we want to display a simple Google Map. Let’s do it! I’ll be using ES6 syntax, I hope that you’re familiar with it, if not, please check this site.

First of all, we need to include the library in our component. Then, we need to render it in the render method. We should define a center point (lng, lat) and a map zoom. There is the one really important thing – each time we update a component’s state, our map will be rerendered. We don’t want to re-render the map, only components on it. How can we achieve it? By defining a GoogleMap component (from the react-google-maps library) outside the Map component, wrapped in the withGoogleMap method provided by the library.

As you can see, we set a state with a default lng and lat. We don’t need to do it with the zoom – we won’t change it, it’s only used for the initialization process. Yeah, as I mentioned before, we return in the render method the Airbnb map instance, defined outside the Map component.

Great, please visit your browser, you should be able to see an empty map!

Place Marker

We have our map, now let’s display something real, our first marker! Let’s add the PlaceMarker component. It’ll render only a simple Marker based on passed lng and lat via props:

If you’ve visited Airbnb or read this tutorial from the beginning, you probably know that when you click on one of their map’s markers it shows a window with an apartment’s info, like price, description, and name. Let’s add this feature now. When you click on a marker, we gonna open or close an info window.

Add in the PlaceInfoWindow component. It will display a basic info, nothing special. Also when you will click on the “x”, it will be closed.

Now we need to modify the PlaceMarker component a little bit. First of all, we need to include the PlaceWindowComponent, then initialize via state, if a window is opened or closed. Also, implement methods which will be responsible for opening or closing the info window and render it when it should be visible.

Now, when you click on a marker, an info window should appear! You can also close it by clicking on it or on the ‘x’.

Zoom, drag and drop the map and save its coordinates

Ok, now let’s focus on drag and drop and map zoom. As I told at the beginning, when you drag and drop or zoom the Airbnb map, it gets data from the back-end and rerender markers on a map based on a lng and lat.

How can we do it? First of all, we need to get LngMin, LngMax, LatMin, LatMax and a map center point. Google Maps API allows us to get it by calling these methods:

Center point: map.getCenter().lat() / map.getCenter().lng()

Boundaries: map.getBounds().b – x axis / map.getBounds().f – y axis

When should we do it? Well, there is one main problem. Our code runs faster than a map fully loads (map image), so unless it loads, we can’t run the getBounds() method – we don’t know boundaries yet!

How can we solve it? Google Maps API has a listener called onBoundsChanged. All possible actions, listeners and functions can be found in the official documentation.

Let’s go back to the onBoundsChanged function – when a map’s boundaries are changed, this function is called. So when the map fully loads for the first time, we can mark that the map has fully loaded and set boundaries and a center point. You should know one thing, every time when you move your map and even if a location is changed by one minute, this function is called. So we should prevent it from calling this function all the time and call it only when you move our map. We can set a flag which will notify that a map has fully loaded and that we don’t want to call this function.

We also need two other functions – onZoomChanged and onDragEnd.

onZoomChanged triggers an event when you click on ‘+’ or ‘-’ button on the map.

onDragEnd triggers an event when you drag and drop the map but ends the moment you stop dragging it.

Ok, so what do we want to achieve?

When a map fully loads, mark that the map has loaded and store a map object somewhere in our class.

When the map’s boundaries are changed (by zoom or drag and drop) store the center point and boundaries in our class so that we can make an API call with the coordinates.

What have we added here? Let’s start from the Map class, we will come back later to the AirbnbMap class. In the constructor in the state we added an array called places, which will store all apartments/places/flats visible in a map area.

We added xMapBounds and yMapBounds params which stores the map boundaries coordinates. Also, there is a mapFullyLoaded flag, which informs if a map has fully loaded.

handleMapChanged function is called when the map boundaries have been changed and it calls three different functions:

getMapBounds – the function which gets and sets map boundaries

setMapCenterPoint – the function which gets and sets the center point

fetchPlacesFromApi – the function which every time, for now, creates a one marker

We also added the function called handleMapMounted – it assigns to the this.map value a map object during the map initialization.

The handleMapFullyLoaded function marks that the maps have fully loaded and calls the handleMapChanged function to get the latest data.

There is also a list rendered on the front end, which keeps all assigned coordinates in all variables, to show that when you change a map’s area, everything is updated.

We also need to pass these functions to the AirbnbMap class (handleMapMounted, handleMapChanged, handleMapFullyLoaded).

Let’s go back to the AirbnbMap. As you can see we pass the onMapMounted function as ref in order to save a map object somewhere in our class. Also, we call onZoomChanged, onDragEnd and onBoundsChanged functions from the Google Maps API to run passed functions via props to the AirbnbMap class from the Map class.

Inside the AirbnbMap, we render all passed places as markers on the map, when they exist.

If you did everything correctly, you should see something like:

Fetch data from the API

Everything is almost done but still, we don’t have real data rendered on our map. Let’s finally integrate our back-end with the front-end!

First of all, in order to make possible Xhr calls between front-end and back-end add to the package.json one line which allows accessing back-end:

"proxy": "http://localhost:4000"

Please restart your npm server to reload the config file!

We point our React app to the back-end endpoint. Remember to run the back-end on the same endpoint you added in the package.json file!

Ok, now let’s change the fetchPlacesFromApi function to get data from our back-end:

As you can see, we make a simple HTTP GET call with all needed params to get a JSON. Then we convert a response to a JSON and set rendered data as places. At the beginning, I recommend cleaning our array, as I do before the HTTP call.

Moreover, we need to add more params which are passed to the PlaceMarker component in the AirbnbMap – the most important is unique key!

We need to also add new keys in the PlaceMarker and pass to the PlaceInfowIndow component. Let’s do it:

We need to also add extra keys which we pass to the Marker component in the PlaceMarker. We must specify marker class, inform it that we want to display a custom marker, hide the default one (set capacity to 0) and add the marker’s content.

Related Posts

As you've probably guessed by the title of my article, I still consider Ruby on Rails as a relevant technology that offers a lot of value, especially when combined with ReactJS as it's frontend counterpart. Here's how I approach the topic.

Gitlab Pipeline for Rails is the main part of a powerful GitLab CI/CD tool and can be a useful alternative for other applications like Jenkins and TeamCity. If you’re looking for some more detailed information on exactly how it works, we’ve compiled an example configuration that can help you.