😀

React Native ハンズオン〜第一回目 基本文法編

44 min read

はじめに

環境構築終わっていますでしょうか!!?
まだの方はこちらをみて環境構築を完了させていただけますと幸いです!!
ちなみにですが、後から読み返せるようにしたので、ハンズオンを受けていない方でも大丈夫だと思います!!ぜひお役立てくださいませ!

確認事項

  • nodeいれましたか?
  • editorありますか?
  • emulatorありますか?もしくは実機ありますか?
  • 楽しむ心をもってきましたか?

以上を持ってきた方は、
早速参りましょう!!

インストール

React Nativeの開発環境を構築しましょう!!

yarnのインストール

nodeを入れた段階ですでにnpmというパッケージマネージャーはインストールされています!!
が、このハンズオンではyarnを使うのでyarnをインストールしましょう!
ちなみにyarnnpmとおなじパッケージマネージャーですが、yarnnpmよりも速いため、多くの場合は好んで使われる場合が多いです。

npm install --global yarn

こちらのコマンドで一発オーケーです!!

expo-cli

expo-cliをインストールしましょう。
cliとはcommand line interfaceの略で、黒い画面で命令文を書いて実行する的なツールのことになります。
expoの説明は後でしますが、ここで入れるexpo-cliとはexpoをcliで操作するためのツールってことです!!

yarn global add expo-cli

プロジェクトを作ろう

さて、ここまでで事前準備は完了です。つまりここからが本番です!!

このハンズオンではexpoを使います!!
React Nativeのプロジェクトを作る場合は、

  • expo-cli
  • react-native-cli

をつかって作成する方法があります。
どちらもReact Nativeのプロジェクトを作れるという点においては変わりありませんが、expoを使うことによって、さまざまな恩恵を受けることができます。
逆にreact-native-cliを使った場合、ものすごく自由に作れるんですが、その反面全部自分でやらなければならないという玄人好みの方法なのでExpoを使うことを推奨します!!

expoだとできないことが現れるまではexpoに頼りましょう!!現代においてexpoではできないことはほとんどない・・・!!はず・・!!

expo init handson

こちらのコマンドでプロジェクトを作ることができます!!
handsonというプロジェクト名になるので他のプロジェクト名にしたい人はhandsonを好きな名前に変えましょう。

expo init sukinanamae

こんな感じに!

expo-cliとのやりとり

さて、上記のコマンドを実行するとこんな感じで、expoからの質問に合います。

ここでは迷わずblankを選択しましょう!!。
一応補足がてら他のオプションについて書いておきますと・・・。

  • blank(TypeScript) TypeScriptをつかった空っぽのテンプレートです。
  • tabs(Typescript) TypeScriptを使ったタブナビゲーション付きのテンプレートです
  • minimal bare workflowを使ったテンプレートです
  • minimal bare workflow + TypeScriptをつかったテンプレートです

TypeScript?

今はほとんどの先進的な環境において利用されているオープンソースの言語です。
AltJSと言われるJavaScriptの代わりとなる言語で、使うことによって、構文や型チェックといった便利な機能を使うことができます。
一度触るともう元には戻れなくなるほど素晴らしく便利なのですが、いきなりやるのは少し難しいので今日は見送ります・・!!

bare workflow?

Expo Client上で動くReact Natieアプリのことです。
Expoの持っているExpo縛りというデメリットを打ち消しながら、Expoの恩恵を受けられるというメリットがあります。ただし、その分Expoがやってくれていたことの一部をやってくれなくなるというデメリットがあるので一長一短ではあります。
こちらも今回は使いません。

プロジェクトを起動しよう!

起動コマンドはいくつかに分かれます。

その前に、

cd handson

こちらのコマンドをうって、ターミナルで開いているフォルダの階層を移動しておきましょう!!
先程 handson 以外のプロジェクト名にした人はhandsonではなくプロジェクト名でコマンドを実行することを忘れないようにしましょう!!

emulator

emulatorでの起動はこちらのコマンドになります。

  • iOSの人: yarn ios
  • Androidの人: yarn android

それぞれお好きに打ってください(ちなみにyarn startからでも行うことができます)!

emulatorないというか実機で動かしたい

という人は、こちらのコマンドを実行しましょう!!

yarn start

するとこんな感じにQRコードが表示されます!!

このQRコードを読み取る前にAppleStoreで『expo go』というアプリをインストールしておきましょう!!

そしたらQRコードを読み取るとアプリを実機で動かすことができます。

どちらの方法にせよこのような画面が表示されたら成功です!!

ファイルを編集しよう

エディターでプロジェクトを開きましょう!!

プロジェクトを開くを選択して。。。こんな感じにプロジェクトのフォルダを指定してあげるのが良いです!!

次にApp.jsをみながらReactの構文を学んでいきましょう。

基礎構文理解

では実際にApp.jsを見ながらそれぞれのコードの意味を解説していきます!!

import

没頭のこの部分ですね。

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

これはモジュールを読み出すための構文です。
この記述があるとライブラリからモジュール(パーツとか)を読みこんだり、他のファイルに書いてあるコードを読み込んだりすることができます。

ルールは

import 名前 from 'インポート元'

というルールです。
なので、

import React from 'react';

という記述はreactをReactという変数名で読み込むということになります。
ただしこちらはdefault Exportと呼ばれるものを読み込んでいるということだけ覚えておいてください。

これはReactという名前あるという必要がある(Reactのルール)のでこのままにしておいてください(もうちょっと後のバージョンではいらなくなる記述です)。

そして、観察力の良い方はお気づきかもしれませんが{}という記号を使っている箇所がありますね。

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

こちらもほとんど同じ構文なんですがこちらはNamed Exportを読み込んでいます。

さて、ここでdefault Exportnamed exportという聴きなれない言葉が出てきましたが、説明は後日の回で行います!!!

今は、

  • import xx from xxxで読み込む
  • import xx ってやつと import {xx1, xx2} っていうやつがある

この二点だけわかっておけばオッケーです!!
どうしても今わかっておきたい人はこちら

するとここから読み解くことができる内容は、

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
  • expo-status-barからStatusBarを読み込んでいる
  • reactをReactとして読み込んでいる
  • react-nativeからStyleSheetとTextとViewを読み込んでいる

