Building the TaskList App in React Native

One of my favorite “demo” builds is the TaskList app. It is a simple app and is used quite often to “learn a new environment”. You can check out TodoMVC for several examples of web-based versions of this app, and my book for Xamarin editions of this app. It’s useful because you can start from something simple, and build it into a complete cloud-enabled app in a relatively short amount of time. I somewhat know React, but I don’t really know React Native, so I’m going to use this experience to learn the differences.

React still has a nasty patent claim clause in their license, so you should certainly seek legal advice before using React in a commercial project.

I started the application as I did before, using react-native init TaskList. Then, I replaced the contents of index.android.js and index.ios.js with the following:

import React, { Component } from 'react';
import { AppRegistry } from 'react-native';
import TaskList from './application';

AppRegistry.registerComponent('TaskList', () => TaskList);

This is nice and simple and defers all my code to the application directory, which doesn’t exist yet. The application/index.js file needs to expose a React component that boots the application. More on that in a moment. First, let me think about what I am going to do. In every other TaskList app, there is a list of items, rendered as something akin to a ListView object. That ListView object will take a template for a single item. For the actual TaskList page, I’m going to want to have two components – one that takes a list of items and renders it as a list (let’s called this TaskList), and a second component that renders a single item (let’s call this TaskListItem). It makes sense, then, that I work on rendering a single item first, then use that (once it is working) in producing the list. My application/index.js for testing the TaskListItem looks like this:

import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import TaskListItem from './components/TaskListItem';

class Application extends Component {
  render() {
    const item = { id: 'newitem', text: 'First Item', completed: false };

    return (
      <View style={styles.container}>
        <TaskListItem item={item}/>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    backgroundColor: '#F5FCFF',
    marginTop: 16
  }
});

export default Application;

It’s really only here for testing. I may expand on this to check out event handlers once I get to that.

Rendering the UI

Now, my TaskListItem needs to render an object with three items – I’d like to display the id although I wouldn’t normally. I want to display two rows – the text above the ID, and then a checkbox on the right-hand side. React Native has a View object in the default set that acts kind of like a flex-box in the Web CSS world. I can use this fact to produce the required layout. When I am generating these layouts, I am pathetically bad at working out the effects of flex-box, so I use colored margins to show me where the boxes are. Here is my first attempt at the application/components/TaskListItem.js component:

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

const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        borderBottomWidth: 1,
        borderColor: '#404040'
    },

    textcontainer: {
        flexGrow: 1,
        flexDirection: 'column',
        justifyContent: 'flex-start',
        alignItems: 'flex-start',
        borderWidth: 1,
        borderColor: 'yellow',
        margin: 2,
        padding: 4,
    },

    cbcontainer: {
        borderWidth: 1,
        borderColor: 'red',
        margin: 2
    },

    itemtext: {
        fontSize: 12,
        color: 'black'
    },

    idtext: {
        fontSize: 6,
        color: '#808080'
    }
});

class TaskListItem extends Component {
    constructor (props) {
        super(props);
    }

    get item () {
        return this.props.item;
    }

    render () {
        return (
            <View style={styles.container}>
                <View style={styles.textcontainer}>
                    <Text style={styles.itemtext}>{this.item.text}</Text>
                    <Text style={styles.idtext}>{this.item.id}</Text>
                </View>
                <View style={styles.cbcontainer}>
                    <Text>{this.item.completed}</Text>
                </View>
            </View>
        )
    }

}

TaskListItem.propTypes = {
    item: PropTypes.shape({
        id: PropTypes.string.isRequired,
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired
    }).isRequired
};

export default TaskListItem;

Actually, I exaggerated a little there. It’s my second attempt. My first attempt did not include the style sheet, but this, if you run it – sort of works:

There are two things that don’t work for me here, both dealing with the “completed” flag. Firstly, I want the text to be strike-thru when the item is completed. Secondly, I want a checkbox where the red box is at the right-hand side. Dealing with the strike-thru text is relatively easy. Replace the Text entry for the text with this:

                    <Text style={[styles.itemtext, this.item.completed && styles.completed]}>{this.item.text}</Text>

Then add a completed style as follows:

    completed: {
        textDecorationLine: 'line-through'
    }

To test, set the completed field of the item to true in application/index.js. The checkbox is a little harder, since there is no native Icon class within React Native. To implement the checkbox, I need to bring in an external library – react-native-vector-icons. This library uses standard web-based truetype fonts from a variety of sources and wraps them into an Icon class. I’m going to use the Ionicons set for this project. Start by adding the library to your project:

yarn add react-native-vector-icons

If you haven’t installed yarn (and you should), then you can also use npm install instead. Next, follow the instructions in the README to install the fonts in each of the iOS and Android projects. The directions are different for each platform and depend on the font-family you are using. The instructions are also likely to change as react-native plugins mature, so I’m not going to cover them here. For iOS, the process currently involves opening the React Native project in XCode, copying the Fonts directory and then adding the fonts to the Info.plist file. For Android, it’s similar. One notable thing here is that you MUST clean and re-compile the project before continuing. You can clean the project using Project > Clean from within XCode. For Android, follow the “with Gradle” instructions. For once, the Android instructions are easier than the iOS instructions.

On to the code. Within application/components/TaskListItem.js, add the following import:

import Icon from 'react-native-vector-icons/Ionicons';

Then replace the “ entry for the completed flag with the following:

<Icon name="ios-square-outline"/>

Run this, and you will get a major red box where you weren’t expecting one. There isn’t a major worry here. In the background, you are running the React packager and it has not noticed that you have added a new package. You need to kill off the old packager and restart it. If you are using Visual Studio Code, then the easiest way to do this is to quit out of Visual Studio Code and then restart it. If you are using a terminal to run react-native run-android, then you will want to kill off the process or extra window that React Native started, then re-run it. Once you do that, you will get a square in the red outline:

IonIcons has three sets of icons – one for Android and two different ones for iOS. To handle this situation, you can import the Platform section of react-native (just add it to the list of imports for react-native) and check that Platform.OS === 'ios' to determine which one to use. I’m going to use ios-checkbox / ios-square-outline on iOS and md-checkbox / md-square-outline on Android. To do this, I’m going to add an icon property to my component, then use it in the render() method:

    get icon () {
        const platform = Platform.OS === 'ios' ? 'ios' : 'md';
        const iconName = this.item.completed ? `${platform}-checkbox` : `${platform}-square-outline`;
        const iconColor = this.item.completed ? 'green' : 'black';
        return <Icon name={iconName} color={iconColor} size={20} style={{marginRight: 8}} />;
    }

    render () {
        return (
            <View style={styles.container}>
                <View style={styles.textcontainer}>
                    <Text style={[styles.itemtext, this.item.completed && styles.completed]}>{this.item.text}</Text>
                    <Text style={styles.idtext}>{this.item.id}</Text>
                </View>
                <View style={styles.cbcontainer}>
                    {this.icon}
                </View>
            </View>
        );
    }

Once I remove the borders and margins from my stylesheet, I have my final UI rendering in two forms – completed and not completed:


Event Handlers

This component is not yet interactive, and I need to think about what I want to be able to do. This is the list I came up with:

  • Swipe-right to delete the item.
  • Press on the checkbox to set or clear the checkbox.
  • Press on the text to edit the item.

I can deal with the latter two within the TaskListItem component. The Swipe-right action has to be dealt with at the list level (more on that later). To deal with the two press events, add two event handlers to the PropTypes:

TaskListItem.propTypes = {
    item: PropTypes.shape({
        id: PropTypes.string.isRequired,
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired
    }).isRequired,
    onCompleteItem: PropTypes.func,
    onSelectItem: PropTypes.func
};

