photo-1455636820250-908db9925403-1

appendTo is based out of Colorado, a US state famous for the quantity and size of its mountains. 53 mountains over 14,000 feet (4267.2 meters) are located in Colorado’s borders. During the summer, many Colorado residents make it a mission to climb as many of these ’14ers’ as they can.

In this React and Redux tutorial, we will be creating an interactive map application of Colorado’s 14ers using React and Redux. No previous knowledge of Redux is required, but a basic understanding of React concepts such as the state and components will be very helpful.

Here’s a demo of the app that we’re going to build (for the sake of not making this React tutorial longer, we did not make this responsive yet)

react tutorial demo app final

Here’s an outline of what we will be doing in this React tutorial:

  1. Redux Concepts
  2. Setting Up the Project
    1. Installing Third-party Packages
  3. Accessing the App
  4. Building the App
    1. Actions
    2. Reducers
      1. currentSelectionsReducer
      2. searchReducer
      3. index
    3. Components
      1. Main Component
      2. VisibleMarkers Container
      3. VisibleCards Container
      4. MarkerList Component
      5. CardList Component
      6. SearchFilters Component
    4. Helpers

The final code is available on this Github repository

Redux Concepts

Many developers have found Redux harder to grasp than React. Before we build out the React components, we’re going to cover how Redux will be used in this application. Here are the 4 primary Redux concepts that this demo app will use:

  • State – The state is data that needs to be updated during the life cycle of a component. It contains information on the current state of the component. For example, if you have a switch that is toggled on, you might store something like switch_value in the state and then assign it a value of true.
  • Store – The store is a big JavaScript object that represents the state of the entire app. It is where you save, update, and retrieve the data that needs to be accessible throughout the whole app.
  • Actions – The specific actions in your app which allow you to update the store. For example, you can have a SWITCH_ON action which turns on a <Lightbulb> component.
  • Reducers – Reducers specify how the state may change, given a specific action. These accept the data provided by the action and use it to update the store.

To bring all these concepts together, let’s explain using a metaphor with real-world objects. This will be a simplified explanation of Redux.

Imagine you have a <Switch> component and a <Lightbulb> component. They’re independent of each other but they’re both used in your app. The <Lightbulb> component will not be aware of the changes in the state of the <Switch> component. They are separate and their state won’t affect each other. So even if you change the switch_value to true within your <Switch> component, the <Lightbulb> component won’t actually turn on. We need a way for these separate components to be able to draw from the same state. This is where Redux and ‘state management’ come in. Redux gives us a ‘store’ that can be accessed throughout the whole app. The Redux store is the single source of truth of the current switch_value.

We now have a store to serve our single source of state/data/truth. We need a way to update the store and update it in such a way that all the relevant components will be notified of the change. Those components can then update their own state and re-render. This is where Actions and Reducers come in. Actions are used to create a payload of information to update the store. Reducers are the part of Redux who receive this data. Reducers specify how the store will be updated based on the action type (switch_value) and the payload.

Redux provides the functionality to create the store, dispatch actions and subscribe to changes in the store. This is how the different components in the app get updated.

To better understand how Redux works, check out the following diagram:

redux diagram

In our demo app, a user can hover over a mountain card. Everything starts from a specific component. In this case, it’s a card component which displays the details of a mountain. When the user hovers over a card, we use React’s event system to listen for the mouseEnter event. When this event is triggered, an action (plain-old JavaScript object) containing the type and details of the action is dispatched. In this case, the only detail that we need to submit is the id of the card being hovered over.

component to action

This action goes to the store and the store executes the corresponding reducer based on the type that was specified in the action. The reducer’s job is to determine and return the new state of the store based on the details of the action. Here we can see that the reducer is fetching additional details based on the id and passes this new data back to the store. The store is then updated with this new data under the currently_hovered_card object.

action to store

Next, the store provides the default state and methods that allows the components to subscribe to changes in the store and dispatch actions that can be used to update it. This is made possible by the Provider component that comes with Redux. When the store is updated, we can execute a code that will allow us to update the state of the component involved. As you already know, React automatically re-renders the whole component if the state changes so updating the state updates the UI as well.

store to provider

Later on, you’ll also see that there are two kinds of components: containers (smart components) and plain components (dumb components). The provider component passes down the state to the containers, and the components handle the rendering of the UI.

