この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
テントの中から失礼します、CX事業本部のてんとタカハシです!
React で SPA を開発する際は、Material-UI
と Redux
を使うことが多かったのですが、お仕事案件で React Native for Web
と Recoil
を使うことになり、どちらも触れたことがなかったので、試しに TODO リストを作ってみました。せっかくなので、その辺のことについて記事にしようと思います。
作ったもの
画面はこんな感じです。gif を README に貼り付けてます。
下記の機能を実装しました。
- TODO の追加/削除/変更
- チェック入れると DONE になる
- TODO リストを localStorage に保存
環境
$ sw_vers
sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.7
BuildVersion: 19H2
$ node --version
v14.7.0
$ yarn --version
1.22.10
$ yarn list --depth=0
├─ react@16.13.1
├─ recoil@0.0.13
├─ typescript@3.7.5
React Native for Web
is 何
React Native for Web とは、React Native で用意されているコンポーネントを React 上で使えるようにするライブラリです。
これを見て、ほえ?ってなった方。僕もなりました。存在意義がパッと見よくわからなかったのですが、下記の記事がとても参考になりました。
ブラウザは元々単なるドキュメントビューアでしかなく、そこに GUI アプリケーションプラットフォームを抽象化した React Native を 持ち込むことで、いろいろ便利になって開発が捗るよって感じでしょうか。
確かに今回初めて触れてみて、HTML のタグをモリモリ書いていくよりも、React Native が用意しているコンポーネントをモリモリした方が書き心地は良いと感じました。
導入
下記でライブラリを追加します。型情報も入ってくれています。
$ yarn add react-native-web
実装
実装する際のポイントは下記になります。
react-native-web
ではなく、react-native
から必要なコンポーネントを取り込む- style は
react-native
の StyleSheet を使って定義する - style には
react-native
特有のpaddingVertical
などが使える - 基本的に Flexbox で書きまくる
例えば、TODO を追加する Form コンポーネントの実装は下記になります。
src/components/AddItemForm.tsx
import React, { useState } from 'react';
import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
const AddItemForm: React.FC<{
handleAddItem: (text: string) => void;
}> = ({ handleAddItem }) => {
const [text, onChangeText] = useState('');
return (
<View style={styles.root}>
<TextInput
style={styles.textInput}
onChangeText={onChangeText}
value={text}
/>
<TouchableOpacity
style={styles.addButton}
onPress={() => {
handleAddItem(text);
onChangeText('');
}}
>
<Text style={styles.addText}>追加</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
root: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
paddingHorizontal: 20,
},
textInput: {
flex: 1,
paddingVertical: 6,
fontSize: 24,
borderColor: '#666',
borderWidth: 1,
marginRight: 10,
},
addButton: {
minWidth: 100,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#90CAF9',
},
addText: {
fontSize: 18,
},
});
export default AddItemForm;
下記の赤枠の部分になります。
Recoil
is 何
Recoil とは、Facebook 製の状態管理ライブラリです。useState
のグローバル版みたいな感じで State を扱えたりします。Redux と比べるととてもシンプルな使い心地でした。Redux との違いは下記の記事が参考になりました。
Recoil はまだ Experimental な段階のライブラリですので、プロジェクトに導入するかは検討する必要があります。今回作った TODO リストでも、 UNSTABLE な API を叩いていたりするので、ライブラリの Ver が変わった時に動かなくなる可能性があります。
導入
下記でライブラリを追加します。型情報も入ってくれています。
$ yarn add recoil
準備
Recoil で作った State を使用する箇所を <RecoilRoot>
で囲みます。今回の TODO リストでは、App コンポーネントを囲んでいます。
src/App.tsx
import { RecoilRoot } from 'recoil';
...
const AppWrapper: React.FC = () => (
<RecoilRoot>
<App />
</RecoilRoot>
);
State を作る
Recoil から atom
をインポートして、State を作ります。atom
には引数として、アプリ全体で一意となる key とデフォルト値を指定してあげます。
TODO リストを管理するための State を作るコードは下記になります。
src/store/todoList.tsx
import { atom } from 'recoil';
export type TodoListItemProps = {
createdAt: number;
title: string;
done: boolean;
};
export const list = atom<TodoListItemProps[]>({
key: 'store/todoList',
default: [],
});
State を使う
useRecoilState
の引数に、先ほど作った State を指定すると、useState
の戻り値と同様に、State と 更新用の関数が返ってきます。
下記は、新しい TODO を追加する処理です。
src/hooks/useTodoList.tsx
import { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import Store from '../store'; // 今回の実装では、これに TODO リストの State をぶら下げている
const useTodoList = () => {
const [todoList, setTodoList] = useRecoilState(Store.TodoList.list);
const handleAddItem = useCallback(
(title: string) => {
if (title !== '') {
setTodoList([
{ createdAt: Date.now(), title, done: false },
...todoList,
]);
}
},
[todoList, setTodoList]
);
...
};
useRecoilState
の他に、State だけを返す useRecoilValue
や、更新用の関数だけを返す useSetRecoilState
などが用意されています。
localStorage への保存
Recoil で作った State の変更を監視して localStorage に保存することができます。ただ、この機能は ドキュメント に記載があるように、開発途中で変更が入る可能性があります。ご注意ください。
State の変更を監視 & 保存
State への変更を監視して localStorage に保存するためのコードです。Snapshot
型に getNodes_UNSTABLE
が定義されていなかったため、any にして逃げています。
src/App.tsx
const RecoilStatePersist: React.FC = () => {
useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
for (const modifiedAtom of (snapshot as any).getNodes_UNSTABLE({
isModified: true,
})) {
const atomLoadable = snapshot.getLoadable(modifiedAtom);
if (atomLoadable.state === 'hasValue') {
localStorage.setItem(
modifiedAtom.key,
JSON.stringify({ value: atomLoadable.contents })
);
}
}
});
return null;
};
上記を <RecoilRoot>
の中に含めます。これで State が保存されるようになります。
src/App.tsx
const AppWrapper: React.FC = () => (
<RecoilRoot>
<RecoilStatePersist />
<App />
</RecoilRoot>
);
localStorage から Recoil へのロード
ページロード時に localStorage から Recoil へ TODO リストをロードしてあげる必要があります。
src/App.tsx
const initializeState = (mutableSnapshot: MutableSnapshot) => {
const item = localStorage.getItem(Store.TodoList.list.key);
if (item) {
mutableSnapshot.set(Store.TodoList.list, JSON.parse(item).value);
}
};
上記のコードを、RecoilRoot
に渡してあげます。
src/App.tsx
const AppWrapper: React.FC = () => (
<RecoilRoot initializeState={initializeState}>
<RecoilStatePersist />
<App />
</RecoilRoot>
);
これで、ページをリロードした後も、TODO リストが保たれる状態になっています。
おわりに
React Native for Web と Recoil。どちらも書き心地が良いというか、よりシンプルに開発ができるようになる印象を持ちました。まだどちらも上っ面しか理解ができていないので、引き続き何かを作りながら勉強していこうと思います。
あと、逆に React Native でネイティブアプリを作る体験をしてみたい欲が湧きました。
今回は以上になります。最後まで読んで頂きありがとうございました!