ということになります!
そして読みこんでいるものを使うのはここからのコードになります!!

コンポーネントの定義

Reactにはコンポーネントベースという特徴があります。

コンポーネントとは?

コンポーネントは何か?というと、画面パーツのことになります!。


出典:Material-UI

ちょうどこんな感じのものイメージしていただければと思います。
僕らが普段利用するアプリケーションにはさまざまなパーツが使われていますよね。
入力フォームだったりボタンだったりいろんなものがあります。そういった画面パーツを簡単に使えるようにしたものコンポーネントと言います。

コンポーネントを作ることによって簡単に再利用することができるようになります。アプリケーションの画面はいろんなパーツの組み合わせでできているのでこのようにして作成したコンポーネントを組み合わせて新しいコンポーネントを作ったり、コンポーネントを使って画面を作っていきます。

ちょっとまだピンと来ない方もいらっしゃるとは思うんですがそれはこの先コンポーネントを作っていくことを何回か繰り返してくうちに理解できると思います。

今の段階では

  • コンポーネントという小さい部品パーツを作ることができる
  • コンポーネントを作ることによって再利用が簡単になる
  • コンポーネントを組み合わせることによって画面を作成する

この3つだけ押さえておけばオッケーです。

コンポーネントの定義方法

Reactではコンポーネントを関数として定義することができます。そして、定義した関数(コンポーネント)を組み合わせることもできます。
他のフレームワークなどでは少し煩わしい書き方をするものもありますがReactでは関数として定義するだけで良いというとてもシンプルな書き方をすることができます。

早速、コードをみてみましょう。

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

コンポーネントはこのように関数として定義します。
ルールは単純で、DOMっぽい何かをreturnする たったこれだけです。
なのでこのAppというコンポーネントは、画面上に

    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>

を表示させるコンポーネントであるということになります。
ちなみにこのDOMっぽい何かのことをJSXと言います。
JSXには開始タグと終了タグというものがあります(HTMLわかる人はばっちりなはずです)。

なのでこの部分では

      <Text>Open up App.js to start working on your app!</Text>
  • 開始タグ<Text>
  • 終了タグ</Text>
  • 子要素Open up App.js to start working on your app!

ということになります。つまり ”Open up App.js to start working on your app!”がTextコンポーネントによって囲われているのです。
また囲うものがない時は、<Text></Text><Text />のように書くことができます。

ここで、大切なルールを覚える必要があります。

  • タグには開始と終了が必要である
  • JSXは必ず一つのタグとして括らなければならない
  • React NativeではView、TextなどReact Nativeが提供しているコンポーネントまたは関係するコンポーネントを使う

この三点です。

なので、こういう一つにまとまっていないものや、

return (
	<Text>Hello</Text>
	<Text>world</Text>
)

こういう閉じていないものや

return (<View >)

こういうDOMと同じ書き方をしているものは(React Webならオッケー)

return (
	<div>hello</div>
)

はダメだということです。

必ず、View, TextといったReact Nativeに関係するコンポーネントを使い、一つにまとめ、開始タグと終了タグ囲う必要があるので、このように書きます。

return (
	<View>
	  <Text>Hello</Text>
	  <Text>Hello</Text>
	  <View />
        </View>
)

React Nativeコンポーネントについて

ここでは、App.jsで使われているViewやTextといったコンポーネントを簡単に紹介していきたいと思います!!

(StatusBarは詳しくは触れませんが、expoが提供してくれているステータスバーをいじるためのコンポーネントです。詳しく知りたい方はこちら)

Whats' View

React Nativeのコンポーネント四天王のうちの一人といっても過言ではないほど重要なコンポーネントです。
これを使わないことはあり得ないレベルの出現頻度です。
具体的に何らかな意味を持つことはありませんが入れ物を表現します。Viewを使うことによってさまざまなレイアウトを組むことができるようになる基礎的なコンポーネントです。

What's Text

こちらも四天王のうちの一人といっても過言ではないコンポーネントです。
こちらは文字を表示させるために使います。
文字を表示させたい時は必ずTextで囲ってください。
然もなくばこうなります。

https://twitter.com/NuBurritos/status/1217656869419749376?s=20

App.jsの構造

ここまでくるとAppコンポーネントをの中身を読めるようになってきているはずです。

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

AppはコンポーネントなのでJSXを返します。
そして、JSXはViewで囲われていて、Open up App.js to start working on your app!がTextで囲われているという構造になっています。
なぜViewを使っていて、なぜTextを使っているのかはもうすでに一目瞭然だと思われます。

ここまでくると一つだけ謎があります。

<View style={styles.container}>

このstyleは何者なんだろうか? と。
では最後にここを解説をしていきます。

Style

<View style={styles.container}>

このstyleのことをstyle属性と言います。
React Nativeにおいてstyle属性はとても重要で、styleを使うことによってスタイリングをしていきます。つまり、styleを使わないとレイアウトがとんでもないことになってしまう・・・ということです。

styleには2種類の定義方法があります。

<View style={{ backgroundColor: 'red' }} >

という直接書く方法と

<View style={styles.container}>

・・・・・

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'red',
  }
})

というようにStyleSheetをつかって定義する方法があります。
パフォーマンス最適化されるので後者の書き方を推奨します。
ちょっとめんどくさく感じるかもしれないですが、なれてくるとこちらの書き方の方がみやすいと感じるようになってきます。

StyleSheetについて詳しく知りたい方はこちら

stylingに使えるプロパティについて

これも紹介したいんですが、全部紹介するととんでもないことになる(これだけで3記事ぐらいかけるのではないか・・・!!)というレベルなので割愛しますが簡単にまとめて、本文中に少しずつ紹介します。

基本的にはCSSに使えるようなプロパティは大体使うことができます。
本家によってカテゴライズかされているので気になる人は見てみてください!!

  • Image Style Props:Imageにつかえるもの
  • Layout Props:レイアウトを制御するためのもの。基本中のキ。paddingとかもこれ!めっちゃ重要
  • Shadow Props:影をつけたいならこれ!。CSSと違ってちょっとクセがあるので要注意。
  • Text Style Props:Textにスタイルを当てる時に使う。めっちゃ重要
  • View Style Props:Viewにスタイルを当てる時に使う。めっちゃ重要