TaskListItem.defaultProps = {
    onCompleteItem: null,
    onSelectItem: null
};

I also need to add some event handlers that wrap these properties so that they don’t get called when not set:

    onCompleteItem (e) {
        e.preventDefault();
        if (this.props.onCompleteItem !== null) {
            this.props.onCompleteItem(this.item.id, !this.item.completed);
        }
    }

    onSelectItem (e) {
        e.preventDefault();
        if (this.props.onSelectItem !== null) {
            this.props.onSelectItem(this.item.id);
        }
    }

Wiring up the onPress() type events (selecting and completing an item) means wrapping the appropriate view in a TouchableHighlight (part of the react-native package) like this:

    render () {
        return (
            <View style={styles.container}>
                <View style={styles.textcontainer}>
                    <TouchableHighlight onPress={(e) => this.onSelectItem(e)}>
                        <Text style={[styles.itemtext, this.item.completed && styles.completed]}>{this.item.text}</Text>
                    </TouchableHighlight>
                    <Text style={styles.idtext}>{this.item.id}</Text>
                </View>
                <View style={styles.cbcontainer}>
                    <TouchableHighlight onPress={(e) => this.onCompleteItem(e)}>{this.icon}</TouchableHighlight>
                </View>
            </View>
        );

Note where I have placed the TouchableHighlight elements. A TouchableHighlight element must have exactly one child element – the thing you clicked on. I’ve spent a while adjusting my flex-box stylesheet to perfectly deal with the rendering of my component and I don’t want the TouchableHighlight to mess with that. If you want to place the “ outside the view, then you will have to adjust the style sheet to compensate. I can adjust the testing application/index.js to handle the new events:

class Application extends Component {
  onCompleteItem (id, flag) {
    Alert.alert(
      'onCompleteItem',
      `id=${id} flag=${flag}`,
      [
        { text: 'OK', onPress: () => console.log('OK')}
      ],
      { cancelable: false }
    );
  }

  onSelectItem (id) {
    Alert.alert(
      'onSelectItem',
      `id=${id}`,
      [
        { text: 'OK', onPress: () => console.log('OK')}
      ],
      { cancelable: false }
    );
  }

  render() {
    const item = { id: 'newitem', text: 'First Item', completed: false };

    return (
      <View style={styles.container}>
        <TaskListItem
          item={item}
          onCompleteItem={(id, flag) => this.onCompleteItem(id, flag)}
          onSelectItem={(id) => this.onSelectItem(id)} />
      </View>
    );
  }
}

This pops up a new alert message when I press on the appropriate bits.

That has pretty much wrapped up what I need for the TaskListItem component. Make sure you thoroughly test the component before continuing. I could automate some of the testing, but this is such an easy project that human-interaction testing is good enough. Make sure that the component is tested on both Android and iOS before continuing.

It might be a good idea to check in your code at this point.

Wrapping in a TaskList

Now I need to wrap it in a ListView. A ListView presents, well, a list of stuff. I need to provide the code to render each row (which is my TaskListItem component). My first step is to create a new component called application/components/TaskList.js:

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

const styles = StyleSheet.create({
    container: {
        alignSelf: 'stretch'
    }
});

class TaskList extends Component {
    constructor (props) {
        super(props);
    }

    render () {
        return (
          <View style={styles.container}>
            <Text>In TaskList</Text>
          </View>
        );
    }
}

TaskList.propTypes = {
    items: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.string.isRequired,
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired
    })).isRequired
};

export default TaskList;

I also need to adjust my test application/index.js to test the new component:

import React, { Component } from 'react';
import { Alert, StyleSheet, View } from 'react-native';
import TaskList from './components/TaskList';

class Application extends Component {
  render() {
    const items = [
      { id: '1', text: 'First Item', completed: false },
      { id: '2', text: 'Second Item', completed: true }
    ];

    return (
      <View style={styles.container}>
        <TaskList items={items} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    backgroundColor: '#F5FCFF',
    marginTop: 22
  }
});

