個人開発でExpo + React Nativeでアプリを作っているんですが、認証しているユーザーの情報など、アプリ全体でグローバルに変数を持ちたいという気持ちが高まってきました。

React標準でContext APIというものがあるけども、前回そっちを試したので今回はRecoilを触ってみます。

Expo + React NativeのContext APIでユーザー認証情報を引き回すサンプル

新しいプロジェクトの生成とRecoilのインストール

expo cliで新しいプロジェクトを生成。TypeScriptのblankでやります(別になんでもいい)。

$ expo init RecoilSample

❯ blank (TypeScript)

生成が済んだらRecoilをインストール。

$ cd RecoilSample
$ expo install recoil

公式ドキュメントに従って試す

公式のGetting Startedを参考にして動かしてみます。公式はReact NativeではなくReactでの実装例が載っているので適当にReact Nativeに置き換えてます。

App.tsx

公式の例だと、一つのファイルに全部書く感じですが、それだとグローバルの状態管理の良さがまるで分からないので、コンポーネントごとに別ファイルにしてます。

import React from 'react';
import { StyleSheet, View } from 'react-native';
import { RecoilRoot } from 'recoil';
import CharacterCounter from './CharacterCounter';

export default function App() {
  return (
    <RecoilRoot>
      <View style={styles.container}>
        <CharacterCounter />
      </View>
    </RecoilRoot>
  );
}

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

CharacterCounter.tsx

import React from 'react'
import { View } from 'react-native'
import TextInputWithPrint from './TextInputWithPrint'
import CharacterCount from './CharacterCount'

export default function CharacterCounter() {
  return (
    <View>
      <TextInputWithPrint />
      <CharacterCount />
    </View>
  );
}

TextInputWithPrint.tsx

import React from 'react'
import { TextInput, Text, View } from 'react-native'
import { useRecoilState } from 'recoil'
import { textState } from './atoms/Text'

export default function TextInputWithPrint() {
  const [text, setText] = useRecoilState(textState);

  return (
    <View>
      <TextInput style={{ height: 40, borderColor: 'gray', borderWidth: 1 }} value={ text } onChangeText={text => setText(text)}></TextInput>
      <Text>Echo: { text }</Text>
    </View>
  );
}

CharacterCount.tsx

import React from 'react'
import { Text } from 'react-native'
import { selector, useRecoilValue } from 'recoil'
import { textState } from './atoms/Text'

const charCountState = selector({
  key: 'charCountState',
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

export default function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return (
    <Text>Character Count: {count}</Text>
  )
}

atoms/Text.ts

Atomというのは、グローバルに管理されるステートのオブジェクトのことで、atom関数を使ってどんなキーでどんな値が保存されるかを宣言することが出来る。

いろんな箇所で使い回されるはずなのでたぶん別ファイルに切り出すべきなんだろうなと思っている。

参考: https://qiita.com/serinuntius/items/3d6519988233d7ba643c

import { atom } from 'recoil'

export const textState = atom({
  key: 'textState',
  default: 'hoge',
});

こんな感じに動く。

recoil

ReduxRecoilで並べて登場することが多かったので、使ってみる前は、割と重厚なライブラリなのだろうか?って想像してたけど、どうやら単にContext APIの部分だけを扱っているライブラリなので全然そんなことなかった。

Context APIと出来ることはそう大差ないようには思えるが、比較的記述は直感的だと思う。hooksと同じようなインターフェースで使えるのでこっちの方が使いやすいなあという感想。

おまけ

開発中にしょっちゅう下記のエラーが出る。正確に説明出来るかはあやしいけど、Expoには、ソースコードの反映が再ビルドなしに行われるHMR(Hot Module Replacement)という仕組みが備わっている。

おかげで開発がサクサク進んで大変すばらしいんだけども、どうやらソースコードを修正するたびにatomの宣言が行われてしまう?らしく、キーが重複してるよ!って怒られている。

参考ページにある通り、今はどうしてもエラーが出てしまうので、とりあえず無視するしかないねとのこと。

Duplicate atom key "textState". This is a FATAL ERROR in
      production. But it is safe to ignore this warning if it occurred because of
      hot module replacement., undefined

参考: https://file-translate.com/ja/blog/recoil-duplicate-atom-key