JavaScript
iOS
reactjs
reactnative

【React Native入門】Java Scriptでアプリ作ろう【Reactやったことない人向け】2

More than 1 year has passed since last update.

その1:【React Native入門】Java Scriptでアプリ作ろう【Reactやったことない人者向け】1

はじめに

前回の続きです。
前回はちょびっとレイアウトを変更した感じで終わりました。今回はTodoアプリを完成させます。
本当は、いろんなコンポーネントをつかうとレイアウトもよくなるんですが、かっこいいものを作ることが目的ではなく、React Nativeを理解することが目的なのでダサくとも基本的には手作りする方針で行きます。

コンポーネントを作る。

src/component/TodoInput.jsを作成してください。
このファイルにTodoアプリに必要な入力フォームとTodoボタンがセットになったコンポーネント作成してきたいと思います。

下記コードを書きます。

TodoInput.js
import React, { Component } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  TextInput,
  StyleSheet,
} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    padding: 20,
  },
  textInput: {
    flex: 3,
    backgroundColor: '#FFF',
    marginRight: 5,
  },
  button: {
    flex: 1,
    backgroundColor: '#008080',
    marginLeft: 5,
    alignItems: 'center',
    justifyContent: 'center',
    paddingTop: 10,
    paddingBottom: 10,
  },
  buttonText: {
    color: '#FFF',
    fontWeight: '500',
  }
});

export default class TodoInput extends Component {
  render() {
    return (
      <View style={styles.container}>
        <TextInput style={styles.textInput}/>
        <TouchableOpacity style={styles.button}>
          <Text style={styles.buttonText}>追加</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

ここで、新しいコンポーネントが二つ登場しました。

TextInput

テキスト入力を行うために必要になります。HTMLで言う所のinputに相当します。キーボードのタイプ、キーボードの改行ボタンのレイアウトといったinputよりもかなり細かいところまで色々できます。
反面、キーボードとこれがかぶる問題を自力解決する必要があります。
どうやって入力した値を取るんだといったことについては後述します。

詳しくはこちらをご覧ください。

【React Native】TextInputがキーボードに隠れてしまう問題の解決法について

TouchableOpacity

押下した時にふわっと半透明になる挙動をつけることができます。これをボタンに転用している人も多い?
他には、TouchableHightRight派とButton派がいそうです。僕はTouchableOpacity派です(ずっと使ってたので)。
押した時の挙動はどうやってつけるんだ!といったことは後述します。

コンポーネントの読み込み

App.jsを編集しましょう。

App.js
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View
} from 'react-native';
import TodoInput from './src/component/TodoInput';


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

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#333',
    paddingTop: 40,
    alignItems: 'center',
  },
  main: {
    flex: 1,
    maxWidth: 400,
    alignItems: 'center',
  }
});

だいぶさっぱりしました。
ここで見て欲しいのは、

import TodoInput from './src/component/TodoInput';

この記述です。パスが相対パスになっていて先ほどの作ったコンポーネントをさしています。
つくったコンポーネントはこのようにして読み込むことができます。

次に見ていただきたいのは

      <View style={styles.container}>
        <View style={styles.main}>
          <TodoInput />
        </View>
      </View>

この中にある<TodoInput />という記述です。
先ほど読み込んだコンポーネントはこのように<View />と同じ使い方で利用することができます。

ざっと画面を確認してみると、

スクリーンショット 2017-10-29 23.42.17.png

こんな感じに表示されています。
styles.mainにはmaxWidthが指定されているので横にむけると

スクリーンショット 2017-10-29 23.42.25.png

このようになります。
エミュレーター場ででcmd + -> を押すと横転させることができます。

動作をつけるその前に

(contextについては説明しません)。

React にはpropsというシステムがあります。当然ながらReact NativeもReactなのでpropsというシステムがあります。
このpropsというシステムでは親子関係に当たるコンポーネントにおいて親から子に値を渡すことができます。そして、子から親へ値を渡すことはできないというルールがあります。

今作っているアプリケーションはAppが親にTodoInputが子になっています。そのうち、入力されたTodoをリスト表示するTodoListというコンンポーネントをAppの子として作ろうと思っているんですが、子から親へpropsを渡せないので入力情報を親のAppで管理して行きたいと思います。

App 情報を管理
┣ TodoInput 入力させる
┗ TodoList Appから受け取った情報をリスト表示させる

