Creating a Realtime Pokedex App with React and RethinkDB

About the Author:

Kellye Whitney
Our staff is always looking for the latest news, technologies and trends in the developer training space.
, , ,

Creating a Realtime Pokedex App with React and RethinkDB

In this tutorial you’re going to create a realtime Pokedex app with React and RethinkDB. The app will allow users to look for a Pokemon by typing on a search field. If a Pokemon is not found, it will ask if they want to add it. If the user chooses yes, it will make a request to the server. The server will then talk to the Pokemon API to look for the Pokemon. If it exists, the server will save the data to RethinkDB. At this point, RethinkDB’s changefeeds feature will be triggered and it will send the new data to all connected clients using Socket.io. This will update the UI to show the newly added Pokemon to all users.

Here’s what the app is going to look like:

screen-shot-2016-09-07-at-5-12-28-pm

No previous knowledge of React or RethinkDB is required for this tutorial. So I’m going to provide as much detail as I can.

Steps We’ll Take in This Tutorial

  1. Install RethinkDB
  2. Setup a RethinkDB database and table
  3. Create the app
    1. Install the Dependencies
    2. Create the ListItem Component
    3. Create the Server Component

The final code for this tutorial can be found in this Github repository

Setting Up Your Environment

If you don’t want to pollute your OS with lots of installs, you can use the Scotchbox Vagrant box to install all the dependencies for this app.

Installing RethinkDB

You can install RethinkDB by following the installation instructions for your OS on the official docs. However, if you have already Docker installed, you can also run the RethinkDB docker image with the following:

docker run -d -P –name rethink1 rethinkdb

Once the image is installed, it will return the container ID, at this point you can run RethinkDB with the following:

docker start <container ID>

Setting Up the Database

Once you got RethinkDB running, access the admin console in your browser by going to https://localhost:32770. If you used vagrant, be sure to replace localhost with the ip address assigned to it.

Click on tables on the main navigation then click on add database. Enter pokedex as the name of the database. Once it’s created, add a new table named pokemon. This will be the table that you’re going to work on throughout the tutorial.

Creating the App

The app is going to have two components: the actual React app and the server component. You’re going to create the app first. This will reside inside the app folder of your working directory. You can check the project on Github to have an idea of the directory structure.

Installing the Dependencies

You can install the dependencies by creating a package.json file and add the following:

  {"name": "pokedex-app",

  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

"compile": "browserify -t [ babelify --presets [ react es2015 ] ] src/app.js -o js/app.js"

  },

  "author": "",

  "license": "ISC",

  "dependencies": {

"aja": "^0.4.1",

"picnic": "^6.1.5",

"react": "^15.3.1",

"react-dom": "^15.3.1",

"react-image-fallback": "^3.1.1",

"react-loading": "0.0.9",

"react-tooltip": "^3.1.6",

"socket.io-client": "^1.4.8"

  },

  "devDependencies": {

"babel-preset-es2015": "^6.13.2",

"babel-preset-react": "^6.11.1",

"babelify": "^7.3.0",

"browserify": "^13.1.0"

  }

}

Execute npm install to install all of the dependencies. Here’s an overview of what each one does:

  • aja – for making http requests to the server.
  • picnic – for beautifying the app.
  • react – for creating UI components.
  • react-dom – for rendering React components into the DOM.
  • react-image-fallback – for showing a fallback image in case the Pokemon images fails to load.
  • react-loading – for showing a loading animation while saving a new Pokemon data.
  • react-tooltip – for generating tooltips that shows the Pokemon description.
  • socket.io-client – for talking to socket.io.

And here’s what each devDependencies does:

  • browserify – puts all the files together in a single file so you can link to it on your page.

You could actually use Webpack to achieve the same thing plus more features such as live-reloading, but to keep things simple, I’m going to leave that for another day.

Once you’re done installing those, create a src and src/components directory. This is where the React components will reside. Inside the src directory, create an app.js file and import the libraries that you’ve just installed:

import React, {Component} from 'react';

import ReactDOM from 'react-dom';

import ListItem from './components/ListItem';



import io from 'socket.io-client/socket.io';



import Loading from 'react-loading';

import aja from 'aja';



Create the Main component:



class Main extends Component {



}

Inside is the constructor where you create a socket connection to the server component and declare the default state, because this function gets executed once this component is initialized. In React, the state is used to store component data that changes over time. Every time the state is updated, the component is re-rendered to reflect the changes. You can read more about this in Thinking in React.

