Making a simple React Native app

So I decided to learn React Native while making an app that would come in handy for me.

See, I like to keep a little checklist on my phone of all the different items that I need to take with me to work, so I don’t have to think every morning. I use Google Keep for this, and while it works fine, I want it to reset as soon as I’ve checked all the items, so all the items are automatically unchecked for the next day. So I decided to make an app!

I’m using React Native for this because I’ve been meaning to learn React Native for a while now, and what better way to learn than to solve your own little problems?

Let’s begin!

Hello, world!

Let’s start by installing expo-cli as it’s the easiest way to get started.

npm install -g expo-cli

Now we’ll initialize our app itself.

$ expo init DailyChecklist
? Choose a template: 
  ----- Managed workflow -----
❯ blank         minimal dependencies to run and an empty root component  
  tabs          several example screens and tabs using react-navigation 
  ----- Bare workflow -----
  bare-minimum  minimal setup for using unimodules 

Let’s choose blank template, and name our app. I’ll call it “Daily checklist”.

To keep things simple, I’ll try to stick to core React Native and avoid using Redux or anything like that for now.

Let’s start the app.

npm start

So now we have a couple of options as to how to run and “hotload” the app from the computer to the device. In decreasing order of speed: if you have a USB cable available with an Android device, you can choose the “Local” option. If your phone and computer are both connected to the same WiFi or LAN, choose LAN. Otherwise, choose Tunnel. I chose the “Local” option since it’s the fastest. You should see something like below:

Let’s make a few changes in our App.js file.

...
  render() {
    return (
      <View style={styles.container}>
        <Text>Hello React Native!</Text>
      </View>
    );
  }
...

If everything went well, the app on your phone should update automatically. This is called “hot reloading”, and it’s one of the neat things I like about React Native.

It works! Not every interesting, though.

It works! Not very interesting, though. Let’s get ourselves some checkboxes.

Adding checkboxes

Now the CheckBox component offered with React Native works only on Android, but that’s fine in my case because I’m only working on the Android app for now. It shouldn’t be too hard to swap it out with a cross-platform component if needs be.

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

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <View style={{ flexDirection: "row" }}>
          <CheckBox />
          <Text style={{ marginTop: "auto", marginBottom: "auto" }}>Hello</Text>
        </View>
      </View>
    );
  }
}

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

So we have a check box! Now let’s make that into a reusable component, and then render a bunch of these.

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

const Item = ({ checked, value }) => (
  <View style={{ flexDirection: "row" }}>
    <CheckBox value={checked} />
    <Text style={{ marginTop: "auto", marginBottom: "auto" }}>{value}</Text>
  </View>
);

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Item checked={true} value="Item 1" />
        <Item checked={false} value="Item 2" />
        <Item checked={true} value="Item 3" />
      </View>
    );
  }
}

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

That’s great, but right now you can’t uncheck a box once checked, and vice versa. This is because we need to update CheckBox‘s value ourselves, using the callback we provide to onValueChange.

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

const Item = ({ checked, onValueChange, value }) => (
  <View style={{ flexDirection: "row" }}>
    <CheckBox
      value={checked}
      onValueChange={onValueChange}
    />
    <Text style={{ marginTop: "auto", marginBottom: "auto" }}>{value}</Text>
  </View>
);

export default class App extends React.Component {
  state = {
    items: [
      { value: "Phone charger", checked: true },
      { value: "Notebook", checked: false },
      { value: "Pen", checked: false },
    ],
  };

  checkItem = index => {
    this.setState(({ items }) => ({
      items: items.map((item, currentIndex) => {
        if (index === currentIndex) {
          return { ...item, checked: !item.checked };
        }
        return item;
      }),
    }));
  };

  render() {
    return (
      <View style={styles.container}>
        {this.state.items.map(({ checked, value }, index) => (
          <Item
            key={index}
            checked={checked}
            value={value}
            onValueChange={() => this.checkItem(index)}
          />
        ))}
      </View>
    );
  }
}

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

I added a new function called checkItem that checks or unchecks an item based on its index. To do this, I use React’s setState with callback. Then I passed it as a callback function to Item‘s onValueChange prop.

So our checkboxes work, that’s great! But we can’t add any items of our own yet. Let’s fix that.

Adding new items

Let’s add a TextInput component for, well, text input. And let’s store whatever the user types in this field in a state variable. We’ll also add a button. Pressing this button will add the new item to our list.

First, let’s add the state variable to store user input:

...
export default class App extends React.Component {
  state = {
    items = [],
    newItemText: '',
  }
...

Now we’ll add the functions to add items. handleNewItemTextChange is to store the input of TextInput in state. handleAddButtonPress gets called when user clicks the “Add” button, which we also defined below.