といった感じになります。勘の良い方は「ん?TodoInput で入力させたらAppが入力された情報をしることはできなくないか?」とお思いかもしれませんが、関数を渡すことでこの問題が解決されます。

動作をつける

App.jsにonPressメソッドを作成し(アンダースコアをつけるのはコーディングの癖で、つけなくても大丈夫です)、TodoInputにpropsとしてわたしますonPressメソッド受け取った引数をconsole.logするだけのメソッドです。

App.js
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View
} from 'react-native';
import TodoInput from './src/component/TodoInput';


export default class App extends Component<{}> {
  _onPress = (text) => {
    console.log(text);
  }

  render() {
    return (
      <View style={styles.container}>
        <View style={styles.main}>
          <TodoInput onPress={this._onPress} />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#333',
    paddingTop: 40,
    alignItems: 'center',
  },
  main: {
    flex: 1,
    maxWidth: 400,
    alignItems: 'center',
  }
});

propsとして渡すには<TodoInput onPress={this._onPress} />このように渡します。この時onPressとして渡しているので子はonPressとして受け取ることができます。これがonChangeであればonChangeとして受け取ることができます。

次にTodoInput.jsを変更します。

TodoInput.js
import React, { Component } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  TextInput,
  StyleSheet,
} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    padding: 20,
  },
  textInput: {
    flex: 3,
    backgroundColor: '#FFF',
    marginRight: 5,
  },
  button: {
    flex: 1,
    backgroundColor: '#008080',
    marginLeft: 5,
    alignItems: 'center',
    justifyContent: 'center',
    paddingTop: 10,
    paddingBottom: 10,
  },
  buttonText: {
    color: '#FFF',
    fontWeight: '500',
  }
});

export default class TodoInput extends Component {
  render() {
    const {
      onPress,
    } = this.props;

    return (
      <View style={styles.container}>
        <TextInput
          style={styles.textInput}
          onChangeText={text => onPress(text)}
        />
        <TouchableOpacity style={styles.button}>
          <Text style={styles.buttonText}>追加</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

ちょっとonPressなのにpressした時に発火しないようになっていますがこれは今のところ目をつむってください。
onPressとしてpropsを渡しているので、

    const {
      onPress,
    } = this.props;

このようにthis.props.onPressとして受け取ることができます。これはReactの大原則なので覚えておいていただけると幸いです。

今は疎通確認を行いたいので、TextInputのonChangeTextにonPressを渡します。onChangeTextはTextInputに入力値があるたびに発火し、引数で入力値をうけとることができます。

        <TextInput
          style={styles.textInput}
          onChangeText={text => onPress(text)}
        />

ここまででエミュレーターをリロードします。
そして、cmd + Dをおして

スクリーンショット 2017-10-30 0.39.04.png

「Debug JS Remotely」を押します。これを押すとブラウザがたちあがったかと思うのですが、ここでcmd + alt + iをおしてchrome developer toolsを立ち上げます。実はReact Native、webと同じようにデバッグして開発していくのです

早速consoleを開きつつ、TextInputに入力して見ましょう!

スクリーンショット 2017-10-30 0.42.30.png

スクリーンショット 2017-10-30 0.41.38.png

こんな感じでconsoleがはきだされていたらおっけーです!

ワーニングが出てますがこれは気にしないでください。エミュレーターとブラウザを同じ画面で見つつ作業しないとこうなります。
いまはこの記事を書いていて画面がいっぱいいっぱいなので僕の場合はでてしまいます。
普段はエミュレーターとブラウザは同じ画面で作業するのが色々と楽なのでオススメします。

入力値のみを親のコンポーネントに伝える

今のままだと入力するたびに親に入力値がわたされるので、refとよばれるものをつかって、ボタンを押した時だけ親に値が伝わるように変更します。
※refの紹介をするためにrefを使った解説にしています。stateで管理していった方がReactライクです。stateの紹介はこの次に行います。

TodoInput.js
import React, { Component } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  TextInput,
  StyleSheet,
} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    padding: 20,
  },
  textInput: {
    flex: 3,
    backgroundColor: '#FFF',
    marginRight: 5,
  },
  button: {
    flex: 1,
    backgroundColor: '#008080',
    marginLeft: 5,
    alignItems: 'center',
    justifyContent: 'center',
    paddingTop: 10,
    paddingBottom: 10,
  },
  buttonText: {
    color: '#FFF',
    fontWeight: '500',
  }
});