以上です、とりわけLayout Propsが最も重要で、 Text Style Props, View Style Propsも重要なので『気になる人はみて』と言いましたが、さらっとだけでも目を通しておいた方がいいかもしれません。

早速書き始めましょう!!

習うより慣れろ!!ということでコードを書きながらなれていきましょう!!

初期状態

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

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

この状態のUIを整備して皆さんの大好きなあれっぽい感じにしていきますよ!!。

このセクションの完成物

コード

一旦無心になってコードを書いて行っていただければ大丈夫ですが、書きながら画面がどのように更新されていくかを感じ取りながらUIを確認していきましょう!!

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View, TouchableOpacity, SafeAreaView, TextInput } from 'react-native';

export default function App() {
  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput style={styles.input}/>
          <TouchableOpacity style={styles.button}>
            <Text style={styles.buttonText}>イートする</Text>
          </TouchableOpacity>
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: '#222',
  },
  container: {
    flex: 1,
    paddingTop: 20,
  },
  button: {
    backgroundColor: 'rgb(29, 161, 242)',
    paddingHorizontal: 20,
    paddingVertical: 15,
    borderRadius: 20,
  },
  buttonText: {
    color: 'white',
    fontWeight: '900',
    fontSize: 16,
  },
  inputContainer: {
    flexDirection: 'row',
    paddingHorizontal: 10,
  },
  input: {
    flex: 1,
    borderColor: 'rgb(29, 161, 242)',
    borderWidth: 2,
    marginRight: 10,
    borderRadius: 10,
    color: 'white',
    paddingHorizontal: 10,
    fontSize: 16,
  }
});

解説

では基本的な構文の理解に入っていきましょう!!

各種コンポーネントのインポートとその使用

import { StyleSheet, Text, View, TouchableOpacity, SafeAreaView, TextInput } from 'react-native';

最初に紹介したViewやTextのように基本的にreact-nativeからコンポーネントをインポートしてくることによってReact Nativeは始まります。

今回ここで新たに追加したものは、

  • TouchableOpacity:押した時に色が変わるようにしてくれるコンポーネント(本当はPressableを使うのが主流でありますが今回は簡単だということでこちらに!)
  • SafeAreaView:ノッチとかiPhoneXタイプのような画面の端を制御してくれるコンポーネント
  • TextInput:テキスト入力をするためのコンポーネント

です。

    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput style={styles.input}/>
          <TouchableOpacity style={styles.button}>
            <Text style={styles.buttonText}>イートする</Text>
          </TouchableOpacity>
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>

これらをこのようにして使っています。
また、CSSと違ってセレクタはないので一つ一つ丁寧にstyleを付与してあげる必要があります。

さらにここではこっそり、

<StatusBar style="light" />

とstyle="auto"をlightに変えています。こうすることでステータスバーの文字色が必ず白くなります(これしないと背景で見えなくなってしまうので)。

ではこのあと触っていくことになる TouchableOpacityTextInputを簡単に紹介します。

TouchableOpacity

使うことによって、ボタンを押した時にOpacityを自動で付与してくれるコンポーネントになります。なので特に何もせずともボタンをおした時にインタラクションをつけることができます。
さまざまなイベントハンドリングを行うことができるのですが、今日扱うのはonPressなのでonPressについて紹介させていただきます。
もっと知りたい方はこちら

<TouchableOpacity onPress={() => console.log('press')}>
   <Text>Click me!</Text>
</TouchableOpacity>

onPressには関数を渡すことができます。
渡した関数はボタンを押した時(厳密には押してから離した時)に実行されます。
なのでユーザーがボタンを押した時と言われるこの挙動の時にはonPressを使って動作をつけることができます。上記のコードの場合はボタンを押した時にpressというログが出力されます。

TextInput

これを使うことによってユーザーに入力させることができるようになります。
イベントハンドリングとして主に使うことになるのはonChangeTextです。
onChangeTextには関数を渡すことができ、この関数は文字を入力するたびに実行されます。
またこの時引数に、入力している文字が渡されることもポイントです。

<TextInput onChangeText={(text) => console.log('You typed', text)} />

上記のコードではユーザーが文字を入力するたびに、入力されている文字列を伴うログが出力されます。
また、キーボードタイプを変更したり、リターンキーのラベルを変更したりなどの便利なpropsもありますので、もし気になったら見てみてください。
詳しくはこちら

スタイリングについて

今回たくさん書きました。
なので、ちょっとづつ紹介していきます!!

backgroundColor

名前の通り背景色を決めるためのプロパティです。

backgroundColor: '#222',

今回は何箇所が出てくるんですが、このようにしてスタイルを当てています。

後述するflexレイアウトを使いこなしていくためにはbackgroundColorを使ってとりあえず着色してみると『このエリアがこの要素なのか』とわかるので僕は開発時には多用しています(みんなそうしているはず・・・?)

指定方法はCSSと一緒で

backgroundColor: '#222', // <- 16進数
backgroundColor: 'white', // <- 文字列
backgroundColor: 'rgb(29, 161, 242)', // <- rgb
backgroundColor: 'rgba(0, 0, 0, 0.3)', // <- rgba

の指定が可能です。

color

文字列の色を決めるためのプロパティです。

color: 'white',

こちらもbackgroundColorと同様の記法を使うことができます。

padding, margin

 paddingHorizontal: 20,
 paddingVertical: 15,

基本概念はCSSと同じくボックスモデルの余白を指定するものなんですが

  • paddingVertical: 垂直方向のpadding
  • paddingHorizontal: 水平方向のpadding
  • marginVertical: 垂直方向のmargin
  • marginHorizontal: 水平方向のmargin

というめっちゃ便利なショートハンドプロパティがあります。
今回はpaddingまとめた指定は使わないで、VerticalとHorizontalを多用していますね。

flex

最後になりますが、めちゃくちゃ重要な概念としてflexがあります。
React Nativeではflexを使ってレイアウトを決めていくことが多いです。いわばflexゲーです。
flexBoxが得意な方ならもうバッチリだと思いますが、ちょっと癖が多いので簡単に紹介します。