provider to components

That’s the bird’s eye view of how Redux works, as we go through this tutorial, you’ll learn all about the details of how each of the piece is implemented.

Setting Up the Project

To quickly get started with setting up the project, we’ll be using Create React App. This allows us to install and setup all the tools required in creating a React project.

The first thing that you need to do is to install the package globally:

Once that’s installed, you can now create a new project using the create-react-app command:

Enter the newly created folder and start the development server:

The root directory of the project contains the following files and folders:

  • node_modules – contains the project dependencies. Most of the time you won’t really need to touch this directory.
  • public – contains the files that are served by the server.
  • src – contains the source files of the project. The different components, reducers, and other React files are stored here. This is where we will be primarily working on, so anytime you see a file path, assume that this is the root directory.
  • package.json – contains information regarding the installed packages and the scripts that you can execute inside the project. Here are the one’s included by create-react-app:

react is the main react library, while react-dom is the one that allows you to attach react components into the DOM.

Installing Third-party Packages

Aside from the one’s installed by create-react-app, we’re also going to need the following:

picnic is used to beautify things. This is pretty much like bootstrap but more lightweight.

react-google-maps is used for rendering a Google map.

react-input-range is used for creating slider components that will be used for the filter controls.

redux is used for storing and managing state used by the whole app.

react-redux is used for easily working with Redux within a React app. Redux is a framework agnostic library for working with state. This means that it’s not something that’s exclusive to React. That’s why we need react-redux to provide the functions necessary for us to work with Redux inside the React environment.

Accessing the App

You can access the app at the following url: http://localhost:3000. By default it should show the default page:

create-react-app default page

Every time you make a change to any of the files inside the src directory, it will trigger a recompilation of the source files and a page refresh. This is nice because you can easily see your output whenever you make a change. Create-react-app also comes with ESlint and an error reporter so you can easily see the errors and warnings in your code.

Adding the Project Assets

So that you’ll have some content to work with, first create a photos folder inside the public directory, then download the Github repo of this project. Copy the contents of the public/photos directory of your download to the public/photos directory of the project.

While still inside the public directory, create a css folder and inside create a style.css file then add the following code:

Next, inside the project root, create a data folder and inside create a filters.js and mountains.js file. The contents for these files are available on the Github repo. Use the following links to view the contents of the files:

  • filters.js – The default data for the query and filters.
  • mountains.js – The mountains data which we will be working on.

Open the index.html file and right before the closing <head> tag, link to the CSS file that you’ve just created. While you’re at it, also change the value of the <title> tag and link to the Google Maps Script. Don’t forget to replace GOOGLE_API_KEY with your actual Google Maps API key. If you don’t have one, you can get it from Google Console.

Building the App

Navigate inside the src directory and open the index.js file. This file is responsible for rendering what you saw on the browser earlier. Delete all of its contents and add the following:

Lines 1 and 2 should already make sense to you, but in case not, you need to import react every time you make use of jsx on the current file and react-dom to render it on the DOM.

On line 5, we’re importing the Provider component from Redux. This is a root-level component which allows us to attach the store on the component. This is the main thing that allows us to have access to the store throughout the entire app. This works by having it passed down to children components by means of the context. Later on, you’ll see how to access the store from a child component, and how to use it to dispatch and subscribe to actions that allows us to update the UI for certain parts of the app.

On lines 6 to 8, we’re importing the createStore method from Redux, and the reducers, to create a store.

On lines 12 to 18, we’re rendering the main component inside the div with the id of root.

Actions

The actions file (actions/index.js) serves as the repository for all the actions that can be performed throughout the app. Actions are plain JavaScript objects which describes what happened in the app. For example, in a grocery list app, you might have the following action when the user clicks on the “Add Item” button:

When the user has put the item into their basket, we can have another action:

Basically anything that can happen inside your app can have its corresponding action. As you have noticed, actions always have a type property. This is a required property as it describes what a specific action is doing. This is how the Reducers know what to do when it receives a specific action.

The functions in this file will be called from the different components later on to perform updates to the store. It returns an object containing the data passed in as arguments, together with the type of action. The reducers that we will create later are the one’s who will utilize the data returned by the actions. This is why the number of functions in the actions file should be equal to the number of reducers that we will create later on.