export default class TodoInput extends Component {
  constructor(props) {
    super(props);

    this.ref = {};
  }

  _onPress = () => {
    this.props.onPress(this.ref._lastNativeText);
    this.ref.setNativeProps({ text: '' });
  }

  render() {
    const {
      onPress,
    } = this.props;

    return (
      <View style={styles.container}>
        <TextInput
          style={styles.textInput}
          ref={(ref) => { this.ref = ref; }}
        />
        <TouchableOpacity
          style={styles.button}
          onPress={this._onPress}
        >
          <Text style={styles.buttonText}>追加</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

こんな感じに書き換えます。
ここで変わったのは

  • onChangeTextを外して、ボタンにonPressを設置した
  • constructorを書いた
  • _onPressメソッドを書いた

です。これによって、ボタンを押下した時のみにconsoleで入力値が吐き出されるようになりました。また、吐き出し後は値がリセットされるようになっています。
ここで注目するべき点はrefです。

  constructor(props) {
    super(props);

    this.ref = {};
  }

...

        <TextInput
          style={styles.textInput}
          ref={(ref) => { this.ref = ref; }}
        />

このようにconstructorでrefの初期値を設定して、TextInputには関数をセットして、引数を受け取りそれをthis.refにセットしています。

refとはなんぞ?

refを使うことによってそのコンポーネントに関する様々な情報を取得することができます。
たとばmeasureメソッドを使うと現在位置や高さ・幅を取得できたり、そのコンポーネントのもつstaticなメソッドを利用できたりなどのことを行うことができます。HTMLで言う所のidに似ています。

例えば、

  _onPress = () => {
    this.props.onPress(this.ref._lastNativeText);
    this.ref.setNativeProps({ text: '' });
  }

この部分。この部分の挙動を書き換えました。ボタンを押した時にここが動くようにしました。
その時のこの処理は、refをつかって入力値を吐き出させる、その後、フォームに入力していた内容をリセットするというように書き換えています。

このようにrefを使うことでrefを設定したコンポーネントにアクセスできるようになります。
割とよく使う手法なので覚えておくことをオススメします。

押した時の挙動づけ

        <TouchableOpacity
          style={styles.button}
          onPress={this._onPress}
        >

ボタンをタップした時にイベントを発火させたいのであればonPressを使います。これによってボタンが押された時_onPressメソッドが実行されるようになっています。

スクリーンショット 2017-11-12 12.58.27.png

親で入力された値を管理する

ここでReactの重要要素であるstateを利用します。

App.js
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View,
  FlatList,
} from 'react-native';
import TodoInput from './src/component/TodoInput';
import TodoItem from './src/component/TodoItem';


export default class App extends Component<{}> {
  constructor(props) {
    super(props);

    this.state = {
      list: [],
    };
  }

  _onPress = (text) => {
    const list = [].concat(this.state.list);

    list.push({
      key: Date.now(),
      text: text,
      done: false,
    });

    this.setState({
      list,
    });
  }

  render() {
    const {
      list,
    } = this.state;

    return (
      <View style={styles.container}>
        <View style={styles.main}>
          <TodoInput onPress={this._onPress} />
          <View style={styles.todoListContainer}>
            <FlatList
              style={styles.todoList}
              data={list}
              renderItem={({ item }) => <TodoItem {...item} />}
            />
          </View>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#333',
    paddingTop: 40,
    alignItems: 'center',
  },
  main: {
    flex: 1,
    maxWidth: 400,
    alignItems: 'center',
  },
  todoListContainer: {
    flexDirection: 'row',
    flex: 1,
  },
  todoList: {
    paddingLeft: 10,
    paddingRight: 10,
  }
});

App.jsをこのように書き換え、component直下にTodoItem.jsを作成し、以下のように書きます。
ちょっと今までとは違う書き方ですがSFCと呼ばれるものです。
これについては後述します。

TodoItem.js
import React from 'react';
import {
  View,
  Text,
  StyleSheet,
} from 'react-native';

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#FFF',
    paddingTop: 10,
    paddingBottom: 10,
    paddingLeft: 5,
    paddingRight: 5,
    marginBottom: 10,
    minHeight: 50,
  },
  text: {
    color: '#333',
  }
})