  handleNewItemTextChange = newItemText => {
    this.setState({ newItemText });
  };

  handleAddButtonPress = e => {
    const newItemText = this.state.newItemText.trim();
    if (newItemText) {
      this.addItem(newItemText);
      this.setState({ newItemText: '' });
    }
  }
...
  <TextInput
    style={styles.inputText}
    placeholder="Add new item..."
    value={this.state.newItemText}
    onChangeText={this.handleNewItemTextChange}
    onSubmitEditing={this.handleAddButtonPress}
    blurOnSubmit={false}
  />
  <Button
    style={styles.addButton}
    onPress={this.handleAddButtonPress}
    title="Add"
  />

We use onSubmitEditing to make sure that the soft keyboard’s “Go” action also works for adding the new item. It’s the button you usually see at the bottom right corner of soft keyboards on Android.

Great! We can check items, uncheck them and even add new ones. Now let’s add the feature to delete items.

Deleting items

To keep things simple, we’ll ask user for confirmation on long press of the item text. User can then either confirm deletion, or cancel.

First, let’s define the functions to delete the item, and to confirm the deletion with an alert dialog.

  deleteItem = index => {
    console.log('Deleting item of index ', index);
    this.setState(({ items }) => ({
      items: items.filter((item, _index) => index !== _index),
    }));
  };

  confirmDeleteItem = index => {
    console.log('confirming deletion of item of index', index);
    const itemText = this.state.items[index].value;
    Alert.alert(
      null,
      `Are you sure you want to delete item '${itemText}'?`,
      [
        { text: 'Cancel' },
        { text: 'Delete', onPress: () => this.deleteItem(index) },
      ],
      { cancelable: true },
    );
  };

Next, we’ll modify our Item component to accept the prop onLongPress.

const Item = ({ checked, onValueChange, value, onLongPress }) => (
  <View style={{ flexDirection: 'row' }}>
    <CheckBox value={checked} onValueChange={onValueChange} />
    <Text style={styles.itemText} onLongPress={onLongPress}>
      {value}
    </Text>
  </View>
);

Now we’ll define our long press handler function and pass it to our Item component.

  handleItemLongPress = index => {
    const itemText = this.state.items[index].value;
    Alert.alert(
      itemText,
      'Please select an action.',
      [
        { text: 'Cancel' },
        { text: 'Delete', onPress: () => this.confirmDeleteItem(index) },
      ],
      { cancelable: true },
    );
  };
...
<Item
  key={index}
  checked={checked}
  value={value}
  onValueChange={() => this.checkItem(index)}
  onLongPress={() => this.handleItemLongPress(index)}
/>
...

Neat! Now let’s add the feature to edit items.

Editing items

As before, let’s add the function to actually modify the state so that items can get edited.

  editItem = (index, value) => {
    this.setState(
      ({ items }) => ({
        items: items.map((item, currentIndex) => {
          if (index === currentIndex) {
            return { ...item, value };
          }
          return item;
        }),
      }),
      () => this.persistItems(),
    );
  };

As before, let’s add the function to actually modify the state so that items can get edited.

  editItem = (index, value) => {
    this.setState(
      ({ items }) => ({
        items: items.map((item, currentIndex) => {
          if (index === currentIndex) {
            return { ...item, value };
          }
          return item;
        }),
      }),
      () => this.persistItems(),
    );
  };

As before, let’s add the function to actually modify the state so that items can get edited.

  editItem = (index, value) => {
    this.setState(
      ({ items }) => ({
        items: items.map((item, currentIndex) => {
          if (index === currentIndex) {
            return { ...item, value };
          }
          return item;
        }),
      }),
      () => this.persistItems(),
    );
  };
const Item = ({ checked, onValueChange, value, onLongPress }) => (
  <View style={{ flexDirection: 'row' }}>
    <CheckBox value={checked} onValueChange={onValueChange} />
    <Text style={styles.itemText} onLongPress={onLongPress}>
      {value}
    </Text>
  </View>
);

Now we’ll define our long press handler function and pass it to our Item component.

  handleItemLongPress = index => {
    const itemText = this.state.items[index].value;
    Alert.alert(
      itemText,
      'Please select an action.',
      [
        { text: 'Cancel' },
        { text: 'Delete', onPress: () => this.confirmDeleteItem(index) },
      ],
      { cancelable: true },
    );
  };
...
<Item
  key={index}
  checked={checked}
  value={value}
  onValueChange={() => this.checkItem(index)}
  onLongPress={() => this.handleItemLongPress(index)}
/>
...

Neat! Now let’s add the feature to edit items.

Editing items

As before, let’s add the function to actually modify the state so that items can get edited.