We can easily omit this file and simply supply the type every time we dispatch an action, but we would be violating the programming principle that there should only be a single source of truth. Having all the actions in a single file would give a general idea to new project members on how the state is updated and what parameters are required to update it.

Putting all the actions in a single file would work for a small app like this, but it could easily become convoluted once you work on a fairly complex app. Remember to split up related actions into different files if your actions file is already trying to do too much.

Reducers

In Redux, Reducers are responsible for specifying how the store changes when a specific action is received. They’re the ones who utilize the data passed by the actions in order to update the store. They represent a separate state that is utilized in the entire app. All reducers are invoked with two parameters: the state and the action. Every time an action is dispatched, the object returned by the action is passed as the value for the action parameter. The value passed to the state parameter is the currently stored state. If you supply a default value to the state argument, it will only be used as the default value of the state the first time the app runs.

Every time an action is dispatched, all reducers are invoked. That is why we need to check for the action type then return the new state based on the action payload. Otherwise, it just returns the current state. Even though all the reducers are invoked, it’s important to note that only the reducer which handles that specific action will do the work.

currentSelectionsReducer

currentSelectionsReducer returns the state which stores the data for the currently hovered mountain.

searchReducer

searchReducer returns the state which stores the data for the user’s query and filters. This uses the data from the data/filters.js file as its initial data.

index

To bring all the reducers together, we create an index.js file inside the reducers folder. Here we use the combineReducers() function provided by Redux to bring all the reducers together. We need to do this because we want to be able to access the current store value by using only a single object. Once we do this, the current selections data can be accessed by calling this.context.store.getState().currentSelections and the search results data can be accessed by calling this.context.store.getState().search. This can be done in any component which has access to the context. Later on, you’ll see how to specify that a component should use the context.

When creating reducers, you should always remember two things:

  1. Reducers should be pure functions. This means that they shouldn’t modify the state directly. As you have seen from the two reducers that we’ve created, they either return an entirely new object which represents the current state or returns the default state. If the state is an object, you can either use Object.assign() or return a new object altogether. If the state is an array, you want to use Array.concat() instead of Array.push() if you want to push a new item into the array. This is because Array.concat() creates a new array with the new item that you want to push, while Array.push() modifies the array that you supplied. If you want to know more about pure functions, check out this article from Eric Elliott: Master the JavaScript Interview: What is a Pure Function.

  2. Filtering and sorting items in the state is not the job of the reducer. For example, on a todo list app, you don’t want to create a reducer named filterItemsReducer which will modify the state based on the item status (active, done, or deleted) selected by the user. Because if the user selects done, the state will only have the items that are already crossed off the list. So if the user selects active after that, it’s no longer all of the items that will be filtered, it will only be the items that have a status of done, so you basically end up with an empty array. Later on, we’ll have a helper function that will do that job for us right before we render the items in the state.

Components

We now proceed with the different components of the app. In this section, you’ll learn how the data flows between the components of the app through the use of dispatching and subscribing on the store.

Main Component

Let’s start by creating the main component. You can do that by creating an App.js file and adding the following:

The code above is pretty self-explanatory but I’d like to address one thing, and that is the difference between components and containers. As you can see above, we’re both rendering <VisibleMarkers> and <SearchFilters> but why is <VisibleMarkers> inside the containers directory while <SearchFilters> is inside the components directory? This is because it allows us to have a distinction between components that have their data handed out to them (components) and those that are providing data (containers). If you scour the internet, you will see people often call them Smart or Dumb components. Later on, you’ll see the difference between the two. But if you want to learn more about this, you can read this article on Presentational and Container components.

VisibleMarkers Container

Create the containers/VisibleMarkers.js file and add the following:

In the code above, we’re creating a container called VisibleMarkers. As mentioned earlier, containers are components which fetch their own data. That data is supplied from the store. We are able to access the store from this file by means of the mapStateToProps() function. This function is where we return an object containing the data that we want to pass as props to the MarkerList component. In the above code, we’re returning an object containing mountains as the property. Its value is an array containing the filtered mountain list which is provided to us by the getVisibleMountains() function. This means that in the MarkerList component we will have a props called mountains. If it were a class-based component, you could access it by using this.props.mountains. If it were a functional component, you could access it using props.mountains.