export default Application;

Running this should display ‘In TaskList’ as text at the top of the page. Now, let’s integrate the ListView. First, I need to create a data source, then populate a clone of the data source with the items that are passed in via props:

import React, { Component, PropTypes } from 'react';
import { ListView, StyleSheet, View } from 'react-native';
import TaskListItem from './TaskListItem';

const styles = StyleSheet.create({
    container: {
        alignSelf: 'stretch'
    }
});

class TaskList extends Component {
    constructor (props) {
        super(props);

        this.dataSource = new ListView.DataSource({
            rowHasChanged: (r1, r2) => r1 !== r2
        });

        this.rows = this.props.items.map(item => {
           return { id: item.id, text: item.text, completed: item.completed };
        });

        this.state = {
            dataSource: this.dataSource.cloneWithRows(this.rows)
        };
    }

    renderRow (item, sectionID, rowID) {
        return (<TaskListItem item={item}/>);
    }

    render () {
        return (
            <View style={styles.container}>
                <ListView dataSource={this.state.dataSource}
                    renderRow={(item, sid, rid) => this.renderRow(item, sid, rid)} />
            </View>
        );
    }
}

TaskList.propTypes = {
    items: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.string.isRequired,
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired
    })).isRequired
};

export default TaskList;

The funny code within the constructor makes a copy of the rows so I can adjust them – adding new fields to them, for example, which I am likely to need later. The actual data source is stored in the component state. The result:

This looks pretty reasonable to me.

Handling Events

Each row produces two events – the onSelectItem and onCompleteItem. I can copy the Alert code from the original testing into the TaskList item for right now and deal with those later. However, I also wanted to handle the swipe-right functionality. This is best done at the list level by wrapping the element for the rendered row into a Swipeout element. The Swipeout element comes from the react-native-swipeout library. There are other libraries that do similar things. For example, check out react-native-swipe-list-view.

To start, I need to add an ‘active’ field to each row. This will determine whether the “swiped” element is visible or not. I can do this by adjusting the copy mechanism I use in the constructor:

        this.rows = this.props.items.map(item => {
            return {
                id: item.id,
                text: item.text,
                completed: item.completed,
                active: false
            };
        });

I’ve added an event handler called onDeleteItem() that looks exactly like the other event handlers for right now. Now for the swipeout code. Firstly, I need an event handler when a user swipes left. This sets the active element on the row, then updates the state with the new data source:

    onSwipeOut (sectionID, rowID) {
        for (let i = 0 ; i < this.rows.length ; i++) {
            this.rows[i].active = (i.toString() === rowID);
        }
        this.setState({ dataSource: this.state.dataSource.cloneWithRows(this.rows) });
    }

I also need to update the renderRow() method to wrap the TaskListItem component in a Swipeout component:

    renderRow (item, sectionID, rowID) {
        const buttons = [
            {
                text: 'Delete',
                backgroundColor: '#FF0000',
                onPress: () => this.onDeleteItem(item)
            }
        ];
        return (
            <Swipeout
                right={buttons}
                rowID={rowID}
                close={!item.active}
                onOpen={(sid, rid) => this.onSwipeOut(sid, rid)}>
                <TaskListItem
                    item={item}
                    onCompleteItem={(id,flag) => this.onCompleteItem(id, flag)}
                    onSelectItem={(id) => this.onSelectItem(id)} />
            </Swipeout>
        );
    }

With this code in place, you should be able to click the text and completed box, plus be able to swipe left then press Delete to get an appropriate dialog box that indicates the right thing happened.

Adding a new item