constructor(props) {



  super(); //execute the initialization code of Component class

  this.base_url = 'https://192.168.33.10:3000'; //base URL of the server component



  //open a Socket connection to the server

  this.socket = io(this.base_url, {

transports: ['websocket']


  });

  //declare the default state

  this.state = {

search_term: '', //default value for the search field

pokemon_list: [], //array of Pokemon data from the server

filtered_pokemon_list: [], //filtered Pokemon data based on search_term

is_loading: false //whether the app is waiting for the server or not.

  };



}

The componentWillMount() is one of the component lifecycle methods in React. It gets executed right before this component is rendered into the DOM. This makes it the perfect candidate for listening for server updates and fetching the pokemon list from the server.

componentWillMount() {

  //executes when the pokedex_updated event gets emitted by the server

  this.socket.on('pokedex_updated', (data) => {

    

var pokemon_list = this.state.pokemon_list; //get the current pokemon list from the state

if(data.old_val === null){ //if its a new item

   pokemon_list.push(data.new_val); //add the new item into the temporary variable

   //actually update the state using the temporary variable for storing state data

   this.setState({

     pokemon_list: pokemon_list

   }, () => {//executed when the state is updated

     //filter the pokemon list based on the search term

     this.filterPokemonList.call(this, this.state.search_term, pokemon_list);

   });

}

  });



  this.getPokemonList(); //get list of pokemon from the server

}

The filterPokemonList() function is responsible for updating the state with a list of filtered Pokemon based on the search term inputted by the user.

filterPokemonList(search_term, pokemon_list) {



  var filtered_pokemon_list = pokemon_list.filter((pokemon) => {//loop through the pokemon list

if(pokemon.name.indexOf(search_term) !== -1){//check if search_term is within the pokemon name

   return pokemon;

}

  });

  this.setState({filtered_pokemon_list}); //update the state with the filtered list

}

The getPokemonList() function performs a GET request to the /pokemon route in the server. This route returns an array containing all the Pokemon data that were previously saved to RethinkDB. Once a response comes back, the state is updated using the returned data. The pokemon list and the filtered one is the same at this point since the user hasn’t typed anything on the search field yet.

getPokemonList() {



  aja()

.method('get')

.url(this.base_url + '/pokemon')

.on('200', (pokemon_list) => { //request returned a success response header

   this.setState({ //update the state with the pokemon list

     pokemon_list: pokemon_list,

     filtered_pokemon_list: pokemon_list

   });

})

   .go();



}

The render() method is the only required method when creating a component with React. It’s what returns the actual UI of the component. The UI is represented using HTML. And most importantly, a React component can only return a single root element.

render() {



  return (

<div>

   <div id="header">

     <h1>RethinkReact Pokedex</h1>

     <input type="text" name="pokemon" id="pokemon" onChange={this.searchPokemon.bind(this)} placeholder="What Kind of Pokemon are you?" />

   </div>

   <div className="pokemon-list flex">

   {this.state.filtered_pokemon_list.map(this.renderListItem.bind(this))}

   </div>

   <div className={this.state.is_loading ? 'loader' : 'loader hidden'}>

     <Loading type="bubbles" color='#f93434' />

   </div>

   {this.renderNoResults.call(this)}

</div>

  );



}

Breaking down the code above, you have a text field for entering the name of the Pokemon you’re looking for. The onChange attribute is used to specify the function to be executed when the value of the text field changes. In this case, the searchPokemon function is executed. There’s a need to use bind instead of simply calling it directly because methods in es6 aren’t automatically binded to the class. Below the search field is the container for the Pokemon list. This uses the filtered Pokemon list as the data source for the map function. The map function goes through the Pokemon list and calls the renderListItem() function for every iteration.

Below the Pokemon list is the container for the loading animation. The current value of the is_loading property is used to control the class name to use. A hidden class is added to hide the container if the component is not in the is_loading state.

The renderNoResults() function is called. This function renders the button for adding the Pokemon to the database. It only renders when the filtered Pokemon list contains nothing. The button when clicked calls the savePokemon() function.

renderNoResults() {



  if(!this.state.filtered_pokemon_list.length){

return (

   <div className={this.state.is_loading ? 'no-result hidden' : 'no-result'}>

     Sorry, I cannot find that Pokemon. Would you like to add it? <br />

     <button onClick={this.savePokemon.bind(this)}>yes!</button>

   </div>

);

  }



}

The savePokemon() function is the one that’s making a request to the server for saving the Pokemon into the database.