But you may ask how is the state being passed to the mapStateToProps() function? Well, that’s where the connect() function provided by react-redux comes into play. Its the one responsible for passing the current state to mapStateToProps() function. Behind the scenes, the connect() function generates a wrapper component which subscribes to the store. So every time the store is updated with the relevant data, the new state is passed as an argument to the mapStateToProps() function.

VisibleCards Container

VisibleCards container (containers/VisibleCards.js) is pretty much the same as VisibleMarkers container, but this time it uses the CardList component to render the content.

MarkerList Component

Next is the MarkerList Component. This component is responsible for rendering the map as well as the markers for the currently displayed results.

MarkerList

Create a components/MarkerList.js file and import React and the components made available by react-google-maps:

Create the MarkerList component and declare the default state inside the constructor:

Before the component is mounted, subscribe for changes in the store. The current state can be fetched by calling the getState() function in the store. This returns an object containing the global app state. Here’s what the object looks like by default:

currentSelections is the only one we need, so we extract it from the result of the getState() call and then use it to update the state of the component.

toggleInfoWindow controls the visibility of the InfoWindow. For those not aware, this is the little pop-over box which becomes visible whenever you click on a marker inside Google maps. This function gets executed when a marker on the map or the close button on the InfoWindow gets clicked. If the loc parameter is null, it means that the user has closed the InfoWindow. Otherwise, it means that the user clicked on the marker so we supply the necessary information by updating the state.

In the render() function, use the <GoogleMapLoader> component to initialize Google maps. This requires a couple of props to be passed in: containerElement and googleMapElement. containerElement, just like the name suggests, requires you to pass the HTML that will serve as the container for the Google Map.

On the other hand, googleMapElement requires the <GoogleMap> component to be passed in. It also has a couple of props: defaultZoom, which is the default zoom of the map and defaultCenter, which is the position of the center of the map.

Inside the <GoogleMap> component, render the markers by looping through the array of mountains. The <Marker> component accepts the position and the key as its props. There’s also an optional onClick props which is the function to call when the marker is clicked.

Right after the loop, render an InfoWindow based on the current state. In the code below, the InfoWindow will only be rendered if showInfoWindow is true. It also accepts a position as its props. This is the same position as the markers, that is why we need to pass in an options props to adjust it accordingly. This is through the use of pixelOffset. Calling new window.google.maps.Size(0,-30) returns that position which is 30 pixels less on the Y axis. This makes it appear at the top of the marker. Note that we used window to access the Google Maps library because google is not available in the current scope.

Lastly, specify the contextTypes. This tells React that we’re expecting a store object in the context. In React, the context is a set of attributes implicitly passed down from its parents to its children. In this case, the store is basically the store that we created earlier on the index.js file and was passed as a props to the <Provider> component. So when we called this.context.store.subscribe() earlier, we’re basically referring to that same store.

CardList Component

Next is the CardList component (components/CardList.js).

CardList Component

The CardList component uses the Card component (components/Card.js) to render the mountain details. It’s also responsible for adding the event handler for when the cards are hovered over. Start by importing the files that we need. Here we’re extracting the updateSelection() method from the actions/index.js file. We didn’t need to specify /index after the folder name because it’s already assumed that we’re referring to an index file if it’s omitted. As you have seen earlier, this file houses all the actions that can be performed throughout the app.

Create the Card component and add a showInfoWindow() function. This function is executed every time the user hovers over a card. It’s responsible for dispatching the action which updates the state for user selection. This state stores the data for the currently hovered card. That’s why the function expects the general mountain details stored in the properties parameter, as well as the position (coordinates) of the marker in the map. We then use this data to dispatch the updateSelection action. In Redux, the way we dispatch an action is by calling the dispatch() method from the store. The store then invokes the reducer functions. Earlier in the reducers/index.js file, we used the combineReducers() functions to create one big object that houses all the state that we need to keep track of. This means that all reducers that we supplied to that function will get invoked. That’s why we were checking the action type on each reducer so that only the reducer which knows what to do with the action will be executed. The store passes in two arguments to the reducer: the current state and the actual action object. You can just imagine that the store executes a code similar to the following every time an action gets dispatched:

Once the store is updated, it will notify all the subscribers that the store has been updated, which then allows them to fetch the updated data using the getState() function. We did this earlier inside the componentWillMount() function of the MarkerList component. This allowed the component to show the InfoWindow on top of the target marker.

