A Definitive React-Native Guide for React Developers: Getting Started

RisingStack's services:

Sign up to our newsletter!

In this article:

Whether you want to experiment with developing a mobile app or have a business idea in mind, kicking off mobile development with React-Native is a great idea, and it’s especially an obvious choice if you are already familiar with React.

In this series, we’ll cover the basics of React-Native development, compare some ideas with React, and develop a game together. By the end of this tutorial, you’ll become confident with using the built-in components, styling, storing persisting data, animating the UI and many more.

React-Native-Guide-Application-Screen

If you want to check out the full sourcecode, here’s the GitHub repo.

This series is recommended for developers already familiar with React, so we won’t take too much time explaining basic React concepts.

In Part I. of this series we’ll discuss the following:

  • React-Native vs real-native: what’s the deal?
    • What is React-Native, who is behind it?
    • Why would you want to develop in React-Native?
    • Key differences and similarities between RN and native apps
    • A few words about real-time reload over recompiling
    • How does RN compile and work in the background?
  • What’s Expo?
    • The core concept behind Expo: extending React-Native features with pre-written native features
    • The brief history of how it became the main interface for developing React-Native apps
    • Pros and cons of using Expo over developing purely in React-Native
  • Differences and similarities between React and React-Native:
    • States and props
    • Flexbox (how it works in React-Native)
    • Components & most important pre-written components
  • Initializing the environment:
    • Installing the CLI, creating the project
    • Exploring directory structure
    • Assets (icons, fonts, music, sfx)

React-Native vs real-native: what’s the deal?

You may look down on hybrid apps because of their poor performance in the past (I’m looking at you Ionic!), but you can forget them for now.

In the background, React-Native uses the same native building blocks as you would use while developing native apps with Swift, Java or Kotlin. The only difference is that you put these building blocks together with JavaScript.

But if it’s just a simple API wrapper, why do developers love it, then?

React-Native’s real power comes from giving you a load of features that improve the developer experience, like Live Reload that reloads the app in a few seconds when saving, or Hot Reloading that injects code changes without reloading the app and losing your state.

Here’s a video that compares these two.

What’s Expo?

As you may have already noticed, the React-Native documentation features Expo CLI as the default toolchain to get started. Let’s take a look at what’s Expo, where does it come from, and why is there a 3rd party in the official documentation.

Expo is an SDK that started as an unofficial toolkit, but then it grew into a much bigger project with features like OTA app updates (bypassing the long App Store review times), social authentication, camera access, push notifications and many more services out of the box, for totally free!

They also have an online playground named Snacks where you can try out other people’s code online, or work on bite-sized projects without the need of creating a project in the CLI.

The core concept behind Expo is to extend React-Native with pre-written components like the ones mentioned above. Expo has awesome documentation with an exhaustingly long list of APIs in the sidebar, be sure to check it out!

However, Expo also has its downsides: your app will have a considerable size when you build it (about 20-30MB .apk for a Hello World project) because it has all of these features built-in, not only the ones that you use; and at this time, you can’t explicitly write or use native Swift/Kotlin code.

Since we aren’t aiming production nor planning to write native code in our project as we’re only building an example project, Expo is the fastest way to start off, thus we’re going with it.

Does React-Native have a different approach for the same concepts as React?

In short: no.

For example, states and props work the same way, components have the same underlying idea to them as in React, and you can even access React APIs (like the Context API) within React-Native. However, some things couldn’t be implemented the same way as in React, simply because web development’s logic differs from mobile.

For example, flexbox works the same way in CSS with a few exceptions: the flex-direction is defaulting to column instead of row, and the flex only supports a single number as a parameter. Also, it’s the default method to build UIs. If it isn’t already familiar for you, consider practicing the use of flexbox: Flexbox Froggy is a cool game that you should try before moving on with the tutorial.

Since you aren’t writing for the web, you cannot use default HTML tags like div or img – you’ll need native UI components.

Please keep in mind that the default components may look and behave differently on different platforms, so you should always test them on all platforms before releasing something. (Oh, and negative margins don’t work on Android)

But what are the building blocks of the UI if not HTML elements?

There are plenty of useful pre-written components that React-Native provides us. For example, instead of <div>s, you use <View>s, for displaying texts, you use <Text>, for buttons, you use <Button>, and so on. Please be sure to check out the official documentation that highlights the most important components (and the full list is always available from the sidebar) before moving on.

But what are we building, exactly?

Good question. We are building a fun minigame I call “ColorBlinder”. The main game screen is pretty simple: it’s built up from a few tiles that are colored the same, except one. The player’s task is to tap the differing tile as fast as possible.

React-Native-Guide-Application-Screen-1

Some mockups I made before starting the development, just to illustrate the main idea. We’ll use these illustrations along the course when building components.

Setting up our React-Native Development Environment

Okay, okay, geez, don’t be so impatient! We’ll get to it soon enough, but first, we’ll need some tools to kick off the development. Make sure that you have Node.js installed, and install the Expo CLI:

npm i -g expo-cli

Then let’s init an empty project. I’ll call it colorblinder as this is the name of the game we’ll work on along the series, but you can call it whatever you want.

expo init colorblinder

## choose `empty` template for this project
## choose `managed` workflow
## write in `ColorBlinder`
## if it asks, use yarn to install dependencies. (If you don’t have it, [be sure to check it out](https://yarnpkg.com/), it’s a fast alternative to npm)

cd colorblinder
npm start ## or yarn start if you have yarn, or expo start, whichever you prefer

This command will start the development server with an empty project for you, and open a new Chrome tab with the Expo DevTools. You can use a simulator or a device with Expo Client installed, whichever you prefer.

If you want to check out the full source code in one piece, check out the GitHub repo.

Project Structuring our React-Native Game

First, let’s explore the directory structure of our new project.

├── App.js
├── app.json
├── assets
│   ├── icon.png
│   └── splash.png
└── package.json
# a few more files that we don’t care about yet