savePokemon() {

  //enable loading state

  this.setState({

is_loading: true

  });

  //check if user is searching for a blank string, only entertain the request if it’s not

  if(this.state.search_term.trim() != ''){    

//make a POST request to the server

aja()

   .method('post')

   .url(this.base_url + '/save')

   .data({name: this.state.search_term}) //add the name of the pokemon

   .on('200', (response) => {

     if(response.type == 'fail'){//if the request failed, inform the user

       alert(response.msg);

     }

     //disable loading state

     this.setState({

       is_loading: false

     });

   })

   .go();

  }

}

Next is the searchPokemon() function. As you’ve seen earlier, this gets executed every time the user types in something on the search field.

searchPokemon(e) {

  //e is the event, e.target refers to the search field, e.target.value is the current value of the search field

  let search_term = e.target.value.toLowerCase(); //convert to lower case

  this.setState({search_term}); //update the state with the search term

  this.filterPokemonList.call(this, search_term, this.state.pokemon_list); //filter the Pokemon list based on the search term.



}
The renderListItem() function gets called for every iteration of the map function inside the render method. The data of the specific Pokemon in the array item is passed in as an argument to this function. You then use it to supply data to the ListItem component by means of passing in props. Props are pretty much like your normal attribute, but they can be used inside the component by referring to them as this.props.{prop name}. In this case, pokemon is a prop and you're using the pokemon data as its value. key on the other hand is also a prop, but it serves a special purpose in React. And that is to uniquely identify each item in the array. This helps React in keeping track of each element whenever you need to sort, delete or add new elements into the array.

renderListItem(pokemon) {



  return (

<ListItem pokemon={pokemon} key={pokemon.id} />

  );



}

Creating the ListItem Component

As you’ve seen earlier, the ListItem component is used to render each Pokemon data. Create a ListItem.js file inside the src/components directory:

import React, {Component, PropTypes} from 'react';

import ReactImageFallback from "react-image-fallback";

import ReactTooltip from 'react-tooltip';

In the above code, aside from Components which you’ve used earlier, PropTypes is also extracted from react. You’ll use this later on to specify which props you expect consumers of this component should specify.

Create the component, this time you need to export it so that it can be used on other files.

export default class ListItem extends Component {}

 

The render() function returns a list item. It has a data-tip attribute which is used by react-tooltip for the contents of the tooltip. The tooltip becomes visible on click to cater for touch devices. Below it is the ReactImageFallback component which takes in the Pokemon image, the fallback image in case it fails to load, and an initial image to display while the image is loading. And below that is the Pokemon name and its types. Inside the types div is where the array of types is rendered.

render() {

  let {id, name, sprite, description, types} = this.props.pokemon;

  let sprite_img = `https://192.168.33.10:3000/img/${sprite}`;

  let loader_img = "img/loader.gif";

  let fallback_img = "img/pokeball.png";

  return (

<li className="pokemon" data-tip={description} data-event="click">

   <ReactTooltip place="bottom" type="dark" effect="float" class="tooltip" />

   <ReactImageFallback

     src={sprite_img}

     fallbackImage={fallback_img}

     initialImage={loader_img}

     alt={name}

     className="pokemon-sprite" />

   <div className="pokemon-name">{name}</div>

   <div className="types">

     {types.map(this.renderTypes.bind(this))}

   </div>

</li>

  );

}

Here’s the renderTypes() function:

renderTypes(type) {

  return (

<div className={type} key={type}>

{type}

</div>

  );

}

The Pokemon type is used as the value for the key because it’s unique.

Define the props that you want to be passed in to this component. In this case there’s only a single one and should be an object.

ListItem.propTypes = {

  pokemon: PropTypes.object.isRequired

};

If this prop isn’t passed in, a warning will show up in the console. If you want to learn more, you can read more about this on the documentation for prop validation.

Next, define the styles css/styles.css:

/*for centering the header*/

#header {

margin: 0 auto;

text-align: center;

width: 300px;

}



/*for adding white space around the edges*/

#main {

padding: 10px;

}

/*added to components that you want to hide*/

.hidden {

    display: none;

}

/*to keep the css loader centered*/

.loader {

width: 70px;

margin: 0 auto;

}



.no-result {

width: 300px;

padding: 10px;

margin: 0 auto;

}



.types {

text-align: center;

font-size: 14px;

}



.types div {

color: #fff;

display: inline-block;

padding: 5px;

}



li.pokemon {

list-style: none;

margin: 0;

padding: 0;

text-align: center;

}



.tooltip {

width: 200px;

}



.description {

font-size: 14px;

display: none;

}



.pokemon-name {

font-weight: bold;

text-transform: capitalize;

margin-bottom: 10px;

}

/*background color to distinguish between different pokemon types*/

.normal {

background-color: #8a8a59;

}



