React Native for Web と Recoil で localStorage に保存ができる TODO リストを作ってみた
はじめに
テントの中から失礼します、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 コンポーネントの実装は下記になります。
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 コンポーネントを囲んでいます。
import { RecoilRoot } from 'recoil'; ... const AppWrapper: React.FC = () => ( <RecoilRoot> <App /> </RecoilRoot> );
State を作る
Recoil から atom
をインポートして、State を作ります。atom
には引数として、アプリ全体で一意となる key とデフォルト値を指定してあげます。
TODO リストを管理するための State を作るコードは下記になります。
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 を追加する処理です。
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 にして逃げています。
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 が保存されるようになります。
const AppWrapper: React.FC = () => ( <RecoilRoot> <RecoilStatePersist /> <App /> </RecoilRoot> );
localStorage から Recoil へのロード
ページロード時に localStorage から Recoil へ TODO リストをロードしてあげる必要があります。
const initializeState = (mutableSnapshot: MutableSnapshot) => { const item = localStorage.getItem(Store.TodoList.list.key); if (item) { mutableSnapshot.set(Store.TodoList.list, JSON.parse(item).value); } };
上記のコードを、RecoilRoot
に渡してあげます。
const AppWrapper: React.FC = () => ( <RecoilRoot initializeState={initializeState}> <RecoilStatePersist /> <App /> </RecoilRoot> );
これで、ページをリロードした後も、TODO リストが保たれる状態になっています。
おわりに
React Native for Web と Recoil。どちらも書き心地が良いというか、よりシンプルに開発ができるようになる印象を持ちました。まだどちらも上っ面しか理解ができていないので、引き続き何かを作りながら勉強していこうと思います。
あと、逆に React Native でネイティブアプリを作る体験をしてみたい欲が湧きました。
今回は以上になります。最後まで読んで頂きありがとうございました!