The structure may seem simple, but just to make sure that it all makes sense, we’ll look through it.

First, let’s explore the app.json. It’s a configuration file for Expo: it contains the SDK version, name, logo/splash screen directions, platforms, GitHub URL, and a lot more. Explore it in details from the documentation.

If you want to develop the game on your own, download the assets and copy-paste it into your assets directory. Make sure that you are overwriting icon.png and splash.png! The sound effects originate from here, and we’ll use music made by Komiku – be sure to check them out and donate for their work if possible, they deserve it.

After you pasted in the assets and open the app, you may notice that the sizing of the splash screen is a bit off, so before we start initializing our home screen, we should fix that. In the app.json under the splash property, set the backgroundColor to #0a0a0a.

(If you don’t see it yet, restart the packager and/or the client).

Getting Started with React Native!

As you may have seen, we’ll have a few different screens (two, to be exact), so we are going to need a router to navigate between them. Expo’s documentation has a pretty good explanation on why you may need a router if you aren’t familiar with the idea of using routers already. We are going to use react-navigation, so I suggest to check out their Getting Started guide.

Add react-navigation as a dependency to the project:

yarn add react-navigation
## if you don’t have yarn: npm install react-navigation

Create a components directory for our components that we’ll build (like the header or the grid), and a screens directory for the screens we’ll compose (the Home and the Game screen exactly). Make sure that both of the directories are in the root of the project, and create Home.js in the screens directory. Just to make sure that it renders something, copy-paste this into it:

import React, { Component } from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class Home extends Component {
 render() {
   return (
     <View style={styles.container}>
       <Text>ColorBlinder - Home screen!</Text>
     </View>
   );
 }
}

const styles = StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: '#fff',
   alignItems: 'center',
   justifyContent: 'center',
 },
});

Notice that you are extending the React.Component class, so nothing new there. Instead of divs, you are using <View>s that are imported from the react-native package, and to show some text to the user, you use a <Text> tag.

Also, the StyleSheet on the bottom of the file may look weird: it’s a React-Native specific API. It’s basically an abstraction for CSS: you can create new styles in the object and name them anything you’d like, and you can use basic CSS properties (just camelCased) to define properties.

You can use them in your JSX simply by putting the name of the style in the style={} prop. (If you want to separate the JSX and the styling, you can, but for now, we’ll have them in the same file – we will move the StyleSheet to a different file later, though!)

Initializing the Routing

Now that Home.js is rendering something, let’s initialize the routing in the App.js:

import { createStackNavigator, createAppContainer } from "react-navigation";
import Home from './screens/Home';

const AppNavigator = createStackNavigator({
 Home: {
   screen: Home
 }
});

export default createAppContainer(AppNavigator);

We are using react-navigation’s createStackNavigator: it initializes a new stack navigator that returns a React component that we can render – thus we can export it from App.js and except it to work.

When you run the code, you’ll see something like this:

React Native Mobile App Screen Colorblinder

You can see that there’s an (empty) navigation bar on the top of the screen: we definitely don’t want it to be visible as it would make the game look terrible, so in the App.js, let’s spice up our stack navigator with some properties:

const AppNavigator = createStackNavigator(
 {
   Home: {
     screen: Home
   }
 },
 {
   initialRouteName: "Home",
   headerMode: "none",
 }
);

The top navigation bar will now be hidden.

Using Custom Fonts

The Expo documentation’s guide gives you a straightforward idea on how to use custom fonts in your app, but we will need some major changes in our app structure to be able to integrate them into the project since we are using a navigator.

As you can see, you need to render a different component while you are loading the font (the splash screen would be perfect!), so first, rewrite your App.js as the following:

import React, { Component } from 'react'
import { AppLoading, Font } from 'expo';
import Routes from './screens/Routes'

class App extends Component {
 state = {
   isFontLoaded: false
 }

 async componentDidMount() {
   await Font.loadAsync({
     'dogbyte': require('./assets/fonts/dogbyte.otf'),
   });
   this.setState({ isFontLoaded: true })
 }

 render() {
   if(!this.state.isFontLoaded) {
     return <AppLoading />
   } else {
     return <Routes />
   }
 }
}

export default App;

(A small notice for those who are using vanilla React-Native without Expo: the exact font name is d0gbyte as u/keithj0nes pointed out.) We are asynchronously importing the font dogbyte. The state has an isFontLoaded property that will be true when the font is loaded. While it’s false (or in other words, while the font is not loaded), it will render the <AppLoading /> component that comes from the Expo SDK. This component shows the splash screen of the app when rendered.

When the this.state.isFontLoaded is true (when the font is loaded), it will render the Routes component, that is undefined as of now, so let’s quickly create a file under screens named Routes.js and copy-paste our previous navigator code into it:

import { createStackNavigator, createAppContainer } from "react-navigation";
import Home from "./Home";

const StackNavigator = createStackNavigator(
 {
   Home: {
     screen: Home
   }
 },
 {
   initialRouteName: "Home",
   headerMode: "none"
 }
);

export default createAppContainer(StackNavigator);

Prepare for the next part of our React-Native Guide!

At this point, we have basic navigation initialized in our app and can use our custom font. It’s time to call it a day, pat yourself on the back for making it this far into your journey and have a rest.

In the next session, we’ll build the home screen of the app!

colorblinder-react-native-homescreen

If you weren’t following along or you got stuck somewhere, you can access the code that’s written at this point here.

Part 2

In this episode, we’ll build the home screen of our React-Native application.

For a quick reminder, here’s the sketch of it:

React Native Mobile App HomeScreen

Table of contents:

Splitting the Home screen

First things first, let’s split up the Home screen into two files: an index.js and a styles.js, just so that the main logic and the stylesheets are well-separated.

# the screens’ directory structure as of now
screens
├── Home
│   ├── index.js
│   └── styles.js
└── Routes.js

Let’s initialize the styles.js with a basic container style:

import { StyleSheet } from "react-native";

export default StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: "#0a0a0a",
   justifyContent: "center",
   alignItems: "center"
 }
});

Next, import the styles in the index.js, delete the StyleSheet and modify the JSX a bit, so that the index.js looks like this:

// basic imports ...
import styles from './styles';

export default class Home extends Component {
 render() {
   return (
     <View style={styles.container}>
	{/* // this View is empty on purpose! */}
     </View>
   );
 }
}

// notice the deleted styles - they are imported from the styles.js!

Creating the Header

Let’s create the header! We want it to be visible on the Home and the Game screen too, so we will build a component named Header that will display the rainbow text, and re-use it on both screens.

React Native Mobile App Name

Simply create a Header.js file in the components directory and copy-paste the code below.

import React from "react";
import { Text, View, StyleSheet } from "react-native";

const Header = () => (
 <View style={{ flexDirection: "row" }}>
   <Text style={[styles.header, { color: "#E64C3C" }]}>c</Text>
   <Text style={[styles.header, { color: "#E57E31" }]}>o</Text>
   <Text style={[styles.header, { color: "#F1C431" }]}>l</Text>
   <Text style={[styles.header, { color: "#68CC73" }]}>o</Text>
   <Text style={[styles.header, { color: "#3998DB" }]}>r</Text>
   <Text style={styles.header}>blinder</Text>
 </View>
);

const styles = StyleSheet.create({
 header: {
   fontSize: 50,
   color: "#ecf0f1",
   fontFamily: "dogbyte"
 }
});

export { Header };

Because the Header does not need an internal state, we can define it as a stateless functional component. If you aren’t familiar with the idea of using SFCs yet, Hackernoon has a really great summary of what they are and how they work.

Next off, let’s initialize Components/index.js:

export * from './Header'

And import the Header component in the Home/index.js:

import { Header } from '../../components'
// …
<View style={styles.container}>
       <Header />
     	</View>

If you check your Expo Client at this point, your app will look like this:

react native mobile app homescreen only with text

This looks cool, but there’s a small error that we should fix before moving on to our next component: the iOS status bar blends into the background. We can fix this in a few lines of code on the root level, at the App.js,

with the StatusBar component.

Fixing the Status Bar

First, import the StatusBar from react-native and the Fragment from react (if you don’t know about React.Fragment yet, be sure to check the documentation, but in a nutshell, you can use Fragments when you don’t want to add another div to the DOM, but you need to return two or more components from somewhere for React).

import React, { Component, Fragment } from 'react';
import { StatusBar } from 'react-native';

Then add the StatusBar component to our app:

else {
   return (
      <Fragment>
        <StatusBar barStyle="light-content" />
        <Routes />
      </Fragment>
    )
}

It’s just a few lines of code that’s definitely worth adding – the app will now look like this:

react native mobile app homescreen with status bar

That’s one small step for the code, one giant leap for the overall UX.

Adding Interactive Elements to our React-Native App

The logo looks pretty cool, but maybe it’s time to make some elements that the user can interact with – so let’s start with the big Play button that will dominate the screen:

react native mobile app play button

Since we aren’t going to make a traditional button (it will contain an image, a text, and won’t have its own background or border), we won’t use a <Button> – instead, we’ll use a <TouchableOpacity>. It’s a react-native component that gives any component the ability to be tappable and respond to the interactions by dimming the opacity of it. You can read more about it in the React-Native docs.

Let’s import Text, Image, and TouchableOpacity along with View:

import { View, Text, Image, TouchableOpacity } from "react-native";

Create a callback named onPlayPress. Until we figure out what to do with it, a placeholder console.log() will be fine:

onPlayPress = () => {
  console.log("onPlayPress event handler");
};

And finally, the button itself:

<TouchableOpacity onPress={this.onPlayPress} style={{ flexDirection: 'row', alignItems: 'center' }}>
  <Image
    source={require("../../assets/icons/play_arrow.png")}
    style={styles.playIcon}
  />
  <Text style={styles.play}>PLAY!</Text>
</TouchableOpacity>

Notice how you can import images with the require() function. We will use it along this course because the documentation features it, however, there are better solutions out there, so be sure to check them out if you are aiming for production.

If you run the app, you’ll have to realize that we already passed the styles, but they are not defined yet, so let’s go to styles.js and create them:

play: {
   fontSize: 45,
   fontFamily: "dogbyte",
   color: "#ecf0f1",
   marginTop: 5
 },
 playIcon: {
   height: 60,
   width: 60,
   marginRight: 15
 }

There’s nothing special there that would need any explanation, so let’s move on. The app should look like this at this point:

react native mobile app logo and start

Let’s continue with the Hi-score component:

we can worry about the layout later.

Building the High Score Panel

react native mobile app highscore

The high score panel has a similar layout to the Play button, but it’s not tappable, and it’s slightly smaller too – thus we’ll need another bunch of styles for the new, smaller sizes:

hiscore: {
  fontSize: 28.5,
  fontFamily: "dogbyte",
  color: "#ecf0f1",
  marginTop: 5
},
trophyIcon: {
  height: 45,
  width: 45,
  marginRight: 12.5
}

And include the new <Image /> and <Text> in a <View>:

<View style={{ flexDirection: 'row', alignItems: 'center' }}>
  <Image
    source={require("../../assets/icons/trophy.png")}
    style={styles.trophyIcon}
  />
  <Text style={styles.hiscore}>Hi-score: 0</Text>
</View>

We will make this section show the real high score later in the 5th post of this series in the “Persisting data –

storing the high scores” section.

DIY: Build the Leaderboard Button!

This button will look and behave very similar to the Play button:

react native mobile app highscore leaderboard

At this point, try to pause reading and copy-pasting things from the article, and try to build this on your own! Practice makes perfect. If you are stuck with it, try to look back at the Play button and the Hi-score panel’s source for some inspiration, or if you really don’t feel like it,