I don’t want to handle just existing items. I also want to add new items. To do that, I need a Floating Action Button, also known as a FAB, or just plain Action Button. If you have ever seen the icon in the circle at the bottom-right of the screen, then you have seen a FAB. Again, this isn’t out of the box functionality for React Native. (To be fair, it isn’t out of the box functionality for Xamarin either!). However, there is a handy package called react-native-action-button that does the job and it is fairly simple to integrate.

The “New Item” functionality is not part of the task list. I’ve integrated it into the application/index.js file as follows:

import React, { Component } from 'react';
import { Alert, StyleSheet, View } from 'react-native';
import ActionButton from 'react-native-action-button';
import TaskList from './components/TaskList';

class Application extends Component {
  onNewItemPressed () {
    Alert.alert(
      'onNewItemPressed',
      `New Item Selected`,
      [
          { text: 'OK', onPress: () => console.log('OK Pressed') }
      ],
      { cancelable: false }
    );
  }

  render() {
    const items = [
      { id: '1', text: 'First Item', completed: false },
      { id: '2', text: 'Second Item', completed: true }
    ];

    return (
      <View style={styles.container}>
        <TaskList items={items} />
        <ActionButton
          buttonColor='#9b59b6'
          onPress={() => this.onNewItemPressed()}/>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    backgroundColor: '#F5FCFF',
    marginTop: 22
  }
});

export default Application;

Line 3 brings in the library. Lines 7-16 are the event handler that is called when the user presses the new item button. Lines 27-29 are the JSX code for using the action button. I like the react-native-action-button over others because it also supports action menus – something that is not required on this application, but good to use for other applications should I need to.

You know what would be really nice here? I click on the Add New Item button and a new item form slides up. To do that, I need a Modal form – part of the react-native base package. Here is a nice application/components/AddNewItem.js component:

import React, { Component, PropTypes } from 'react';
import { Button, StyleSheet, Switch, Text, TextInput, View } from 'react-native';
import * as uuid from 'uuid';

const styles = StyleSheet.create({
    container: {
        marginTop: 30,
        flexDirection: 'column',
        justifyContent: 'flex-start'
    },
    textinput: {
        height: 40,
        borderWidth: 1,
        borderColor: '#CCCCCC',
        margin: 2,
        paddingLeft: 4,
        paddingRight: 4,
        paddingTop: 0,
        paddingBottom: 0
    },
    completed: {
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center'
    },
    completedtext: {
        fontSize: 16
    },
    buttons: {
        flexDirection: 'row',
        justifyContent: 'space-around'
    },
    title: {
        fontSize: 24
    }
});

class AddNewItem extends Component {
    constructor (props) {
        super(props);

        this.state = {
            text: '',
            completed: false
        };
    }

    onNewItem (e) {
        e.preventDefault();
        if (this.props.onNewItem !== null) {
            this.props.onNewItem({ id: uuid.v4(), text: this.state.text, completed: this.state.completed });
        }
    }

    onCancel (e) {
        e.preventDefault();
        if (this.props.onCancel !== null) {
            this.props.onCancel();
        }
    }

    render () {
        return (
            <View style={styles.container}>
                <Text style={styles.title}>Add New Item</Text>
                <TextInput
                    onChangeText={(text) => this.setState({ text: text })}
                    placeholder="Enter Task"
                    autoCapitalize="sentences"
                    autoCorrect={true}
                    autoFocus={true}
                    keyboardType="default"
                    maxLength={200}
                    value={this.state.text}
                    style={styles.textinput} />
                <View style={styles.completed}>
                    <Text style={styles.completedtext}>Completed?</Text>
                    <Switch
                        onTintColor='green'
                        onValueChange={(value) => this.setState({ completed: value })}
                        value={this.state.completed} />
                </View>
                <View style={styles.buttons}>
                    <Button
                        title="OK"
                        onPress={(e) => this.onNewItem(e)}/>
                    <Button
                        color='#888888'
                        title="Cancel"
                        onPress={(e) => this.onCancel(e)}/>
                </View>
            </View>
        );
    }
}