const TodoItem = (props) => {
  const {
    text,
  } = props;

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

export default TodoItem;

以上の変更で、
スクリーンショット 2017-11-12 13.21.26.png

このように表示されるようになったかと思います。

stateとはなんぞ?

propsは親から渡されるものであるのに対してstateはコンポーネントがもつ状態そのものになります。

  constructor(props) {
    super(props);

    this.state = {
      list: [],
    };
  }

App.jsのコンストラクターをみるとこのようになっています。
これはstateの初期値を設定している記述になります。

  _onPress = (text) => {
    const list = [].concat(this.state.list);

    list.push({
      key: Date.now(),
      text: text,
      done: false,
    });

    this.setState({
      list,
    });
  }

次に今までconsole.logしか書いてなかった_onPressメソッドはこのようになっています。
ここで見るべき点は、this.setStateになります。こうすることでstateを更新することができます。このようにstateを更新した場合は、再びrenderメソッドが走ることになるので変更が反映されます。renderメソッドが発火する場合はこの場合とpropsが変更された時で、うまく変更が反映されない場合はrenderメソッドが走らないような変更をしている時なのでどこかしらに原因があります。
この時keyというプロパティを追加しているのは後述するFlatListの仕様のためです。key: list.lengthにしてもいい説はありますが、keyは必ず一意である必要があり、listを削除することを念頭におくとkeyになりうるものがないので一旦Date.nowしています。
concatしているのは参照でthis.stateを直接変更することになりうるためです。

FlatListとはなんぞ?

React Nativeではいかに要素を詰め込んでオバーフローしていたとしてもスクロールすることはできません。スクロールを実装するためには、ScrollView, FlatList, ListViewなどのコンポーネントを使ったり、自分でイベントをハンドリングする必要があります。
そこで今回はFlatListを紹介します。
ScrollViewがもっとも簡単でなかなかに使う機会も多いのでもし興味があれば見ていただくのが良いかもしれません。

FlatListは割と最近追加されたコンポーネントで今回の場合のようなListを表示させるのに適しています。ScrollViewの方が簡単なんですが、ScrollViewだとリスト表示する際に色々と考える必要があるのですが(パフォーマンスとか各アイテムの管理など)そこらへんをいい感じにしてくれます。
ListViewはネイティブ独特の概念をそのまま持ってきたような実装方法になるので高度なことをやりたいといったニーズがない限りはやる必要はないかもしれません。ザッパに言うとListViewを簡単にしてくれたのがFlatListです。
簡単なんですが、渡す配列にはkeyという一意な項目が必ず必要になるという点には注意してください。

またもう一点注意すべき事項があります。
dataにレンダリングしたい配列を渡して、renderItemにコンポーネントを渡すのですが、この時渡ってくる引数はオブジェクトになっていてindex, itemを持ちます。itemの中は各々の要素なので、

            <FlatList
              style={styles.todoList}
              data={list}
              renderItem={({ item }) => <TodoItem {...item} />}
            />

このように受け取る必要があります。

SFCとはなんぞ?

Stateless Functional Componentの略でclass構文を使わず、関数としてコンポーネントを定義することができます。

const TodoItem = (props) => {
  const {
    text,
  } = props;

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

こんな感じに関数になっています。

パフォーマンスが良いためパフォーマンスをあげたい時や、リストはたくさん表示される場合があるためリスト項目の一アイテムに使ってあげるのが良いかと思われます。

ただし、少し制約があります。

  • class構文のように内部メソッドを持たせられない
  • stateを持つことができない
  • componentWillMountといった(まだ解説していないです)ライフサイクルイベントが使えない

といった制約があります。

汎用的なコンポーネントを作る

実施するのと消すのの、両方をやりたいのでボタンを実装します。
component配下にButton.jsを作ってください。

component/Button.js
import React from 'react';
import {
  TouchableOpacity,
  Text,
  StyleSheet,
} from 'react-native';

const styles = StyleSheet.create({
  button: {
    paddingTop: 5,
    paddingBottom: 5,
    paddingLeft: 10,
    paddingRight: 10,
    backgroundColor: '#008080',
    marginLeft: 5,
    marginRight: 5,
  },
  textStyle: {
    color: 'white',
  },
});

const Button = (props) => {
  const {
    onPress,
    children,
    style,
    textStyle
  } = props;

  return (
    <TouchableOpacity
      onPress={onPress}
      style={[styles.button, style]}
    >
      <Text style={[styles.textStyle, textStyle]}>{children}</Text>
    </TouchableOpacity>
  );
};

export default Button;

次に、TodoItem.jsを編集します。

component/TodoItem.js
import React from 'react';
import {
  View,
  Text,
  StyleSheet,
} from 'react-native';
import Button from './Button';

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#FFF',
    paddingTop: 10,
    paddingBottom: 10,
    paddingLeft: 5,
    paddingRight: 5,
    marginBottom: 10,
    minHeight: 50,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  text: {
    color: '#333',
  },
  deleteButton: {
    backgroundColor: '#800000',
  },
  left: {
    flexDirection: 'row',
    alignItems: 'center',
  }
})

