React Native for Web と Recoil で localStorage に保存ができる TODO リストを作ってみた

はじめに

テントの中から失礼します、CX事業本部のてんとタカハシです!

React で SPA を開発する際は、Material-UIRedux を使うことが多かったのですが、お仕事案件で React Native for WebRecoil を使うことになり、どちらも触れたことがなかったので、試しに TODO リストを作ってみました。せっかくなので、その辺のことについて記事にしようと思います。

作ったもの

GitHub - iam326/rnw-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 でネイティブアプリを作る体験をしてみたい欲が湧きました。

今回は以上になります。最後まで読んで頂きありがとうございました!