check the finished code in the GitHub repo.

Let’s add a copyright banner & speaker icon!

In the sketch, there were a few smaller things on the bottom of this screen: a copyright banner, and a speaker icon for muting the game.

react native mobile app copyrights

The container is just an absolute positioned View, nor the text style isn’t too exciting:

bottomContainer: {
   position: "absolute",
   left: 15,
   right: 15,
   bottom: 12.5 // the 2.5px bottom margin from the text is subtracted from the 15px spacing
 },
 copyrightText: {
   fontSize: 16,
   fontFamily: "dogbyte",
   marginBottom: 2.5
 }

And the JSX isn’t a big hack either:

<View style={styles.bottomContainer}>
  <Text style={[styles.copyrightText, { color: "#E64C3C" }]}>
    Music: Komiku
  </Text>
  <Text style={[styles.copyrightText, { color: "#F1C431" }]}>
    SFX: SubspaceAudio
  </Text>
  <Text style={[styles.copyrightText, { color: "#3998DB" }]}>
    Development: RisingStack
  </Text>
</View>

You may notice that toggling the speaker icon only updates an internal state as of now. We will add music and SFX later, in the 5th post of this series. Let’s define our initial state:

state = {
  isSoundOn: true
};

Toggling the sound will also change the icon, and as it has two possible states (music muted and enabled), we’ll have two corresponding icons:

react native mobile app music toggle

In the render function, we want to dynamically import the icon based on the state:

 render() {
   const imageSource = this.state.isSoundOn
     ? require("../../assets/icons/speaker-on.png")
     : require("../../assets/icons/speaker-off.png");
   // ...

We need to add a TouchableOpacity with an image in it. It will display the speaker icon, but to push it to the right side of the screen, you can either play with adding margins, or adding a <View style={{ flex: 1 }} /> before the button.

The empty view will fill up all the empty space on the screen because of its flex: 1 property. It may seem a bit odd for first, but it’s a commonly used practice in React-Native development to use this when doing MVPs, but in production, you should probably stick with using margins or any other solution that’s idiomatic to your use case.

<View style={{ flex: 1 }} />
<TouchableOpacity onPress={this.onToggleSound}>
    <Image source={imageSource} style={styles.soundIcon} />
</TouchableOpacity>

Currently, our app looks like this:

react native mobile app styling

You may have already noticed that the spacing is messed up, so let’s fix that by adding some spacing with margin:

  • For the TouchableOpacity wrapping the play button, add marginTop: 80 to the style property
  • For the View wrapping the Hi-score, add marginTop: 20
  • For the TouchableOpacity wrapping the Leaderboard button, add marginTop: 80

Now that the UI elements can breathe and our home screen looks nice, we can move on the screen that the players are going to spend most of their time on – the game screen.

You can access the code that’s written at this point here.

In the next episode, we’re building the main logic of our mobile game. Check it out!

Part 3

Now, we are going to work on the main game logic and the Game screen.

Table of contents below:

You can find the whole codebase of our react-native mobile app here!

In the third article, He wrote: “Let there be game!”, and there was a game.

Let’s initialize our Game screen inside our screens directory by creating a Game directory with an index.js and styles.js. Then, in the Routes.js, import the screen so that we can use it in our router:

import Game from "./Game";

Also, inside the first argument of the createStackNavigator, there’s already a Home object: use that as a sort of template to add the Game screen to the router.

const StackNavigator = createStackNavigator(
 {
   Home: {
     screen: Home
   },
   Game: {
     screen: Game
   }
 },
…

After you save your code, the app will crash. (If it didn’t, good luck debugging it.) That’s because the Game/index.js is empty but we are already importing and using it in our router. Let’s initialize it with some boilerplate to silence the error!

import React, { Component } from "react";
import { View } from "react-native";
import { Header } from "../../components";
import styles from "./styles";

export default class Home extends Component {
 render() {
   return (
     <View style={styles.container}>
       <Header />
     </View>
   );
 }
}

Notice how it’s already using the ./styles – let’s define it! In the styles.js, add the following code:

import { StyleSheet } from "react-native";

export default StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: "#0a0a0a",
   justifyContent: "center",
   alignItems: "center",
 }
});

Also, the Header is a reusable component, but we need to modify it so that it suits our needs. As you can see on the picture below, the font size is slightly smaller.

react native mobile game main screens

You may want to work around it with a fontSize number property so that the size can be modified any time, or with an isMini boolean property that you can simply pass for the component, and it will automatically decide the font size.

Both approaches are totally valid, but I’ll go with the fontSize number property approach because I think it’s more flexible and future-proofed, since we can pass in any number we’d like.

How about PropTypes?

In React, you may already be familiar with the concept of PropTypes – you can type-check the components properties with it. In React-Native, you can use the very same method for type checking like in React: you just import the PropTypes with the line import PropTypes from ‘prop-types’ and then at the end of the file, you just add the .propTypes and .defaultProps properties. After that, everything will be all set:

Header.propTypes = {
 fontSize: PropTypes.number
}

Header.defaultProps = {
 fontSize: 55
}

However, we are not applying this property to the text itself – yet. Delete the fontSize property from the StyleSheet to make sure that the two properties won’t have a battle in the background and overwrite each other, and since we used a stateless functional component to declare the Header, we can’t use this.props. We can, however, use the arguments of the function to access the props by modifying the declaration line as it follows:

const Header = ({ fontSize }) => ( … } 

And from now on, you can just add the fontSize to every Text components style property like this:

<Text style={[styles.header, { fontSize }]}>blinder</Text>

Now, pass the desired fontSize prop to the Header component in the Game screen. After reloading the app, you’ll see that the Header component is now rendering properly on both screens –

Before we start building our game screen, it is a good idea to add routing so that we can get there and see what we are building. It couldn’t be any simpler with react-navigator: we just need to add this.props.navigation.navigate('Game'); to our onPlayPress event handler: the react-navigator already managed to pass a navigation object as a property to our Home screen, and we can use its functions to navigate between screens. If you save the code and tap on the Play button, you are going to be routed to the Game screen.