const TodoItem = (props) => {
  const {
    text,
  } = props;

  return (
    <View style={styles.container}>
      <View style={styles.left}>
        <Button>
          Done
        </Button>
        <Text style={styles.text} >{text}</Text>
      </View>
      <Button style={styles.deleteButton}>
        Delete
      </Button>
    </View>
  );
}

export default TodoItem;

以上でこのようになります。
スクリーンショット 2017-11-19 2.55.26.png

ボタンの作り自体にはあまり疑問がないかと思われます。
ちょっと特殊な感じになっている部分について説明します。

styleについて

    <TouchableOpacity
      onPress={onPress}
      style={[styles.button, style]}
    >
      <Text style={[styles.textStyle, textStyle]}>{children}</Text>
    </TouchableOpacity>

こんなかんじにstyle={[styles.button, style]}となっています。React Native(ReactのCSS in JS)ではこのように書くことでstyleを合成することができます。
これをやっておくことによってButtonの色を変更できるようにしています。
このコンポーネントを使う時に、styleをpropsとして渡せばそれも適用され、そうでなければデフォルトのもののみが使われます。複数のstyleが渡っているときは、後ろのものが優先されます。
例えば、

const styles = StyleSheet.create({
  button: {
    paddingTop: 5,
    paddingBottom: 5,
    paddingLeft: 10,
    paddingRight: 10,
    backgroundColor: '#008080',
    marginLeft: 5,
    marginRight: 5,
  },
  textStyle: {
    color: 'white',
  },
});

backgroundColorが定義されているので、styleを渡さなければ#008080が適用されます。
しかし、

  deleteButton: {
    backgroundColor: '#800000',
  },

・・・

      <Button style={styles.deleteButton}>
        Delete
      </Button>

styleを渡すことで、backgroundColor#008080を上書きして#800000を適用させることができます。
このように中に持っている情報をpropsで書き換えることで汎用性をあげることができます。
これをしないとDoneButton.js, DeleteButton.jsを作り、他のボタンが必要になるたびにXXXButton.jsを作るハメになります。

また、themeみたいなものを作っておいてそれが適用されるような作りもいいかもしれません。

childrenとは?

childrenとは子要素をさします。

        <Button>
          Done
        </Button>

例えばこれ、子要素としてDoneをそのまま渡しています。
これは、props.children(this.props.children)で受け取ることができるためButton.jsでは

const Button = (props) => {
  const {
    onPress,
    children,
    style,
    textStyle
  } = props;

  return (
    <TouchableOpacity
      onPress={onPress}
      style={[styles.button, style]}
    >
      <Text style={[styles.textStyle, textStyle]}>{children}</Text>
    </TouchableOpacity>
  );
};

受け取ったchildrenをTextで囲んで表示させてあげています。

各行を押した時の動作の付け方

次に1番目のdoneを押したら、実行済みになって、deleteを押したら消えるという挙動をやりたいと思います。

nazo.gif

一見難しそうに感じますが「関数を返す関数」を使うことで解決します。

App.jsを下記のように編集します。

App.js
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View,
  FlatList,
} from 'react-native';
import TodoInput from './src/component/TodoInput';
import TodoItem from './src/component/TodoItem';


export default class App extends Component<{}> {
  constructor(props) {
    super(props);

    this.state = {
      list: [],
    };
  }

  _delete = (index) => () => {
    const list = [].concat(this.state.list);
    list.splice(index, 1);

    this.setState({
      list,
    });
  }