  editItem = (index, value) => {
    this.setState(
      ({ items }) => ({
        items: items.map((item, currentIndex) => {
          if (index === currentIndex) {
            return { ...item, value };
          }
          return item;
        }),
      }),
      () => this.persistItems(),
    );
  }; 

It’s fairly simple. When called, it’ll edit the item specified by index to value.

Now let’s add a wrapper function around this that gets called when user clicks on the “Edit” button.

handleEditButtonPress = () => {
  this.editItem(this.state.editItemIndex, this.state.editItemText);
  this.setState({ confirmEditModalVisible: false });
};

Now to edit items, we need another input field, where we can type in the new value. To accommodate this input field, we’ll use a Modal component:

import { Modal } from 'react-native';
...
      <View style={styles.container}>
        <Modal
          animationType="slide"
          transparent={false}
          visible={this.state.confirmEditModalVisible}
          onRequestClose={() =>
            this.setState({ confirmEditModalVisible: false })
          }
        >
          <View>
            <TextInput
              style={styles.inputText}
              placeholder="Edit item..."
              value={this.state.editItemText}
              onChangeText={this.handleEditItemTextChange}
              onSubmitEditing={this.handleEditButtonPress}
              blurOnSubmit={false}
            />
            <Button
              style={styles.addButton}
              onPress={this.handleEditButtonPress}
              title="Save"
              disabled={!this.state.editItemText.trim()}
            />
          </View>
        </Modal>
        {this.state.items.map(({ checked, value }, index) => (
...

Here, we’ve added a new Modal component, with a View component inside of it. This View component contains the main UI of what the modal will contain. It has a TextInput and a Button.

We’ve added a bunch of new state variables and functions here. Let’s go ahead and define those.

...
  state = {
    ...
    editItemText: '',
    editItemIndex: null,
    confirmEditModalVisible: false,
  }

...

  handleEditItemTextChange = editItemText => {
    this.setState({ editItemText });
  };

...

editItemText contains the text entered in the TextInput field above. editItemIndex holds the index of the item being edited. confirmEditModalVisible indicates whether or not the modal is visible.

Let’s add another function actually show the modal. We’ll reuse our handleItemLongPress function for this.

  confirmEditItem = index => {
    const editItemText = this.state.items[index].value;
    this.setState({
      confirmEditModalVisible: true,
      editItemText,
      editItemIndex: index,
    });
  };

  handleItemLongPress = index => {
    const itemText = this.state.items[index].value;
    Alert.alert(
      itemText,
      'Please select an action.',
      [
        { text: 'Cancel' },
        { text: 'Delete', onPress: () => this.confirmDeleteItem(index) },
        { text: 'Edit', onPress: () => this.confirmEditItem(index) },
      ],
      { cancelable: true },
    );
  };

Now you should be able to edit items by long pressing an item and choosing “Edit”.

Source code

Hope this article was helpful. For reference, the completed source code of the app is available here.

URL State in Redux – An Introduction to Redux First Router

When you think of routing in a React-Redux app, the first thing that comes to mind is React Router. It’s the de facto standard routing library for React (though there are alternatives like Redux First Router, as we’ll see below). And probably for most use cases, it works great, even if your React app uses a Flux library like Redux. Even the official documentation recommends keeping the two separate. Redux is responsible for your state, React Router is responsible for your routes. Simple enough, right?

Things get a bit more interesting when you start out developing a React-Redux-only app with no routing involved, and then later you need to extract part of the state in your Redux store to the URL. Maybe you want certain bits of the states to be bookmarkable, or maybe you just want your user to be able to refresh and still remain on that same “page”, whatever that may mean for your React app. And this concept of “page” just so happens to be an integral part of your Redux state.

Now it’s still possible to use React Router in this case. The recommended approach is to remove the relevant state from your Redux store completely, then use <Route> components at the top level and pass down props from it to your components.

There are a couple of problems with this approach:

  1. You need to rewrite some of your Redux selectors to accept props as input arguments
  2. Your <Link>s have to be mindful about your routing patterns (was it /page/1 or /pages/1?)
  3. You need to pass down props from the root <Route> component through potentially several components, which is a lot less convenient than Redux’s connected components

Introducing Redux First Router

Redux First Router solves all three of these problems. It is a library that allows you to change URL routes by dispatching actions. Every route change is the result of an action dispatch. You create a routesMap where you map different action names to different URL routes and Redux First Router handles the rest.

In this article, we’ll start with a very simple React-Redux app. The app lets you choose a Game of Thrones character from a dropdown and view the character’s bio.

The app doesn’t use any routing. All the app state belongs only in the Redux store.

Our goal is to sync the currently selected character’s ID (name), which is part of the Redux state, with the URL using Redux First Router.

Start by cloning the initial code:

git clone git://github.com/smtchahal/got-characters-without-router.git
cd got-characters-starter
npm install
npm start

You should see something like the following:

Game of Thrones Characters selector. Go ahead, run the app, see it for yourself!

If you have Redux DevTools enabled on Chrome, you should be able to see how the state is structured and how it changes when you select different characters.

The app consists of two keys in the root level of the state.

  • characters is static data about the different Game of Thrones characters (using byId and allIds pattern)
  • selectedCharacter is the ID of the currently selected character in the dropdown

Every time the user selects a character from the dropdown, we dispatch an action.

Our goal here is to have selectedCharacter seamlessly sync with the URL.

First let’s install redux-first-router.

npm install --save redux-first-router

Next we need to define a routesMap. It’s an object that maps action names to URL routes. You can add multiple routes, each pointing to a different action but in this case one will do.

Create a new file, src/connectedRoutes.js as follows:

import { connectRoutes } from 'redux-first-router';

import { SELECT_CHARACTER } from './actionTypes';

const routesMap = {
  [SELECT_CHARACTER]: '/:characterId?',
};

export default connectRoutes(routesMap);

This tells redux-first-router to dispatch a SELECT_CHARACTER action every time the route /:characterId? changes and, conversely, to update route as described every time the action is dispatched. The ? makes the parameter optional. When not specified, characterId is null.

Now import this in src/index.js and modify the Redux store as follows:

...
import { createStore, compose, applyMiddleware } from 'redux';
import connectedRoutes from './connectedRoutes';

const { middleware, enhancer } = connectedRoutes;
const middlewares = applyMiddleware(middleware);
const composeEnhancers = compose || window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;

const store = createStore(rootReducer, composeEnhancers(enhancer, middleware));
...

You also need to update your rootReducer to include redux-first-router’s location reducer, as follows:

...
import connectedRoutes from './connectedRoutes';

const { reducer: location } = connectedRoutes;

export default combineReducers({ selectedCharacter, characters, location });

And voila! Run npm start again (make sure you Ctrl+C the previous process) and now the routes should change when you select a character.

Stop using your IDE’s drag-and-drop

Think about when you were a beginner at programming, when you were just starting out. Now if your main tool as a beginning programmer was a plain-old text editor like mine was, you probably enjoyed and might still appreciate a more hands-on approach to learning, where you know what’s happening under the hood. Maybe you later on graduated to using an IDE because you either had to (like with Android Studio), or because you found it saved time.

But if you started out learning with an IDE, there’s a decent chance that you used helpful IDE “features” like drag-and-drop, and later realized you had to learn to write raw XML to design layouts anyway.

Obviously this isn’t true for everyone so I don’t mean to generalize, but my point was to illustrate the issue I have with drag-and-drop: it’s a level of abstraction no programmer should strive for.

Generally speaking, I’d say the more abstracted things are, the better. No one writes web apps in assembly. But using drag-and-drop and ignoring generated code completely is generally a bad idea, at least in standard Android development. The drag-and-drop functionality in Android Studio isn’t mean to completely replace raw XML coding (that’s why you always have the option to switch to text), it’s meant to reduce the amount of typing you’d otherwise need to do to get views and view groups. It’s essentially like the “add new activity” wizard: it provides you with stub code that you’re supposed to edit to suit your particular needs, saving the need to write everything from scratch.

When you drag and drop things and ignore the code that gets generated, you’re relying on the IDE to format your code according to your coding conventions (you have those, right?) and any optimizations you may need to do, depending on how complex your layouts are. That, or you just don’t care about any of that stuff, which is Bad Practice™.

Don’t get me wrong, I have nothing against using drag-and-drop for the purpose it is supposed to serve, i.e. to save time. I see the value in dragging and dropping views or widgets and then customizing the code. Point is, you should be aware of what the code does, and relying solely on drag-and-drop cripples this awareness.

Now this isn’t true 100% of the time. There are cases where your IDE generates code that you’re not expected to look at. The most obvious example would be auto-generated code that’s temporary and regenerated on every build (e.g. files in /build and /app/build directories in Android Studio). Another example could be Django’s migrations, but those are still part of your project and you should be ready to get your hands dirty with them if needed.

As a general rule of thumb, anything that you don’t exclude from your version control system should be considered code that you should be willing to work on, and if that’s not the case, you need to ask yourself why.