.fire {

background-color: #f08030;

}



.water {

background-color: #6890f0;

}



.electric {

background-color: #f8d030;

}



.grass {

background-color: #78c850;

}



.ice {

background-color: #98d8d8;

}



.fighting {

background-color: #c03028;

}



.poison {

background-color: #a040a0;

}



.ground {

background-color: #e0c068;

}



.flying {

background-color: #a890f0;

}



.psychic {

background-color: #f85888;

}



.bug {

background-color: #a8b820;

}



.rock {

background-color: #b8a038;

}



.ghost {

background-color: #705898;

}



.dragon {

background-color: #7038f8;

}



.dark {

background-color: #705848;

}



.steel {

background-color: #b8b8d0;

}



.fairy {

background-color: #e898e8;

}

To put it all together, create the actual app page (index.html):

<!DOCTYPE html>

<html lang="en">

  <head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>RethinkReact Pokedex</title>

<link rel="stylesheet" href="node_modules/picnic/picnic.min.css">

<link rel="stylesheet" href="css/style.css">

  </head>

  <body>

<div id="main"></div>

<script src="js/app.js"></script>

  </body>

</html>

As you can see, it’s pretty lightweight since all the UI stuff were already defined in React. All of the styles should be linkable since you already installed and created them earlier. The only thing that hasn’t been created yet is the js/app.js file. This is the compiled version of all the JavaScript files that you previously worked on. In the package.json file earlier, there’s the property called scripts:

"scripts": {

"compile": "browserify -t [ babelify --presets [ react es2015 ] ] src/app.js -o js/app.js"

},

This is where you can define commonly used npm scripts. In this case, a compile script is defined. This is responsible for compiling all the files inside the src directory to the js/app.js file. Browserify is the main script being used here, -t is used for defining the transforms to be utilized for compiling the app. This is where babelify and its presets comes in. They’re responsible for transforming the JSX and es6 code to JavaScript code that can run in modern browsers.

You can run the compile script using the following command:

npm run compile

Once the script is done, you should have a js/app.js file in your working directory.

Server Component

Navigate outside the app directory, create a package.json file and add the following:

{

  "name": "rethinkdb-react-pokedex",

  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "author": "",

  "license": "ISC",

  "dependencies": {

"body-parser": "^1.15.2",

"cors": "^2.7.1",

"express": "^4.14.0",

"pokedex-promise-v2": "^3.0.1",

"rethinkdb": "^2.3.2",

"socket.io": "^1.4.8"

  }

}

Execute npm install to install all the dependencies. Here’s what each one does:

  • body-parser – for parsing the request body for any JSON data.
  • cors – for allowing http requests from any host.
  • express – for creating the web server.
  • pokedex-promise-v2 – for easily making requests to the Pokemon API.
  • rethinkdb – for talking to RethinkDB.
  • socket.io – this handles real time connection between the server and the user’s browser by means of the WebSocket protocol. We’re using it to send new Pokemon data from the server to the client in real time.

Create a server.js file and add the following:

var r = require('rethinkdb'); //for talking to RethinkDB



var http = require('https'); //for making requests to Github for the Pokemon sprite

var fs = require('fs'); //for creating the file for the Pokemon sprite



//for talking to pokemon api

var Pokedex = require('pokedex-promise-v2');

var P = new Pokedex();



var express = require('express'); //for creating a web server

var app = express();



var cors = require('cors'); //for allowing cross-origin resource sharing on this server



var server = require('http').createServer(app); //create web server

var io = require('socket.io')(server); //attach socket.io to the server so it runs on the same port as the server. This means that the server will also have all the event listeners attached to socket.io, making it possible to send and receive data through the same port that the server runs in.



var bodyParser = require('body-parser'); //for parsing all sorts of request data



app.use(cors()); //allow cors in this server



app.use(bodyParser.json()); //for parsing json data

app.use(bodyParser.urlencoded({ extended: true })); //for parsing urlencoded data

Connect to RethinkDB and listen for changes on the pokemon table inside the pokedex database. If a change happens (e.g. a row is deleted/updated, a new row is created), loop through the changed data and emit a pokedex_updated event for each one. The row contains an old_val and a new_val. If it’s an insert operation, the old_val is null, if it’s an update operation, old_val contains the value from before the row was updated.

var connection; //rethinkDB connection



r.connect({host: 'localhost', port: 32769}, function(err, conn){

  if(err) throw err;

  connection = conn;

  //listen for any changes to the pokemon table

  r.db('pokedex').table('pokemon')

.changes()

.run(connection, function(err, cursor){

   if (err) throw err;

   io.sockets.on('connection', function(socket){

     cursor.each(function(err, row){ //loop through all the changed data

       if(err) throw err;

       io.sockets.emit('pokedex_updated', row); //emit an event to the clients  

     });

   });

  });

});