AddNewItem.propTypes = {
    onNewItem: PropTypes.func,
    onCancel: PropTypes.func
};

AddNewItem.defaultProps = {
    onNewItem: null,
    onCancel: null
};

export default AddNewItem;

This is fairly straight forward, as far as components go. I fill in the information, click on OK and an event is fired. I can also click on Cancel and a different event is fired. To wire this up, I am going to move the array of items in application/index.js to the component state, then wire up a few elements in render():

import React, { Component } from 'react';
import { Alert, Modal, StyleSheet, View } from 'react-native';
import ActionButton from 'react-native-action-button';
import AddNewItem from './components/AddNewItem';
import TaskList from './components/TaskList';

class Application extends Component {
  constructor (props) {
    super(props);

    this.state = {
      items: [
        { id: '1', text: 'First Item', completed: false },
        { id: '2', text: 'Second Item', completed: true }
      ],
      modalVisible: false
    }
  }

  onNewItem (item) {
    this.setState({
      modalVisible: false,
      items: [ ...this.state.items, item ]
    });
    console.log(`Added item ${JSON.stringify(item)}`);
  }

  render() {
    return (
      <View style={styles.container}>
        <Modal animationType="slide" transparent={false} visible={this.state.modalVisible} onRequestClose={() => this.setState({ modalVisible: false })}>
          <AddNewItem onNewItem={(item) => this.onNewItem(item)} onCancel={() => this.setState({ modalVisible: false })} />
        </Modal>
        <TaskList items={this.state.items} />
        <ActionButton
          buttonColor="#9b59b6"
          onPress={() => this.setState({ modalVisible: true })}/>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    backgroundColor: '#F5FCFF',
    marginTop: 22
  }
});

export default Application;

This should work, right?

Ok – there is a bug

If you run this code, then you will note that the modal comes up and you can fill in the form, but the effect is not seen in the UI. Worse yet, if you set a break point on the onNewItem() method in application/index.js, you will note that the state after the setState indicates that the state is set correctly. I have to admit that I scratched my head for a few hours on this one. The problem turned out to be in my TaskList component. I’d done a foolish thing in the constructor – I’d taken a copy of the rows at the point that the component was instantiated, rather than when the component was mounted. This was a major misunderstanding about the lifecycle of a component – something that you will need to get to grips with when you write React Native apps. Specifically – props are read only. Don’t change them! It’s also indicative of a substantial problem when debugging in React. Because you have a heirarchy of components, the bug can be anywhere in the component tree, which effectively increases the surface area that you have to delve into when dealing with bugs.

Now I know where the bug is, how do I fix it? Well, it’s a multi-step process. When a component is given new props, the componentWillReceiveProps() method is called with thew new props. I can use this to set up the new state. However, I also need to move the rows into the state. The major disruption is in the TaskList component:

    constructor (props) {
        super(props);

        this.dataSource = new ListView.DataSource({
            rowHasChanged: (r1, r2) => r1 !== r2
        });
        let rows = this.propsToRows(this.props);
        this.state = {
            rows: rows,
            dataSource: this.dataSource.cloneWithRows(rows)
        };
    }

    propsToRows (props) {
        return props.items.map(item => {
            return {
                id: item.id,
                text: item.text,
                completed: item.completed,
                active: false
            };
        });
    }

    componentWillReceiveProps (props) {
        let rows = this.propsToRows(props);
        this.setState({
            rows: rows,
            dataSource: this.dataSource.cloneWithRows(rows)
        });
    }

    onSwipeOut (sectionID, rowID) {
        let rows = this.propsToRows(this.props);
        let i = parseInt(rowID);
        if (i >= 0) {
            rows[i].active = true;
        }
        this.setState({
            rows: rows,
            dataSource: this.state.dataSource.cloneWithRows(rows)
        });
    }

Then, whenever I used this.rows, I changed it to this.state.rows. That solves that problem.