We can’t really leave the InfoWindow hanging around when the user is no longer hovering over a card, that’s why we need the hideInfoWindow() function to dispatch an action which will tell the MarkerList component to hide the InfoWindow.

The render() method displays all the relevant details from the data source as well as attach the functions for showing and hiding the InfoWindow on MouseEnter and MouseLeave events.

SearchFilters Component

We now need to implement the SearchFilters component (components/SearchFilters.js).

SearchFilters Component

This is responsible for displaying the search field and all the filters used in trimming down the search results. Inside the file, first, import the slider and its default styling. We’ll be using the react-input-range package to easily create sliders that the user can use to select the elevation, prominence and isolation of the mountain they’re looking for.

Import the initial filters. This is called filters but it includes the query as well.

Extract the updateQuery() method from the actions file. This allows us to update the current user query.

Create the SearchFilters component. Inside the constructor() set the initial state to that of the data contained within the data/filters.js file.

The updateStore() method gets executed every time the user types in something in the search field or when the sliders are adjusted. This is where we dispatch the updateQuery action which we created earlier on the actions/index.js file. As you have seen earlier, the action accepts the current value of the query text field, the checkbox for showing or hiding the filters, and the selected filters.

The onQueryChange() function updates the component state with the current value inputted by the user in the search field, then calls the updateStore() function to update the store. This will trigger the mapStateToProps() function in the VisibleMarkers and VisibleCards component to be called, which in turn calls the getVisibleMountains() function to return an array containing only the mountains which match the user’s query.

toggleFilters() simply flips the current value for the show_filters in the state. This controls whether the filters are hidden or shown.

updateFilter() is basically doing the same thing as the onQueryChange() function. But this time, it updates the value for the filters instead of the query.

Render the text field for entering the query as well as the different filters. The sliders requires the following props to be passed in:

  • maxValue – the maximum value allowed by the slider.
  • minValue – the minimum value allowed by the slider.
  • value – the current slider value.
  • labelSuffix – the text to display right after the max, min and current value.
  • onChange – the function to execute when the slider value changes.

Since we’re using the store passed through the context, we need to specify the contextTypes again:

Helpers

Helpers are functions that are used in multiple files throughout the app. These functions are usually used to filter data based on specific parameters. In our case, we’re using it to filter the mountains data based on the query and filters selected by the user. You would normally have this function inside a container component. But since we have two containers which needs this function, we took it out to a separate file and just have it imported. The file path is helpers/getVisibleMountains.js.

Review

To really drive home the point of using Redux, let’s run through how the app works one more time.

The app is made up of four main components:

  • Card
  • CardList
  • MarkerList
  • SearchFilter

One of the main principles of React is to have a number of components that can be reused throughout the whole app. Which is why each of the components should be able to work independently, except for parent-child components such as the CardList and Card. But this also means that we cannot have the components talk to each other, because if we do so, we will be creating a dependency between each of them. For example, the MarkerList and CardList components are essentially presenting the same data, only in a different form. So the original problem was how to create a link between these two components. This is where we used Redux to solve the problem. With Redux we were able to create a store that can be used by the whole app. We then updated this store by using actions and reducers. Actions describe what specific arguments are required to update the store while reducers are the one’s that describes how the store will change based on the data passed by the actions. Whatever value the reducers return will become the new value of that specific state in the store. By using these two, we were able to dispatch and subscribe to actions from any component in the app with the help of react-redux. We used the Provider component provided by react-redux as the root component for our app. This component allowed us to link the store to our React app. From there we just used React’s context to get a reference to the dispatch() and subscribe() functions from the store. For example, we used the dispatch() function on the SearchFilter component every time a change is made to the query or any of the filters. We then used the connect() function to automatically subscribe to the changes made to the store from the VisibleCards and VisibleMarkers container. This allowed us to re-render the CardList and MarkerList components every time the store is updated. This is made possible by the mapStateToProps() function in which the new store value is getting passed every time it’s updated. From there, we then returned only the mountains that matches the user’s query. The data that we returned are then passed as props to the CardList and MarkerList components which is finally presented to the user.

Conclusion

In this React and Redux tutorial, you’ve created an interactive 14ers clone. By doing so, you learned how to use Redux for managing application state within a React environment. You also learned concepts like the store, actions, reducers, dispatch and subscribe for making data flow throughout the whole application.

Further Reading

Comments