こちらの内容はReact Native Japanさんが4/18に開催したイベント「新生活応援!React Native ハンズオン ~入門編~」をTypeScriptでやってみた記事になります。
とてもわかりやすかったので、近年さまざまな企業で導入が進んでいるTypeScriptで当日の内容をやってみました。実際のイベントで行ったハンズオンの内容はこちらです。
事前準備
こちらの記事で事前準備に関しては丁寧に書かれていますので、まだの方は環境構築からお願いします。
React Nativeハンズオン〜事前準備編
環境
- Macユーザー前提で書かせていただきます。
- Node v14.15.4
- expo 4.1.4
この記事で作るもの
この記事を最後までやると何が作れるのかを最初に共有します。
某SNS風なカラーリングでつぶやきを投稿でき、さらにその呟きに対していいねができるようなものを作っていきます。
プロジェクトを作成して、起動してみよう!
まずはプロジェクトを作成しましょう。
今回はイベント当日どおりnpm
ではなくyarn
を使用します。
npm
とyarn
の違いについて知りたい方は以下の記事で詳しく書かれているので読んでみてください。(参考記事:npmとyarnとpnpmの違い2021)
1. yarn
のインストール
一言で言うとnpmよりも早いパッケージマネージャーです。今回の記事では深くは触れません。とりあえず動かせるアプリを作りましょう!以下のコマンドでインストールします。
$ npm install --global yarn
インストール終了後に以下のコマンドでversion番号が出てきていれば無事にインストール完了です。
$ yarn -v
1.22.10
2. expo-cliのインストール
React Native Japanさんの記事からの抜粋
expo-cliをインストールしましょう。
cliとはcommand line interfaceの略で、黒い画面で命令文を書いて実行する的なツールのことになります。ここで入れるexpo-cliとはexpoをcliで操作するためのツールってことです!!
なのでインストールしましょう!
$ yarn global add expo-cli
3. プロジェクトを作成し、起動しよう!
React Nativeでアプリ開発をする際にreact-native-cli
かexpo
か、などよく言われますがその辺りはReactNativeJapanさんの記事に書かれているのでそちらを参考にしてください!
参考記事
まずは、以下のコマンドでプロジェクトを作成します。
$ expo init handsOnWithTs //⇦ ここには自由なプロジェクト名を入れることができます
以下、React Native Janapnさんの記事抜粋
blank(TypeScript) TypeScriptをつかった空っぽのテンプレートです。
tabs(Typescript) TypeScriptを使ったタブナビゲーション付きのテンプレートです
minimal bare workflowを使ったテンプレートです
minimal bare workflow + TypeScriptをつかったテンプレートです
と言うことなので、今回はTypeScriptベースのプロジェクトを作成したいので、blank(TypeScript) TypeScript
を選択します。
次に作成したプロジェクトに移動して、起動します!
$ cd handsOnWithTs
$ yarn ios または yarn android
これでiosもしくはandroidのエミュレーターが立ち上がって以下のような画面が表示されるはずです!
これでプロジェクトの作成と起動まで完了しました。
これからコードを書いてアプリを実装していきます!
投稿機能の実装
ここから実装に入っていきますが、細かい説明などはReact Native Japanさんの記事に丁寧に書かれていますので、省略しJavaScriptとTypeScriptでの実装の違いを中心に記載していきます。完成版コードをgithubにアップしておりますのでこちらも掲載いたします!
githubはこちら
App.tsxを編集していきます。
初期状態
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.tsx to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
これが最初の起動時に見えていた画面のソースコードです。ここにユーザー投稿をする入力フォームとそれを送信するためのボタンを実装していきます。この部分に関しては変更はありません。以下のコードに書き換えましょう。
import React from 'react';
import { StatusBar } from 'expo-status-bar';
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,
}
});
某SNSのような入力フォームとボタンが実装できましたね!ただ、今の状態だとなんの機能も実装されていない状態です。
Stateを使用して入力したテキストが表示されるようにしてみましょう!
import React, { useState } from 'react'; //追加
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, TouchableOpacity, SafeAreaView, TextInput } from 'react-native';
export default function App() {
const [text, setText] = useState<string>('');
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,
}
})
この状態にコードが変更できたらエミュレーターで入力フォームにテキストを入力してみましょう!ターミナル上にテキストを入力するたびに、入力したテキストが表示されるはずです。
また、今回はわかりやすくするために以下のように書きました。
const [text, setText] = useState<string>('');
TypeScriptは賢いので明示的にstring
を記載しなくても初期値に空文字列を私手入れば、その値がstring
型であることを推測してくれます。ただ、今回はわかりやすいようにあえて記載しました。VsCodeを使用している人はtext
変数にカーソルを合わせると値の方が表示されるはずです。
Inputコンポーネントの作成とPropsの利用
今度は入力フォームとボタンをInputコンポーネントとしてAppコンポーネントから切り出してみます。コードを以下のように変更してください。
import React, { useState } from 'react'; //追加
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, TouchableOpacity, SafeAreaView, TextInput } from 'react-native';
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>
);
}
interface InputProps {
setText: React.Dispatch<React.SetStateAction<string>>;
}
function Input(props: InputProps) {
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>
)
}
interface
が出てきました。これは、型の宣言(型に名前をつけること)です。interface
で宣言した型のProps
をInputコンポーネントが受け取ると言うことを示すことで型の安全性を保つことができます。今回はAppコンポーネントが持っているsetText
をInputコンポーネントに渡したいのでこのような記述になっています。ちなみに、setText
の型はVsCodeが教えてくれます。(そのほかの方法でも確かめられますが、一番楽なのでこれで)
これでInputコンポーネントを切り出すことができたので、先ほど(Inputコンポーネントを作る前)と同じ動きをすると思います。通常であれば、同じファイルに記載せずに新たにファイルを作成してそちらにInputコンポーネントを書くと思いますが、今回は同一のファイルでやっていきます。
応用編①:複数投稿と投稿したものをスクロールで表示できるようにする
ここからはイベント当日の記事では応用になっていたものです。一気にコードを記載していくので大変かと思いますが、やっていきましょう。以下のコードに変更してください。
interface AppState {
text: string;
}
interface InputProps {
addEet: (text: string) => void;
}
interface EetProps {
text: string;
}
export default function App() {
const [eet, setEet] = useState<AppState[]>([]);
const addEet = (text: string) => {
const initialState: AppState[] = [];
const newEet = initialState.concat(eet);
newEet.push({
text: text,
});
setEet(newEet);
};
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<Input addEet={addEet}/>
<View style={styles.content}>
{eet.map((elem: AppState, index: number) => <Eet key={index} text={elem.text}/>)}
</View>
<StatusBar style="light" />
</View>
</SafeAreaView>
);
}
function Input(props: InputProps) {
const [text, setText] = useState<string>('');
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>
)
}
function Eet(props: EetProps) {
const { text } = props;
return (
<View style={eetStyles.container}>
<Text style={eetStyles.text}>{text}</Text>
</View>
)
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#222',
},
container: {
flex: 1,
paddingTop: 20,
},
inputContainer: {
flexDirection: "row",
paddingHorizontal: 10,
},
input: {
flex: 1,
borderColor: "rgb(29, 161, 242)",
borderWidth: 2,
borderRadius: 10,
color: "white",
paddingHorizontal: 10,
fontSize: 16,
},
button: {
backgroundColor: "rgb(29, 161, 242)",
paddingHorizontal: 20,
paddingVertical: 15,
borderRadius: 20,
},
buttonText: {
color: "white",
fontWeight: "900",
fontSize: 16,
},
content: {
padding: 20,
},
contentText: {
color: "white",
fontSize: 22,
},
});
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,
}
})
各コンポーネントざっくり以下のような役割になっています。
**Appコンポーネント:**全体のState管理
**Inputコンポーネント:**ユーザー入力・投稿管理
**Eetコンポーネント:**投稿データの表示
また、それぞれのコンポーネントに対してinterface
を利用して型付けされたPropsやデータを利用しています。ここまで記載できたら、ユーザーが複数投稿できるようになり、投稿されたデータが一覧で表示されるようになっていると思います。
ただし、今のままだと画面いっぱいに投稿データが表示された時にスクロールで下まで表示することができません。スクロールで表示できるようにします。AppコンポーネントとAppState
を以下のように変更してください!
import {
...省略,
FlatList,
} from 'react-native'; // 追加
interface AppState {
text: string;
id: number;
}
export default function App() {
const [eet, setEet] = useState<AppState[]>([]);
const addEet = (text: string) => {
const initialState: AppState[] = [];
const newEet = initialState.concat(eet);
newEet.push({
text: 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>
);
}
これでスクロール表示することができるようになりました!
応用編②:いいね機能を実装する
いよいよラストです。投稿データに対していいねできるようにしましょう!
以下に完成形のコードを記載します!
import React, { useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import {
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
FlatList,
} from 'react-native';
import { Ionicons } from "@expo/vector-icons";
interface AppState {
text: string;
id: number;
like: boolean;
}
interface InputProps {
setText?: React.Dispatch<React.SetStateAction<string>>;
addEet: (text: string) => void;
}
interface EetProps {
text: string;
like: boolean;
onLike: any;
}
export default function App() {
const [eet, setEet] = useState<AppState[]>([]);
const addEet = (text: string) => {
const initialState: AppState[] = []
const newEet: AppState[] = initialState.concat(eet);
newEet.push({
text: text,
id: Date.now(),
like: false,
});
setEet(newEet);
}
const onLike = (index: number) => {
const initialState: AppState[] = [];
const newState = initialState.concat(eet);
newState[index].like = !newState[index].like;
setEet(newState);
}
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}`}
/>
</View>
<StatusBar style="light" />
</View>
</SafeAreaView>
);
}
export function Input(props: InputProps) {
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 function Eet(props: EetProps) {
const {
text,
like,
onLike
} = props;
return (
<View style={eetStyle.container}>
<Text style={eetStyle.text}>{text}</Text>
<View style={eetStyle.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>
)
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#222',
},
container: {
flex: 1,
paddingTop: 20,
},
inputContainer: {
flexDirection: "row",
paddingHorizontal: 10,
},
input: {
flex: 1,
borderColor: "rgb(29, 161, 242)",
borderWidth: 2,
borderRadius: 10,
color: "white",
paddingHorizontal: 10,
fontSize: 16,
},
button: {
backgroundColor: "rgb(29, 161, 242)",
paddingHorizontal: 20,
paddingVertical: 15,
borderRadius: 20,
},
buttonText: {
color: "white",
fontWeight: "900",
fontSize: 16,
},
content: {
padding: 20,
},
contentText: {
color: "white",
fontSize: 22,
},
});
const eetStyle = 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,
},
})
基本的にはイベント当日の内容と同じですが、要所要所でデータやオブジェクトに型をつけてあげていますね。これがあると、実行する前にエラーがわかるので開発の効率や、規模が大きい開発での安全性が高まるはずです。
筆者もReact Nativeは初心者なのでこれからイベントや自己学習をしていきながら面白いアプリを作れるようになりたいです。React Native Japanさんは定期的にイベントを開催sてくれているのでこちらも要チェックです!
最後にReact Native Japanさんの各種ページを以下に記載して終わります!
connpass
Youtube
Twitter
stand.fm
※ハンズオンの第二回が開催されたらそっちのTypeScript版もやろうかな。。。