Notice that by swiping back, you can get back to the Home screen. This may be the expected behavior when building an app, but it would be very nerve-racking to accidentally swipe back to the home screen while playing the game so it may be a good idea to disable this feature for now.

Please note that when you disable both the swipe navigation and the navigation bar, you need to be sure that have your own button on the UI that the user can use to navigate back to the previous screen!

You can read more about good navigation UX in Apple’s Human Interface Guidelines.

You can easily disable the swipe navigation on a particular screen by disabling the gesturesEnabled property in the navigationOptions of the Game screen in the Router.js, as it follows:

Game: {
     screen: Game,
     navigationOptions: {
       gesturesEnabled: false,
     },
   }

If you reload the app and try to swipe back from the Game screen, you’ll notice that you can’t, and that’s the behavior we wanted to achieve, so let’s move on.

We’ll get started by understanding the underlying game logic before trying to build the UI.

How will this work, exactly?

When the player starts the game, they will see a 2×2 grid with one tile slightly off:

mobile game grid

They will have 0 points and 15 seconds after starting the game. When touching the correct tile, they’ll get +1 point and +2 seconds. If they touch the wrong tile, they get -2 seconds as a punishment. You can never win this game – it’s endless.

The grid will grow over time, but the maximum is a 5×5:

react native mobile game mechanics

The colors are going to be randomly generated by generating the 0-255 values and passing these as an RGB color to the tiles.

The differentiating tile will have its RGB values mutated with a random value between 10 and 20.

Let’s create our random RGB value generator!

Since we are trying to make our code clean, we don’t want to create this in the Game directory. We’ll also have some other utilities, so let’s create a utilities directory in the root of the project, create an index.js and a color.js, and initialize the index.js before moving on:

export * from './color'

export default {}

And create our RGB value generator and the mutator in the color.js:

export const generateRGB = () => {
   const r = Math.floor(Math.random() * 255);
   const g = Math.floor(Math.random() * 255);
   const b = Math.floor(Math.random() * 255);
   return { r, g, b }
};

export const mutateRGB = ({ r, g, b }) => {
   const newR = r + Math.floor(Math.random() * 20) + 10;
   const newG = g + Math.floor(Math.random() * 20) + 10;
   const newB = b + Math.floor(Math.random() * 20) + 10;
   return { r: newR, g: newG, b: newB }
};

The mutator may seem a bit hacky:

it creates a random number between 10 and 20 and adds it to the original RGB value passed as a prop, then returns the new colors.

Defining the Main Logic

Now that we have some utilities for working with colors, we should set some basic things up on the Game screen, too – for example, defining the initial state is a good place to start off:

 state = {
   points: 0,
   timeLeft: 15,
 };

Also, adding a timer that divides the timeLeft in the state by one after every second can be done with setInterval()Component lifecycle methods work the same way as in React, thus we can use componentWillMount() and componentWillUnmount() to create and destroy our timer:

 componentWillMount() {
   this.interval = setInterval(() => {
     this.setState(state => ({ timeLeft: state.timeLeft - 1 }));
   }, 1000);
 }

 componentWillUnmount() {
   clearInterval(this.interval);
 }

Notice how I added the interval to the Game screens scope (or this) – it’s in order that we can destroy it later in the componentWillUnmount(). If this arrow function thingy in the this.setState() looks a bit weird, be sure to check out the React docs –

it will convince you on why you shouldn’t use this.setState({ timeLeft: this.state.timeLeft - 1 }).

Let’s build the grid with some flex magic ✨

react native flex

The key component on the screen is the grid with all them colorful tiles hangin’ out there, so let’s build that with flex. We are trying to keep the code needed as small as we can – so we are going to generate the grid instead of hardcoding it. Go to the screens/Game/index.js and import our shiny new utilities:

import { generateRGB, mutateRGB } from '../../utilities';

Then when declaring the state, initialize the first color, too:

 state = {
   points: 0,
   timeLeft: 15,
   rgb: generateRGB()
 };

Next off, add some constants to our render() function – we will use these later:

const { rgb } = this.state;
const { width } = Dimensions.get("window");

If you are not familiar with this syntax yet, it’s called object destructuring. With this, you can access an objects (e.g. this.state) properties without writing out this.state.rgb, but by destructuring and then just typing in rgb.

Dimensions is yet another very useful React-Native class (and you need to import it before you can use it, so be sure to add it to the top of the file): you can get the devices’ width and height and add event listeners to when the dimensions change (e.g. window resize, screen orientation change). Be sure to check out the related docs!

Now, add the container that will wrap our tiles:

<View style={{ height: width * 0.875, width: width * 0.875, flexDirection: 'row' }}>
</View>

Notice that it uses width * 0.875 for both width and height: I decided to style like this so that it’s always a perfect square and the containers dimensions match. You can style differently if you want to, but I recommend going with this.

Now that the container and the helper functions are finished, we can generate the grid inside the container with the following code:

{Array(2).fill().map((val, columnIndex) => (
  <View style={{ flex: 1, flexDirection: 'column' }} key={columnIndex}>
     {Array(2).fill().map((val, rowIndex) => (
        <TouchableOpacity
          key={`${rowIndex}.${columnIndex}`}
          style={{
             flex: 1,
             backgroundColor: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
             margin: 2
          }}
          onPress={() => console.log(rowIndex, columnIndex)}
        />
      ))}
  </View>
))}

This solution may seem a little bit hacky for first, but it’s flexible and definitely shorter than hard-coding every possible grid layout. It creates an empty array with 2 items with the Array(2), fills it so that we can map over it, and creates a <View> with the style properties flex: 1 to fill in the available space and it makes sure that the rows are under each other with flexDirection: column.