  _done = (index) => () => {
    const list = [].concat(this.state.list);
    list[index].done = !list[index].done;

    this.setState({
      list,
    });
  }

  _onPress = (text) => {
    const list = [].concat(this.state.list);

    list.push({
      key: Date.now(),
      text: text,
      done: false,
    });

    this.setState({
      list,
    });
  }

  render() {
    const {
      list,
    } = this.state;

    return (
      <View style={styles.container}>
        <View style={styles.main}>
          <TodoInput onPress={this._onPress} />
          <View style={styles.todoListContainer}>
            <FlatList
              style={styles.todoList}
              data={list}
              renderItem={({ item, index }) => (
                <TodoItem
                  onDone={this._done(index)}
                  onDelete={this._delete(index)}
                  {...item}
                />
              )}
            />
          </View>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#333',
    paddingTop: 40,
    alignItems: 'center',
  },
  main: {
    flex: 1,
    maxWidth: 400,
    alignItems: 'center',
  },
  todoListContainer: {
    flexDirection: 'row',
    flex: 1,
  },
  todoList: {
    paddingLeft: 10,
    paddingRight: 10,
  }
});

次にcomponent/TodoItem.jsを編集します。

component/TodoItem.js
import React from 'react';
import {
  View,
  Text,
  StyleSheet,
} from 'react-native';
import Button from './Button';

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#FFF',
    paddingTop: 10,
    paddingBottom: 10,
    paddingLeft: 5,
    paddingRight: 5,
    marginBottom: 10,
    minHeight: 50,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  text: {
    color: '#333',
  },
  doneText: {
    textDecorationLine: 'line-through'
  },
  deleteButton: {
    backgroundColor: '#800000',
  },
  left: {
    flexDirection: 'row',
    alignItems: 'center',
  }
})

const TodoItem = (props) => {
  const {
    text,
    onDone,
    onDelete,
    done,
  } = props;

  return (
    <View style={styles.container}>
      <View style={styles.left}>
        <Button onPress={onDone}>
          {done? "Undo" : "Done"}
        </Button>
        <Text style={[styles.text, done && styles.doneText]} >{text}</Text>
      </View>
      <Button
        style={styles.deleteButton}
        onPress={onDelete}
      >
        Delete
      </Button>
    </View>
  );
}

export default TodoItem;

App.jsでは_done, _deleteを追加しました。

  _delete = (index) => () => {
    const list = [].concat(this.state.list);
    list.splice(index, 1);

    this.setState({
      list,
    });
  }

  _done = (index) => () => {
    const list = [].concat(this.state.list);
    list[index].done = !list[index].done;

    this.setState({
      list,
    });
  }

これが「関数を返す関数」の正体です。indexを受け取ると件数を返すようになっていると思います。
この関数の使い方は「関数を返す関数」なので

              renderItem={({ item, index }) => (
                <TodoItem
                  onDone={this._done(index)}
                  onDelete={this._delete(index)}
                  {...item}
                />
              )}

このようになります。こうすることでindexを渡した関数を実行させることができます。
ゆくゆくreduxをやると「関数を返す関数を返す関数」が出てきたりするので今のうちに慣れておくといいかもしれません。
Reactだけやる場合もHOCと呼ばれるコンポーネントを返す関数といった実装パターンもあります。

コンポーネント内で評価式を使う。

gifをみるとdoneを押すと打ち消し線がはいってDoneというテキストがUndoになっているかと思います。

        <Button onPress={onDone}>
          {done? "Undo" : "Done"}
        </Button>

それはこのようにすることで実装可能です。このとき出ている{}{}内をJSとして評価するためのシンタックスです。故に三項演算子を使うことができます。変数を使う場合も同じように{}で囲うことで実現できます。

終わりに

以上でTodoの実装は終わりです。
余力があれば、モーダルや画面遷移の記事も書いていきたいと思っています。
React Nativeはちゃんとしたものが作れるのでとても素晴らしいと思っています。僕も最近転職したんですが転職先でReact Nativeを実戦導入することができてとても嬉しい気持ちになっています。
是非とも素晴らしいReact Nativeライフをお送りいただけますと幸いです。
あと、わりと技術書店で出した本がいろんな方々に読まれていて嬉しい限りです。
是非今後ともよろしくおねがいいたします!