The /save route accepts the name of the Pokemon passed in from the request body. This is used to determine if it already exists in the database.

app.post('/save', function(req, res){



    var pokemon = req.body.name; //name of pokemon

    //check if Pokemon already exists in the database

    r.db('pokedex').table('pokemon').filter(r.row('name').eq(pokemon)).

run(connection, function(err, cursor) {

   if (err) throw err;

   cursor.toArray(function(err, result) { //convert cursor to an array

     if (err) throw err;

     if(result.length){ //pokemon already exists

     res.send({type: 'fail', msg: 'Pokemon already exists'}); //send failure message back to the client

     return;

     }

   });

});

    

});

After that, a request is made to the pokemon endpoint of the API to get the types (e.g. water, electric) of the Pokemon. Here you’re using the pokedex-promise-v2 library to easily get the details that you want. Once a response comes back, construct the object that contains the data that you want and then return it. You will be using promise.all later on to extract this data.

    //get data on pokemon type as well as the sprite

    var p1 = P.getPokemonByName(pokemon).then(function(response){

     

        var name = response.name;

        var sprite = response.sprites.front_default;

        var filename = 'public/img/' + name + ".png";

   var file = fs.createWriteStream(filename);

   var request = http.get(sprite, function(response){ //download the image from its source

     response.pipe(file); //save the image to its destination

      });



        var types = []; //array for storing the types (e.g. electric, water)

        response.types.forEach(function(row){

       types.push(row.type.name); //only push the name

        });

       //construct object to be saved to database

        var data = {

     name: name,

       sprite: name + ".png",

       types: types

        };



        return data;



})

.catch(function(error){

        res.send({type: 'fail', msg: 'Pokemon not found'}); //unknown pokemon was entered

        return;

});

Another request is also performed to the pokemon-species endpoint to get the Pokemon description. Again, extract the data that you want and then return it.

   //get description/flavor text data.

    var p2 = P.getPokemonSpeciesByName(pokemon).then(function(response){

            //only return the description of this specific pokemon in the alpha sapphire game and the language is english. This returns an array containing a single item which matched the condition 

     var result = response.flavor_text_entries.filter(function(row){

     return row.version.name == 'alpha-sapphire' && row.language.name == 'en';

     });

     var description = result[0].flavor_text;

     return description;

    })

    .catch(function(error) {

      res.send({type: 'fail', msg: 'Pokemon not found'});

      return;

    });

Both requests returns a promise so Promise.all is used to listen for when both promises has been resolved. At this point you know that all the data that you need has been acquired. So you can now save the data to RethinkDB. You can do that by calling the insert method and passing in an array containing the object that you want to save.

 //executed when both promises are resolved

    Promise.all([p1, p2]).then(function(response){

     var basic_info = response[0];

     var description = response[1];

     basic_info.description = description; //merge the second response

     //save to RethinkDB

     r.db('pokedex').table('pokemon').insert([

       basic_info,

     ]).run(connection, function(err, result){

       if(err) throw err;

    //send an success response

     res.send({

     type: 'ok'

     });

     });

    });

The /pokemon route returns all the data from the pokemon table:

app.get('/pokemon', function(req, res){

  res.header("Content-Type", "application/json");

  r.db('pokedex').table('pokemon')

.run(connection, function(err, cursor) {

   if (err) throw err;

   cursor.toArray(function(err, result) {

     if (err) throw err;

     res.send(result);

   });

  });

});

Finally, run the server on port 3000:

app.use(express.static(‘public’)); //specify directory for static assets

server.listen(3000); //run server on port 3000

Now you can access the app on your browser and add some Pokemon. For me it’s in https://192.168.33.10/rethinkreact-pokedex/app/index.html.

If you didn’t setup a server, you can just access the html file in the browser.

Summary

In this tutorial you learned how to use React, RethinkDB and Socket.io to create a Realtime Pokedex app. In a nutshell, RethinkDB is a NoSQL database which stores data as JSON documents. It also provides real time capabilities by means of its changefeeds feature. But since RethinkDB is just a database, you needed to use Socket.io in order to propagate the changes to all connected clients.

For further improvement of the app, you can try to add more data from the Pokemon API. And if you’re playing Pokemon Go, you can also add a Pokemon location feature where Pokemon locations are plotted in a Google map in real-time. You would need Geospatial queries for this one which RethinkDB supports by default.