Then, inside these rows, it generates the tiles by creating another empty array and iterating over it and adding a TouchableOpacity for every iteration. This view has a flex: 1 property to flexibly fill up the space available, the backgroundColor is just a string with the RGB values (more about passing colors in React-Native in the docs), and the margin is added to make sure that there’s some space between the tiles.

You can add more margin to make the game harder or get rid of it to make the game very-very easy, but I think the 2-pixel margin is just ideal for a fun gameplay.

For now, the game looks like this:

mobile game screen

It’s a nice grid – but let’s just don’t stop there. We need a differentiating tile, too! But before we dive into that, make sure that we have the size of the grid in our state:

state = {
  points: 0,
  timeLeft: 15,
  rgb: generateRGB(),
  size: 2
};

Then in the grid generators, replace every Array(2) with Array(size). Use object destructuring to get the size out of the this.state.

After you replaced them and you still get a 2×2 grid, so you’re good to go to generate the position and the color of the differentiating tile:

generateSizeIndex = size => {
 return Math.floor(Math.random() * size);
};

generateNewRound = () => {
 const RGB = generateRGB();
 const mRGB = mutateRGB(RGB);
 const { points } = this.state;
 const size = Math.min(Math.max(Math.floor(Math.sqrt(points)), 2), 5);
 this.setState({
   size,
   diffTileIndex: [this.generateSizeIndex(size), this.generateSizeIndex(size)],
   diffTileColor: `rgb(${mRGB.r}, ${mRGB.g}, ${mRGB.b})`,
   rgb: RGB
 });
};

The generateSizeIndex() generates a new number between 0 and the size passed as an argument. The generateNewRound() creates the data needed for a new round (differentiating tile index and color) and modifies the state to match it. This will run every time the user taps on the correct tile, and when initializing the game for the first time.

Call the this.generateNewRound() at the top of the componentWillMount() to make sure that the game initializes a round when the player opens the screen.

Now, we have a dynamic grid, and the properties of the differing tile: now, the only thing we need to do is to merge these two so that the differing tiles’ color actually differs from the others.

First, in the render() where we destructure the state, add diffTileIndex and diffTileColor. Then, when passing the backgroundColor to the TouchableOpacity, you can check if the tile that’s being generated is the differing tile with the following: rowIndex == diffTileIndex[0] && columnIndex == diffTileIndex[1]. This checks if the row and the column of the tile and the differing tile match. We can use a ternary operator to modify the color of the tile, as it follows:

backgroundColor:
  rowIndex == diffTileIndex[0] && columnIndex == diffTileIndex[1]
     ? diffTileColor
     : `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,

If you refresh the app now, you’ll have one tile with the color slightly off:

a generated grid with a differentiating tile

If you don’t see the difference, increase the brightness on your screen.

At this point, we are just a few steps away from finishing off the grid –

a proper onPress handler is the only thing needed before we can move on to the bottom bar, so let’s finish off with that!

Handling Taps on Tiles

We need to add 1 point and 3 seconds to the timer if the player taps on the right tile, then generate a new color and increase the size of the grid if needed. If the player taps on the wrong tile, we decrease the timer by 1 second as a punishment and shake the grid (shaking the grid will be implemented later in the “Shaking the grid – animating in React Native” section, so don’t worry about that now!).

First, just defining a placeholder event handler and passing it as a prop onto our TouchableOpacity will do it:

onTilePress = (rowIndex, columnIndex) => {
  console.log(`row ${rowIndex} column ${columnIndex} pressed!`)
}

And in the <TouchableOpacity>:

onPress={() => this.onTilePress(rowIndex, columnIndex)}

And boom, when you press a tile, you will see which one was pressed in the console.

In the onTilePress(), we can use the same ternary code as we used when passing the backgroundColor to determine if the user has tapped on the differing tile:

onTilePress = (rowIndex, columnIndex) => {
  const { diffTileIndex, points, timeLeft } = this.state;
  if(rowIndex == diffTileIndex[0] && columnIndex == diffTileIndex[1]) {
    // good tile
    this.setState({ points: points + 1, timeLeft: timeLeft + 2 });
  } else {
    // wrong tile
    this.setState({ timeLeft: timeLeft - 2 });
  }
}

If you add console.log(this.state) to the end of the onTilePress(), you’ll be able to see how the state mutates over time. However, the grid does not change, so let’s call this.generateNewRound() in the good tile block, and modify the function a little bit so that it generates new color, too:

generateNewRound = () => {
   const RGB = generateRGB();
   const mRGB = mutateRGB(RGB);
   this.setState({
     diffTileIndex: [this.generateSizeIndex(), this.generateSizeIndex()],
     diffTileColor: `rgb(${mRGB.r}, ${mRGB.g}, ${mRGB.b})`,
     rgb: RGB,
   });
 };

Now tapping the correct tile will give you some extra time, points, and a new color. The only thing left is to increase the grid size.

Before generating a new index for the winner tile, calculate the grid size:

 generateNewRound = () => {
   const RGB = generateRGB();
   const mRGB = mutateRGB(RGB);
   const { points } = this.state;
   const size = Math.min(Math.max(Math.floor(Math.sqrt(points)), 2), 5);
   this.setState({
     size,
     diffTileIndex: [
       this.generateSizeIndex(size),
       this.generateSizeIndex(size),
     ],
     diffTileColor: `rgb(${mRGB.r}, ${mRGB.g}, ${mRGB.b})`,
     rgb: RGB,
   });
 };

This sizing method may look a bit odd, so let’s break it down to see a clearer picture of how it works: I wanted to achieve a one-liner, well-balanced solution for making the game harder by increasing the grid size over time.

My initial idea was to make the grids size the square root of the points – so when you reach 4 points it is 2, at 9 points it is 3, at 16 points it is 4, and so on. But I needed a minimum size for the grid (2) because before reaching that, there would be either zero or one tile. I also wanted a hard cap on the maximum size of the grid (it’s 5 in the example, but it could be any number you’d like).

Now if you play the game, you’ll start with a 2×2 grid, and as your points increase, you’ll see the tiles getting smaller, expanding into a 5×5 grid over time. Great job there!

Part 4

As of now, we have the main grid logic finished – but we don’t see our points, how much time we have left, we can’t lose, nor we can pause the game, and without these, our game couldn’t even be more pointless – so let’s implement these!

Building the Bottom Bar

React Native app game screen grid

As you can see, the bottom bar plays a main role in informing the user about their progress in the game: it shows their points, their time left, the bests they have done yet, and they can pause the game from there. Let’s build it already!

Let’s start with the container: create the style, name it as bottomContainer (or anything you’d like to), and to make sure that the grid and the bottom bar have the same width, let’s start with width: Dimensions.get(“window”).width * 0.875.

We want to make sure that our app looks nice on every screen size, thus we have to initialize flex as a method to make our layout responsible, too: let’s bring flex: 1 to the party. Last but not least, to display the views inside the container next to each other, give it a flexDirection: ‘row’ property. When finished, your stylesheet should look like this:

bottomContainer: {
   flex: 1,
   width: Dimensions.get("window").width * 0.875,
   flexDirection: 'row'
}

(Don’t forget to import Dimensions from react-native among with StyleSheets!)

Next off, add a <View>, pass it our bottomContainer stylesheet as a prop, and add 3 empty views with flex: 1 as a child.

They will contain the three main parts of the bar:

<View style={styles.bottomContainer}>
 <View style={{ flex: 1 }}>
 </View>
 <View style={{ flex: 1 }}>
 </View>
 <View style={{ flex: 1 }}>
 </View>
</View>

When you save and reload the app, you won’t really notice anything: that’s because there’s nothing in these containers yet! So let’s start with the Points and the Time counters, then move on to the “best yet” labels, and finish off with the pause button.

To make sure that both of the counters have the same style, create one style and re-use them on both the counters:

counterCount: {
  fontFamily: 'dogbyte',
  textAlign: 'center',
  color: '#eee',
  fontSize: 50
},
counterLabel: {
  fontFamily: 'dogbyte',
  textAlign: 'center',
  color: '#bbb',
  fontSize: 20
}

You can simply add some <Text>s to display the values that are in the state:

<View style={{ flex: 1 }}>
 <Text style={styles.counterCount}>
   {this.state.points}
 </Text>
 <Text style={styles.counterLabel}>
   points
 </Text>
</View>
(...)
<View style={{ flex: 1 }}>
 <Text style={styles.counterCount}>
   {this.state.timeLeft}
 </Text>
 <Text style={styles.counterLabel}>
   seconds left
 </Text>
</View>

If you save and refresh your app, you’ll see something like this:

basic bottom bar with text

If you wait for long enough without touching the correct tile, you’ll see the timer going negative. We’ll fix that later. Let’s continue with the “best yet” labels. You can build them yourself, but if you get stuck, continue reading the guide. (If you think you created a suitable solution, feel free to skip to the “Pausing the game” section)

First, create a container that will have the icon and the text as children in it:

bestContainer: {
  marginTop: 10,
  flexDirection: 'row',
  justifyContent: 'center'
}

This snippet will look differently in the current state of the git repo as I added this after initially writing the article – but you can check the correct code on the latest tree, too.

The styling is pretty self-explanatory there. The icon and the label also have a really straightforward styling:

bestIcon: {
  width: 25,
  height: 25,
  marginRight: 5
},
bestLabel: {
  fontFamily: 'dogbyte',
  color: '#bbb',
  fontSize: 25,
  marginTop: 2.5,
}

We can use hardcoded values for now as the value of the <Text> – we’ll make these display the correct value later in the “Persisting data” section.

You can import the trophy icon from the assets as you can see below. (For the “longest time” counter, the icons/clock.png is the suitable icon.)

<View style={styles.bestContainer}>
    <Image source={require('../../assets/icons/trophy.png')} style={styles.bestIcon} />
    <Text style={styles.bestLabel}>
       0
    </Text>
</View>

And if you reload the app, you’ll see the bottom bar with the icons:

bottom bar with high scores and icons

Making the Layout a Bit more Flexible

If you are not developing on an iPhone X/s/r, you may have already noticed that this layout looks weird.

When defining the bottomContainer style, I already mentioned making our app responsible and flexible. We’ll also cover this in a later section of the course, but the layout needs a fix ASAP.

You can simply fix it by adding a container for the Header with the style properties flex: 1, justifyContent: "center", then wrapping the grid in a supplementary/spacing buffer container, too, with the style properties flex: 5, justifyContent: "center". Then, add a container for the bottom bar and give it flex: 2 as a property. Inside the bottom bar, you have three views with only the flex: 1 property as a style.

Outsource the styles from the bottom bar section’s container to the styles.js as it follows:

bottomSectionContainer: {
 flex: 1,
 marginTop: 'auto',
 marginBottom: 'auto'
},

This will vertically center the views inside the container and make sure that they fill up their space. Then, use this style in all three views. The game screen’s layout will now look great on all devices.

This whole “Making the layout a bit more flexible” section will be missing from the current state of the git repo as I added this after initially writing the article –

but you can check the code on the latest tree if you need to.

Pausing our React-Native Game

Before just adding the pause button icon to a TouchableOpacity, we need to think about a way to prevent the player from cheating with the pause feature: if we just pause the game without hiding the grid, the players can easily search for the differing tile, continue the game and repeat this for the end of the time.

So instead, we should spice things up with some conditions. For the initial state, add another property:

gameState: 'INGAME' // three possible states: 'INGAME', 'PAUSED' and 'LOST'

Then, inside the componentWillMount()’s setInterval, add a ternary operator to make sure that the timer doesn’t get modified while the game is paused:

this.state.gameState === 'INGAME' && this.setState({ timeLeft: this.state.timeLeft - 1 });

Then wrap the grid generator in a ternary operator, too – so that we can hide the grid when the game is paused.

{gameState === 'INGAME' ? ? Array(size) (...)
 )) : (
    <View style={styles.pausedContainer}>
       <Image
         source={require("../../assets/icons/mug.png")}
         style={styles.pausedIcon}
       />
      <Text style={styles.pausedText}>COVFEFE BREAK</Text>
    </View>
 )
}

Oh, and create the styles needed for the pause-related elements, too:

pausedContainer: {
  flex: 1,
  alignItems: 'center',
  justifyContent: 'center'
},
pausedText: {
  fontFamily: 'dogbyte',
  textAlign: 'center',
  color: '#eee',
  marginTop: 20,
  fontSize: 60,
},
pausedIcon: {
  width: 80,
  height: 80
}

And finally, add the pause/play/replay button to the bottom bar. To decide which icon we need to import, I used a ternary operator, but you can use if statements if that’s what you prefer:

const bottomIcon =
     gameState === "INGAME"
       ? require("../../assets/icons/pause.png")
       : gameState === "PAUSED"
       	? require("../../assets/icons/play.png")
      : require("../../assets/icons/replay.png");

And in the JSX, add the code below to the second child of the bottomContainer. This code uses the bottomIcon constant we declared the previous snippet as a source, and uses the bottomIcon style from the stylesheet. Keep an eye on not mixing them up!

<TouchableOpacity style={{ alignItems: 'center' }} onPress={this.onBottomBarPress}>
<Image source={bottomIcon} style={styles.bottomIcon} />
</TouchableOpacity>

The bottomIcon style is just a simple width: 50; height: 50, I think you can do it yourself by now.

Now, let’s add the event handler for the bottom button:

onBottomBarPress = async () => {
   switch(this.state.gameState) {
     case 'INGAME': {
       this.setState({ gameState: 'PAUSED' });
       break;
     }
     case 'PAUSED': {
       this.setState({ gameState: 'INGAME' });
       break;
     }
     case 'LOST': {
       await this.setState({ points: 0, timeLeft: 15, size: 2 });
       this.generateNewRound();
       this.setState({
         gameState: "INGAME",
       })
       break;
     }
   }
 };

This will pause the game if you are in a game, resume the game if you paused, and restart the game if you have lost.

(The losing case may seem a bit odd: first, I reset the state to the original except the gameState so that the grid does not get rendered yet. I need to await the setState to make sure that the generation happens only with the new state – if not, the game could accidentally create a winner tile out of the grid (with a coordinate like [3, 2] on a 2-by-2 grid) and you’d have no possibility but to lose. After the new round’s data is generated, the gameState is updated so that you can see the grid again.)

If you save and reload the app, you’ll see the icon, and if you tap on it, you’ll be able to pause the game and resume it – but you still can’t lose yet, so let’s implement that.

Inside the componentWillMount(), add an if operator that will decide whether you used or not:

   this.interval = setInterval(() => {
     if (this.state.gameState === "INGAME") {
       if (this.state.timeLeft <= 0) {
         this.setState({ gameState: "LOST" });
       } else {
         this.setState({ timeLeft: this.state.timeLeft - 1 });
       }
     }
 }, 1000);

And since you can lose, you need a screen for that, too. You can add another ternary to achieve this:

gameState === "INGAME" ? (
   Array(size) (...)
) : gameState === "PAUSED" ? (
   <View style={styles.pausedContainer}>
       <Image
         source={require("../../assets/icons/mug.png")}
         style={styles.pausedIcon}
       />
          <Text style={styles.pausedText}>COVFEFE BREAK</Text>
       </View>
) : (
   <View style={styles.pausedContainer}>
       <Image
          source={require("../../assets/icons/dead.png")}
          style={styles.pausedIcon}
       />
       <Text style={styles.pausedText}>U DED</Text>
   </View>
)

Please keep in mind that using a lot of ternary operators may make your code unreadable, and you should try to avoid overusing them.

If you run the game now, you can properly pause, continue, lose and replay the game. Nice job! ??

Just one more thing before finishing with this section: you can’t quit to the main menu! *

(I noticed this after initially writing this article, so in the current state of the GitHub repo tree, you are not going to be able to see it – but you be able to see it on the latest tree, though)*

Let’s just add a simple button to fix that:

) : (
 <View style={styles.pausedContainer}>
   {gameState === "PAUSED" ? (
     <Fragment>
       <Image
         source={require("../../assets/icons/mug.png")}
         style={styles.pausedIcon}
       />
       <Text style={styles.pausedText}>COVFEFE BREAK</Text>
     </Fragment>
   ) : (
     <Fragment>
       <Image
         source={require("../../assets/icons/dead.png")}
         style={styles.pausedIcon}
       />
       <Text style={styles.pausedText}>U DED</Text>
     </Fragment>
   )}
   <TouchableOpacity onPress={this.onExitPress}>
     <Image
       source={require("../../assets/icons/escape.png")}
       style={styles.exitIcon}
     />
   </TouchableOpacity>
 </View>
)}

Notice how I didn’t copy-paste the TouchableOpacity again and again: since we need to show it on all screens, we can prevent copy-pasting by adding a container with the pausedContainer style on, then changing the original Views with the pausedContainer style to Fragments.

If using Fragments is a new concept for you (it may be because it’s a relatively new concept in React), be sure to check out the docs. In a nutshell, you can wrap your components with them without adding a new div to the DOM.

Let’s add the event handler for the exit button, too:

onExitPress = () => {
 this.props.navigation.goBack();
};

Now we’re all set. You can go back to the home screen with a shiny new button.

If you want to check out the code that’s finished as of now in one piece, here’s the GitHub repo.

You totally deserve a pat on the back for making it this far in the series, so kudos on that! ? In the next section, we’ll continue with animations, music, and SFX.

Read the next part of the tutorial series here: React-Native Sound & Animation Tutorial

Share this post

Twitter
Facebook
LinkedIn
Reddit