flex: 1

のように指定をするとその比率で親のボックスを占有するようになります。
なので試しに、inpuContainerにflex:1の指定をしてみましょう。

inputContainer: {
    flexDirection: 'row',
    paddingHorizontal: 10,
    flex: 1, // <- あとでもどしましょう!
  },

すると・・・

『おっきくなっちゃった!!』って感じですね。
デフォルトでは縦方向に占有比率を指定できるので、inputContainerがflex:1って言った場合は、他にflex:xという指定をしている要素や他の要素もいないため親のボックスの100%を占有することになります。
この方向を変えるためにはflexDirectionを使うことができます。inputContainerにはflexDirectionが指定されているのでinputContainerの適用されているViewは横向きに並んで行くことになります。
flexDireactionの方向については以下の4つを使うことができます

  • column: デフォルトです。上ー>下
  • row: 左ー>右
  • column-reverse:下ー>上
  • row-reverse:右ー>左

です!
ではflexがその比率に応じて画面を占有するという挙動を確かめるために、inputContainerのJSXをコピペして増やしてみましょう。

export default function App() {
  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput style={styles.input}/>
          <TouchableOpacity style={styles.button}>
            <Text style={styles.buttonText}>イートする</Text>
          </TouchableOpacity>
        </View>
        <View style={styles.inputContainer}>
          <TextInput style={styles.input}/>
          <TouchableOpacity style={styles.button}>
            <Text style={styles.buttonText}>イートする</Text>
          </TouchableOpacity>
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

すると今度はflex: 1と言っているinputContainerが二つあるので1:1の比率でそれぞれが大きくなります。これがflexの正体です。もう少し理解したい方はこちらが参考になるかもしれません。


flexの他にはflex関連のプロパティとして

  • justifyContent: flex方向の並び方を指定するプロパティ
  • alignItems: flex方向とは逆の並び方を指定するプロパティ

があります。詳しくはこちら

Stateを使いこなそう

React Native、あるいはReactを書く上でめちゃくちゃ重要な要素の一つにstateがあります。
stateの役割はそのコンポーネントの状態を管理することです。

  • カートに入れた
  • ログインしている
  • 文字を打った

とか・・・、そういった様々な状態を扱うためにstateが存在しています。
ではstateを学ぶために早速ですがコードを書いてみましょう。

『イート』したものが表示されるようにしてみましょうか!。
それも🤩という絵文字を伴って・・・。


こんな感じです!

import React, { useState } from 'react';

export default function App() {
  const [text, setText] = useState('')
  console.log(text)
  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput style={styles.input} onChangeText={(_text) => setText(_text)}/>
          <TouchableOpacity style={styles.button}>
            <Text style={styles.buttonText}>イートする</Text>
          </TouchableOpacity>
        </View>
        <View style={styles.content}>
          <Text style={styles.contentText}>{text}🤩</Text>
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

const sytles = StyleSheet.create({
  ...省略
  content: {
    padding: 20,
  },
  contentText: {
    color: 'white',
    fontSize: 22,
  }
})

ここからちょっと丁寧に解説します。

useState

stateを扱うためにはreactからuseStateをインポートしてそれを使います。

import React, { useState } from 'react';

useStateの書式は下記のようになっています。

const [state本体, stateを変える関数] = useState(初期値)

なので今回は

  const [text, setText] = useState('')

と書いているので、

  • text: stateの入った変数名
  • setText: stateを更新するための変数名

となっています。
ここでとても重要なことは、stateを更新したいなら絶対にsetする関数を使うことということです。なので、うわぁ〜state更新してぇ〜って思っても

text = '更新してぇ〜'

としないで

setText('更新してぇ〜')

としてください。Vueとは違ってReactにおけるstateはプレーンなものなので絶対にこのルールを守ってください。ここがReactの簡単であり難しいところになります。

stateが更新されるとどうなるか? onChangeTextって?

すでに先程のコードにはconsole.log(text)が挟まっているので、ログをみて挙動を理解してみましょう!
エミュレーターを使っている人はcmd + D(Androidならcmd + M)を押すとこのようなメニューが表示されます(たまに調子悪くなったりすることがあるのですがcmd + ctr + zなら確実に開けます)。

これの下から3つ目の『Debug Remote JS』を押しましょう。
これを押すことによってデバッグすることができます。

するとこんな感じの画面が立ち上がります(立ち上がらない人はどこかのタブで立ち上がっている。きっと。)。

cmd + option + i でデバッガメニューを開きましょう。
React NativeではこのようにWeb開発のノウハウを使って開発していくことができます。

ログをみよう

では早速cmd + Rを押して画面をリロードして綺麗な状態にしたのちに、操作をしながらログを確認してみましょう。

初期状態

一文字うつと・・・

二文字うつと・・・

三文字うつと・・・

このように文字を打つたびにログが出力されていることがわかると思います。
これは下図のようにsetText(setState)が実行されるたびに関数が実行(onChangeTextにより)されて、更新されたtext(state)がログとして出力されているということを意味しています。

  1. 関数の実行  <ー これにより初期値("")が出力される(state = "")
  2. Aとタイプする
  3. 関数の実行 <ー これにより"A"が出力される(state = "A")
  4. sとタイプする
  5. 関数の実行 <ー これにより"As"が出力される(state = "As")
  6. sとタイプする
  7. 関数の実行 <ー これにより"Asx"が出力される(state = "Asx")

といった手順になります。
このようにstateが更新されると関数が再実行されるという挙動は再レンダリングとよばれReactの概念を理解する上で非常に大切になるのでここでぜひマスターしてください。

さて、この調子でアプリケーションを開発していくのもいいですが、それをやりすぎるとコンポーネントがとんでもなく長くなってしまうので、ここでコンポーネント化をしましょう。

コンポーネントを作ろう

コンポーネント化をすることによって関心ごとを切り離すことができます。
これはとても大切なことでコードが短くなってみやすくなるし、脳のリソースを増やすことができるし、いいコードを書くために必要なことです。これはコンポーネントに限った話ではないですね。

では早速コンポーネントを分けましょう!
コンポーネントの分け方は大きく分けて2種類あります。

  • ファイルごとガッツリ分ける
  • ファイルは一緒だけど宣言を分ける

の二つの方法があります。では一旦慣れるためにも簡単な『ファイルは一緒だけど宣言は分ける』という方法でコンポーネントを作ってみましょう。

Inputコンポーネントの作成

名前はネーミングセンスが問われます。。。では、この部分をInputコンポーネントとしてコンポーネント化してみましょう(他の名前でも大丈夫ですよ!!)

また、さっきのコードでは入力した文字が画面上に表示される箇所もあったと思いますが、一旦説明の都合上削除します(後でしっかり解説します)!!

function Input() {
  const [text, setText] = useState('')

  return (
      <View style={styles.inputContainer}>
        <TextInput style={styles.input} onChangeText={(_text) => setText(_text)}/>
        <TouchableOpacity style={styles.button}>
          <Text style={styles.buttonText}>イートする</Text>
        </TouchableOpacity>
      </View>
  )
}

export default function App() {
  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <Input />
        <View style={styles.content}>
          <Text style={styles.contentText}></Text>
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

このように書きます。

  • コンポーネントは関数で書く(すでにご存知だとおもいますが・・!!)
  • <Input />のようにViewなどのコンポーネントと同じようにかける
  • コンポーネントは頭文字を大文字で書く

といったことに注目してください!!
ここまではそんなに難しいことはないはずです!!

さてさて、ここで困ったことが発生しました・・・。
本当は元の状態のコードに戻したいんですが・・・以下のような問題が発生していて戻せそうにありません・・・。

ここで大活躍するのがpropsです!!

Props

propsはstateと並んで、React 最重要項目の一つです。
propsはただ単に親から子へ何かを渡すための仕組みです。
渡すものは何でも大丈夫です、それこそ数値だったりオブジェクトだったり、コンポーネントだったり・・・。
ではこれを活用することによってどうなるのかというものをお見せします!!

その前にもう一度状況をおさらいしましょう。

このような状況になっています。つまり、

  • App: ここにstateが欲しい
  • Input: Appの子。stateを持っている

ということです。これを今僕らがやりたいことを実現するためにはAppとInputで何らかの疎通をする必要があります。
これを親から子へ値を渡すというpropsの特性を使って今僕らのやりたいことを実現しましょう。

そのために、

  • App: stateを持ち、stateを表示する(打った文字を画面に表示)
  • Input: propsとしてsetStateを受け取り親のStateを更新する

という状況にしましょう。

ではこれにしたがってコードを書いていきます。

function Input(props) {
  const {
    setState
  } = props
  return (
      <View style={styles.inputContainer}>
        <TextInput style={styles.input} onChangeText={(_text) => setState(_text)}/>
        <TouchableOpacity style={styles.button}>
          <Text style={styles.buttonText}>イートする</Text>
        </TouchableOpacity>
      </View>
  )
}

export default function App() {
  const [text, setText] = useState('')
  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <Input setState={setText}/>
        <View style={styles.content}>
          <Text style={styles.contentText}>{text}</Text>
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

このようになります。
ではコードの中身を見ていきましょう。

親でやっていること

stateの定義

  const [text, setText] = useState('')

stateを親で定義しました!!
これはバッチリですね!!

propsを渡す

        <Input setState={setText}/>

propsを利用するためにはこのように書きます!
ちょっとstyleと書き方似ていると思いませんか・・?そう、実はstyleもpropsだったわけです。

ここで注目する点としては二つあります。

  • props名= の形で渡す
  • 変数などを使いたい場合は {}で囲う

この二点です。なのでこのコードはpropsの名前をsetStateとしてsetText関数を渡している。ということになります。
今回は関数を渡しましたが例えば文字列を直接渡したい場合だったら

<AnyComponent text={'hello'}/>
<AnyComponent text="hello" />

のどちらで渡してもオッケーです!!
また、

<AnyComponent done={true} />
<AnyComponent done />

は同じ意味を持ちます。真偽値の、それもtrueに限った話なのですが割とよく出てくるのでぜひ覚えておきましょう!!。

子でやっていること

さて子でやっていることを紹介していきたいと思います!!
子ではpropsを受け取る必要があります。propsの受け取り方はとても簡単で、関数の引数として受け取ることができます。

function Input(props) {
  const {
    setState
  } = props

ちょうどこの部分ですね。
属性名をpropsのプロパティとして受け取れます。
親の方では

<Input setState={setText}/>

とsetStateという属性名でsetTextを渡しているので、子の方ではsetStateとして受け取れます。
当然ながら親の方をこのように

<Input setText={setText} />

とすると子の方でもsetTextとして受け取ることができます。
とてもシンプルですね。

今のコードにはないですがもう一つ覚えておくといいのはchildrenというものです。Vueをやっている人はslotというとイメージがつきます。
childrenはその名のごとし子要素のことになります。

// 親
<SomeComponent bold value={value}>
   <Text>Hello</Text>
</SomeComponent>

// 子
function SomeComponent(props) {
  const {
    bold, // <- trueになります。さっきやりましたね!
    value, // <- value
    children, // <- <Text>Heloo</Text>になります。
  }= props
  
  return (
    <View style={[bold && styles.bold]}>
      <Text>{value}</Text>
      {children}
    </View>
  )
}

こんな感じになります!!なのでこのコードでは、画面上にはboldスタイルでvalueと<Text>Hello</Text>が表示されるという挙動になります。

と、ここまでできたなら一旦Reactの基本的なところについてはバッチリでしょう!!

さて応用的な話をやっていきましょうか!!

応用しながらアプリケーションを作り込んでいく

今はすでにご存知の通り打った文章がそのまま表示されるだけでイートしている感がないですよね。
なので、

  1. 何件もイートを登録できるようにする
  2. 何件もイートを表示できるようにする
  3. ????

という順番でやっていきましょう。

何件もイートを登録できるようにする

まずはこれからですね。
すでにご想像されているかと思いますが、配列を使います!!
ついでにコンポーネントの責務をきれいに分けて作れるようにしましょう。

こんな感じに、Appは全体のマネジメントをするように、Inputは入力途中のものをマネジメントするようにします。

なので

  • App: 全体のイートを管理する配列のステートが必要
  • Input: 入力途中のものを管理する文字列のステートが必要。ボタンが押されたらAppのステートを更新するメソッド(addEet)を実行

という構成にしましょう(今わからなくてもコードをみたらわかるはず!!)。

function Input(props) {
  const [text, setText] = useState('')
  const onPress = () => {
    props.addEet(text)
    setText('')
  }
  return (
      <View style={styles.inputContainer}>
        <TextInput style={styles.input} onChangeText={(_text) => setText(_text)} value={text}/>
        <TouchableOpacity style={styles.button} onPress={onPress}>
          <Text style={styles.buttonText}>イートする</Text>
        </TouchableOpacity>
      </View>
  )
}

export default function App() {
  const [eet, setEet] = useState([])
  const addEet = (text) => {
    const newEet = [].concat(eet)
    newEet.push({
      text,
    })
    setEet(newEet)
  }

  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <Input addEet={addEet}/>
        <View style={styles.content}>
          <Text style={styles.contentText}>{JSON.stringify(eet)}</Text>
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

イートのスペルやばいですね・・・。こんなことになると思いませんでした・・・。
気を取り直して親から見ていきましょう。

親によるイートの管理

まずはここですね。

  const [eet, setEet] = useState([])
  const addEet = (text) => {
    const newEet = [].concat(eet)
    newEet.push({
      text,
    })
    setEet(newEet)
  }

まず[]を初期値にstateを宣言しています。
次にaddEetという関数を定義します(useCallbackおじさんが出てきそうなんですが今は目を瞑ってください)。
この関数はやや癖のあるところなんですが。。。プリミティブ(string, boolean, numberなど)でない、つまり参照を持つような値の場合にsetStateをするためには新しいインスタンスとして再構築した値をsetStateに入れないといけないんです。これをしないとstateの変更を検知することができません。要は非破壊的な更新をしましょう!ということです。
と難しい話をしましたが、簡単にいうと
アレイとかオブジェクトをsetStateに使う場合は、新しいインスタンスを作ってからsetStateしましょう!
という話です!
なのでaddEetでやっていることは

  1. 配列インスタンスを新しくする (concatのところ)
  2. 新しいeetを追加する(newEet.pushのところ)
  3. setEetで更新

ということになります!配列やオブジェクトを扱う場合にはよくあるデンプシーロールみたいなコンボなので覚えておきましょう!!

そしてここで作成したaddEet関数は・・・

        <Input addEet={addEet}/>

このようにpropsとしてInputコンポーネントに渡しています。

そして配列をどうやって表示させるか・・・といった部分については、今はJSON.stringifyで無理やり文字列にすることによってお茶を濁しています。

          <Text style={styles.contentText}>{JSON.stringify(eet)}</Text>

ここについてはしばしお待ちを!!

子による値の管理

まずはこの部分ですね。

  const [text, setText] = useState('')
  const onPress = () => {
    props.addEet(text)
    setText('')
  }

一行目の部分については疑問の余地はないと思います!!
onPressはボタンを押した時に実行されるようにしています。
なのでボタンが押されると、propsとして受け取っているaddEetが実行されイートが追加されます。そして、textというstateの値を初期化し入力フォームの内容をクリアするという方法です。

        <TextInput style={styles.input} onChangeText={(_text) => setText(_text)} value={text}/>
        <TouchableOpacity style={styles.button} onPress={onPress}>

今回はvalueを与えることによってTextInputの値をstateと同期させています(他の方法もあるんですが、今日はこれでいきます)。
そして、ボタンを押された時にはonPressを実行するという動作の紐付けを行なっています。

何件もイートを表示できるようにする

さて、今までところで何と無くアプリケーションとしての体裁は保てた気がします。
ここからはより一層本格的なものにしていきましょう!!

新しくEetというコンポーネントを作ります!!

const eetStyles = StyleSheet.create({
  container: {
    borderWidth: 1,
    paddingVertical: 10,
    paddingHorizontal: 10,
    borderColor: 'rgb(29, 161, 242)',
    marginBottom: 10,
    borderRadius: 5,
  },
  text: {
    color: 'white',
    fontSize: 16,
  }
})
function Eet(props) {
  const {
    text,
  } = props
  return (
    <View style={eetStyles.container}>
      <Text style={eetStyles.text}>{text}</Text>
    </View>
  )
}

こんな感じですね!!
Eetはpropsとしてはtextという文字列を受け取ることを期待しています。
これを親の方に適用させてeetの数だけこのコンポーネントがレンダリングされるようにしましょう。

        <View style={styles.content}>
          {eet.map((elem, index) => <Eet key={index} text={elem.text}/>)}
        </View>

こちらがそのコードになります。
ちょっと最初は慣れないかもしれないですが、配列を使うことによって画面にリスト表示をすることができます。
ちょびっとだけ解説すると
mapメソッドは配列で使うことができるので配列であるeetも当然使うことができます。
なので、mapメソッドを使ってコンポーネントを返すことでリストを表示が可能になるということです!!
ちょっと雑になってしまいましたが、配列をリスト表示したい時はmapを使ってコンポーネントを返すようにしましょう! ということです。

またこのようにmapなどでリストを表現する際にはkeyと呼ばれる一意キーが必要になります。これはReactが効率よくリストのそれぞれを扱っていくためのものです。本来は一意にする必要があるので、消すとキーとアイテムの対応関係がごちゃごちゃになるようなindexをキーに使うべきではないですが、今は説明のため・・・。

ではこれで一旦アプリケーションの改修はおわったということで、次なる問題提示に向けて無尽蔵にイートしまくってみましょう!

さてここで問題があります。
それは、スクロールできないということです。
webの場合はスクロール前提になっているところがあるんですがアプリの場合はそうもいきません。
そこでスクロールできるようにしていきましょう!!
その方法としてよく使われる方法としては二つあります。

  • ScrollView:いろんなコンテンツを並べるために使う
  • FlatList:リストを並べるために使う

ScrollViewをつかってもおんなじようなことはできるんですがFlatListはパフォーマンスやその他のインターフェースにて色々とリスト形式を表示させるサムシングに対してお膳立てをしてくれるので基本的にはこっちを使いましょう。

FlatListを使ってスクロールできるようにしていく

ここでFlatListの出番です。
FlatListにはいろんなインターフェースがあるんですが基本的なインターフェース3つだけは今日覚えましょう。

  • data: FlatListに描画させたいデータをここに入れます
  • renderItem:コンポーネントの描写はこちら
  • keyExtractor:keyを設定するための関数

この3つです。
といっても説明されるよりもコードみた方がわかるのがエンジニアなので早速FlatListを導入してみましょう!!

export default function App() {
  const [eet, setEet] = useState([])
  const addEet = (text) => {
    const newEet = [].concat(eet)
    newEet.push({
      text,
      id: Date.now() // <- こっそり追加しました・・・!!
    })
    setEet(newEet)
  }

  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <Input addEet={addEet}/>
        <View style={styles.content}>
          <FlatList
            data={eet}
            renderItem={({ item }) => <Eet text={item.text}/>}
            keyExtractor={(item) => `${item.id}`}
          />
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

今回こっそりとidを追加しているのでその点は注意してください。
またFlatListのインポートも忘れずに!!

import { StyleSheet, Text, View, TouchableOpacity, SafeAreaView, TextInput, FlatList } from 'react-native';

これでidは一意になったので(超絶高速に入力できる人がいない限り)晴れ晴れとした気持ちでkeyを使うことができます!!

dataにはeetを与えています。これはバッチリですね!!

renderItemここにはちょっとまどろっこしい書き方がされていますね。
renderItemのイターフェースはこのようになっています。

renderItem={(a) => b}

a = {
  item: リストで渡しているデータの要素
  index: 添字
}

b = コンポーネント

なので、

renderItem={({ item }) => <Eet text={item.text}/>}

という記述は、各要素を受け取り(itemという名前で渡ってきます)、itemの持つtextプロパティをEetコンポーネントのtextに渡しているということです。
こうやってみると簡単ですね。

ちょっと混濁しないようにおさらいだけしておくと・・・
map関数は(elem, index) と第一引数に要素、第二引数に添字が入っているのにたいして、
renderItemは({ item, index }) とそれらが第一引数に全て入っていて、itemという名前で要素、indexという名前で添字が入っているということです。

keyExtractorです!最後に。
こちらはkeyを抽出して設定するものになります。
インターフェースは、

keyExtractor={(要素) => キーの文字列}

このようになっていて、各要素を引数にして、文字列が帰ってくることを期待しています。
なので、item.idを文字列にしているということですね!

ちなみにkeyは以下の条件を満たす必要があります。

  • 一意であること
  • 文字列であること

なのでこで万事オーケーなんですが、少しだけ仕様のところも覚えておきましょう。

リストの場合には各要素にkeyプロパティidプロパティがある場合はこれらを設定しなくてもうまいことやってくれます(ちなみにid, keyも持たずkeyExtractorすら省略した場合は悪しきindexが使われるの要注意)。

と今回はidプロパティがあるので省略できそうなものの、idプロパティはnumberなのでこの変換を挟んでstringにする必要があります・・!!!

逆にいうとつまり、

    newEet.push({
      text,
      id: `${Date.now()}`
    })

こうするとkeyExtractorはいらないということになります(最初から文字列にしとけよって思われるかもですが、あえてやりました)。

何にせよ、これでスクロールできるようになりました。

最後にレイアウト上少し気になるところがあるので直しておきます。

  content: {
    padding: 20,
    flex: 1,   // <- 追加
  },
  contentText: {
    color: 'white',
    fontSize: 22,
  },
  contentContainer: {  // <- 追加
    paddingBottom: 50,
  },
});
  <FlatList
    data={eet}
    renderItem={({ item }) => <Eet text={item.text}/>}
    keyExtractor={(item) => `${item.id}`}
    contentContainerStyle={styles.contentContainer} // <- 追加
  />

contentContainerStyleはリスト内側のスタイルのことでこれをつけることによってスクロースするコンテント下部に余白を設けることができます。

応用篇

さてここからは応用篇です(ハンズオンの時間があればやります!!間に合わなかったら・・・・宿題です・・・笑)。
いいねボタンをつけてみましょう!!
いいねボタンをつくるためには・・・アイコンが必要ですね。
しかしExpoではアイコンも最初からお膳立てしてくれています。

ここからアイコンが探せるので、アイコン変えたり色々使いたい方はここをチェック!!。

アイコンコンポーネントのインポート

import { Ionicons } from '@expo/vector-icons';

こちらでアイコンをインポートしておきます。
他のアイコン使いたい人はIoniconsでないこともあるので気をつけてください。

Eetコンポーンネントのカスタマイズ

const eetStyles = StyleSheet.create({
  container: {
    borderWidth: 1,
    paddingVertical: 10,
    paddingHorizontal: 10,
    borderColor: 'rgb(29, 161, 242)',
    marginBottom: 10,
    borderRadius: 5,
  },
  text: {
    color: 'white',
    fontSize: 16,
  },
  actionContainer: {
    borderTopWidth: 1,
    borderTopColor: '#aaa',
    alignItems: 'flex-end',
    justifyContent: 'center',
    paddingTop: 5,
    marginTop: 20,
  }
})
function Eet(props) {
  const {
    text,
    like,
    onLike,
  } = props
  return (
    <View style={eetStyles.container}>
      <Text style={eetStyles.text}>{text}</Text>
      <View style={eetStyles.actionContainer}>
        <TouchableOpacity onPress={onLike}>
          {like ?
            <Ionicons name="heart-circle-sharp" size={22} color="rgb(252, 108, 133)"/> :
            <Ionicons name="ios-heart-circle-outline" size={22} color="#aaa"/>
          }
        </TouchableOpacity>
      </View>
    </View>
  )
}

さっきまではpropにtextしか受け取っていなかったところに

  • like: いいねされているか、そうでないか
  • onLike: いいねされたことを親に伝えるための関数

という二つのpropsを追加しました。
また、

こんな感じにいいねされている時と、そうでない時に表示を変えたいので、

        <TouchableOpacity onPress={onLike}>
          {like ?
            <Ionicons name="heart-circle-sharp" size={22} color="rgb(252, 108, 133)"/> :
            <Ionicons name="ios-heart-circle-outline" size={22} color="#aaa"/>
          }
        </TouchableOpacity>

このように三項演算子を使うことによっていいねされている時そうでない時のアイコンを出し分けています。
また、アイコンはTouchableOpacityで囲われているのでおされた時にonLikeが実行されるようになっています。

では親の方に行きます。

export default function App() {
  const [eet, setEet] = useState([])
  const addEet = (text) => {
    const newEet = [].concat(eet)
    newEet.push({
      text,
      id: Date.now(),
      like: false,
    })
    setEet(newEet)
  }
  const onLike = (index) => {
    const newEet = [].concat(eet)
    newEet[index].like = !newEet[index].like
    setEet(newEet)
  }

  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <Input addEet={addEet}/>
        <View style={styles.content}>
          <FlatList
            data={eet}
            renderItem={({ item, index }) => <Eet text={item.text} like={item.like} onLike={() => onLike(index)}/>}
            keyExtractor={(item) => `${item.id}`}
            contentContainerStyle={styles.contentContainer}
          />
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

今回追加になったのはこの箇所です。

  const onLike = (index) => {
    const newEet = [].concat(eet)
    newEet[index].like = !newEet[index].like
    setEet(newEet)
  }

いいねボタンが押された時の挙動です。
いいねボタンが押されると何番目のイートが押されたのかが引数として渡ってきます。
なので、その引数のlike(真偽値)を反転させることで、

  • いいね されているなら、いいね の取り消し
  • いいね されていないなら いいね の付与

ということをやっています。

renderItem={({ item, index }) => <Eet text={item.text} like={item.like} onLike={() => onLike(index)}/>}

Eetコンポーネントに渡される時にはindexを知っていないといけないので、このようにrenderItemのindexを活用して、() => onLike(index)とします。
こうすることで、何番目がおされたのかという情報をonLikeが知れるようになります!!

終わりに

以上で完了です!!
まだまだ基本的なところしかやっていないですが、それでもある程度はReact Nativeを使いこなせるようになったと思っています!!
このハンズオンは次回というものがありますので楽しみにしてください!!

今日学んだことは

  • React Nativeの基本文法
  • props, stateの使い方
  • View, Text, TextInput, FlatListの使い方

を学びました!!
これだけでもかなり多くのことができることをわかっていただけたと思います!!。

コードを学ぶ上でコードを書くことはものすごく重要なのでこのアプリケーションに以下の仕様
を盛り込んで復習しておきましょう(答えはありません・・・!!)。

  • 実は一つだけあるバグがあるので探して直してみてください
  • 投稿ユーザーの表示
  • 投稿日時の表示
  • イートの削除機能
  • イートの編集機能

つまったりしたら積極的にDiscordで聞いてみてください!!
では素敵なReact Nativeライフを!!!

最終系のコード(本編の)

import { StatusBar } from 'expo-status-bar';
import React, { useState } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, SafeAreaView, TextInput, FlatList } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

function Input(props) {
  const [text, setText] = useState('')
  const onPress = () => {
    props.addEet(text)
    setText('')
  }
  return (
      <View style={styles.inputContainer}>
        <TextInput style={styles.input} onChangeText={(_text) => setText(_text)} value={text}/>
        <TouchableOpacity style={styles.button} onPress={onPress}>
          <Text style={styles.buttonText}>イートする</Text>
        </TouchableOpacity>
      </View>
  )
}

const eetStyles = StyleSheet.create({
  container: {
    borderWidth: 1,
    paddingVertical: 10,
    paddingHorizontal: 10,
    borderColor: 'rgb(29, 161, 242)',
    marginBottom: 10,
    borderRadius: 5,
  },
  text: {
    color: 'white',
    fontSize: 16,
  },
  actionContainer: {
    borderTopWidth: 1,
    borderTopColor: '#aaa',
    alignItems: 'flex-end',
    justifyContent: 'center',
    paddingTop: 5,
    marginTop: 20,
  }
})
function Eet(props) {
  const {
    text,
    like,
    onLike,
  } = props
  return (
    <View style={eetStyles.container}>
      <Text style={eetStyles.text}>{text}</Text>
      <View style={eetStyles.actionContainer}>
        <TouchableOpacity onPress={onLike}>
          {like ?
            <Ionicons name="heart-circle-sharp" size={22} color="rgb(252, 108, 133)"/> :
            <Ionicons name="ios-heart-circle-outline" size={22} color="#aaa"/>
          }
        </TouchableOpacity>
      </View>
    </View>
  )
}


export default function App() {
  const [eet, setEet] = useState([])
  const addEet = (text) => {
    const newEet = [].concat(eet)
    newEet.push({
      text,
      id: Date.now(),
      like: false,
    })
    setEet(newEet)
  }
  const onLike = (index) => {
    const newEet = [].concat(eet)
    newEet[index].like = !newEet[index].like
    setEet(newEet)
  }

  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <Input addEet={addEet}/>
        <View style={styles.content}>
          <FlatList
            data={eet}
            renderItem={({ item, index }) => <Eet text={item.text} like={item.like} onLike={() => onLike(index)}/>}
            keyExtractor={(item) => `${item.id}`}
            contentContainerStyle={styles.contentContainer}
          />
        </View>
        <StatusBar style="light" />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: '#222',
  },
  container: {
    flex: 1,
    paddingTop: 20,
  },
  button: {
    backgroundColor: 'rgb(29, 161, 242)',
    paddingHorizontal: 20,
    paddingVertical: 15,
    borderRadius: 20,
  },
  buttonText: {
    color: 'white',
    fontWeight: '900',
    fontSize: 16,
  },
  inputContainer: {
    flexDirection: 'row',
    paddingHorizontal: 10,
  },
  input: {
    flex: 1,
    borderColor: 'rgb(29, 161, 242)',
    borderWidth: 2,
    marginRight: 10,
    borderRadius: 10,
    color: 'white',
    paddingHorizontal: 10,
    fontSize: 16,
  },
  content: {
    padding: 20,
    flex: 1,
  },
  contentText: {
    color: 'white',
    fontSize: 22,
  },
  contentContainer: {
    paddingBottom: 50,
  },
});

Discussion

ログインするとコメントできます