Back to wiring up the app

Now that I’ve got that bug out of the way, I can move my attention back to wiring up the rest of the event handlers. the “source of truth” for the entire app is at the top level. I could go all beserk and wire up redux (and I may do that in a future blog post). For right now, I can cascade the event handlers. That means that TaskList needs three new event handlers: one each for deletion, selection and toggling the completion flag. In the application/index.js component, I’ve added the following:

  onDeleteItem (id) {
    this.setState({
      items: this.state.items.filter(item => item.id !== id)
    });
  }

To wire up the event handler, I’ve adjusted the TaskList component definition in the render() method:

  render() {
    return (
      <View style={styles.container}>
        <Modal animationType="slide" transparent={false} visible={this.state.modalVisible} onRequestClose={() => this.setState({ modalVisible: false })}>
          <AddNewItem onNewItem={(item) => this.onNewItem(item)} onCancel={() => this.setState({ modalVisible: false })} />
        </Modal>
        <TaskList
          items={this.state.items}
          onDeleteItem={(id) => this.onDeleteItem(id)} />
        <ActionButton
          buttonColor="#9b59b6"
          onPress={() => this.setState({ modalVisible: true })} />
      </View>
    );
  }
}

Then, in components/application/TaskList.js, I’ve adjusted the onDeleteItem() method:

    onDeleteItem (item) {
        if (this.props.onDeleteItem !== null) {
            this.props.onDeleteItem(item.id);
        }
        this.onSwipeOut ("-1", "-1");
    }

I’ve also added the onDeleteItem property to the propTypes and defaultProps for the TaskList component. It’s a similar story with the onCompleteItem event handler. There are changes in the TaskList item to bubble the event upwards, then in application/index.js, I’ve added the following event handler method:

  onCompleteItem (id, flag) {
    this.setState({
      items: this.state.items.filter(item => {
        if (item.id === id) {
          item.completed = flag;
        }
        return item;
      })
    });
  }

For the “selectItem” event, I wanted a modal that was just like the AddNewItem modal, but allowed the user to edit the text. Rather than reinvent the component, I re-used the AddNewItem component (renaming it to “ViewItem” along the way). The changes to the TaskList component are almost identical to the changes for the onCompleteItem event handler – I just bubble the event up the chain if there is an event handler defined. So, let’s look at the changes to the application/index.js component. First, the render() method:

  render() {
    return (
      <View style={styles.container}>
        <Modal animationType="slide" transparent={false} visible={this.state.modalVisible} onRequestClose={() => this.setState({ modalVisible: false })}>
          <ViewItem item={this.state.modalItem} onSaveItem={(item) => this.onSaveItem(item)} onCancel={() => this.setState({ modalVisible: false })} />
        </Modal>
        <TaskList
          items={this.state.items}
          onCompleteItem={(id, flag) => this.onCompleteItem(id, flag)}
          onDeleteItem={(id) => this.onDeleteItem(id)} 
          onSelectItem={(id) => this.onSelectItem(id)}/>
        <ActionButton
          buttonColor="#9b59b6"
          onPress={() => this.setState({ modalItem: { id: null, text: '', completed: false }, modalVisible: true })} />
      </View>
    );
  }
}

Notable: the AddNewItem component has been replaced by a ViewItem component, which is identical to the AddNewItem component except it has an ‘item’ property that contains the currently selected item. Note also that I’ve added a modalItem definition in the ActionButton when it is pressed. This will fill in the item with a blank item. I also wired up the event handler for when a user presses on a task. That event handler looks like this:

  onSelectItem (id) {
    this.setState({
      modalItem: this.state.items.find(item => item.id === id),
      modalVisible: true
    });
  }

In other words, set up the modalItem (that is passed to the ViewItem) and then make the modal visible. The ViewItem component is enough of a re-write that I will produce it here. However, I’ve highlighted a bunch of lines where functionality has changed as well:

import React, { Component, PropTypes } from 'react';
import { Button, StyleSheet, Switch, Text, TextInput, View } from 'react-native';
import * as uuid from 'uuid';

const styles = StyleSheet.create({
    container: {
        marginTop: 30,
        flexDirection: 'column',
        justifyContent: 'flex-start'
    },
    textinput: {
        height: 40,
        borderWidth: 1,
        borderColor: '#CCCCCC',
        margin: 2,
        paddingLeft: 4,
        paddingRight: 4,
        paddingTop: 0,
        paddingBottom: 0
    },
    completed: {
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center'
    },
    completedtext: {
        fontSize: 16
    },
    buttons: {
        flexDirection: 'row',
        justifyContent: 'space-around'
    },
    title: {
        fontSize: 24
    }
});

class ViewItem extends Component {
    constructor (props) {
        super(props);

        this.state = {
            id: this.props.item.id,
            text: this.props.item.text || '',
            completed: this.props.item.completed || false
        };
    }

    onSaveItem (e) {
        e.preventDefault();
        if (this.props.onSaveItem !== null) {
            this.props.onSaveItem({ id: this.state.id || uuid.v4(), text: this.state.text, completed: this.state.completed });
        }
    }

    onCancel (e) {
        e.preventDefault();
        if (this.props.onCancel !== null) {
            this.props.onCancel();
        }
    }

    render () {
        const title = !this.state.id ? 'Add New Item' : 'Show Item';
        return (
            <View style={styles.container}>
                <Text style={styles.title}>{title}</Text>
                <TextInput
                    onChangeText={(text) => this.setState({ text: text })}
                    placeholder="Enter Task"
                    autoCapitalize="sentences"
                    autoCorrect={true}
                    autoFocus={true}
                    keyboardType="default"
                    maxLength={200}
                    value={this.state.text}
                    style={styles.textinput} />
                <View style={styles.completed}>
                    <Text style={styles.completedtext}>Completed?</Text>
                    <Switch
                        onTintColor='green'
                        onValueChange={(value) => this.setState({ completed: value })}
                        value={this.state.completed} />
                </View>
                <View style={styles.buttons}>
                    <Button
                        title="OK"
                        onPress={(e) => this.onSaveItem(e)}/>
                    <Button
                        color='#888888'
                        title="Cancel"
                        onPress={(e) => this.onCancel(e)}/>
                </View>
            </View>
        );
    }
}

ViewItem.propTypes = {
    item: PropTypes.shape({
        id: PropTypes.string,
        text: PropTypes.string,
        completed: PropTypes.bool
    }),
    onSaveItem: PropTypes.func,
    onCancel: PropTypes.func
};

ViewItem.defaultProps = {
    item: {},
    onSaveItem: null,
    onCancel: null
};

export default ViewItem;

Lines 42-45 set up the state, taking into account that I may have passed a blank item or a fully populated item. Line 52 calls the onSaveItem() event handler, generating a new id only if required, and line 64 picks a title depending on how it was called.

Wrap Up

There were some annoyances (aside from the bug that cost me a few hours). The Android emulator is the one to use for debugging – you can set breakpoints, look at the state of a component, and other nice debugging aids. However, the app always seemed to crash on first run. Restarting the app seemed to work nicely. The iOS simulator started faster but did not have all the nice debugging features. I switched between the two constantly. Yet, this project still took less than 4 hours. I hope it has given you a view into not just how to write the code, but some of the developer thoughts along the way.

Other than that, I’ve now got a working task list. I can set the complete flag by tapping on the box, edit the task by tapping on the text, add a new item by tapping on the floating action button, and swipe left to get a delete button that I can tap on. It all basically works. However, the data is not persisted and it isn’t ready for the cloud just yet.

In the next blog post, I’m going to go through moving this app towards a flux architecture and integrate Redux. Until then, you can find the code for this project on my GitHub Repository.