wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

Recoilを使ってみる

memoirにRecoilを導入してみた

Recoilを採用した理由

ペペロミアでは画面を跨いでのステートは全てContext APIを使用していたが、以下の理由で若干悩ましい点があった。

  • Context APIだと自由に書けてしまい、実装機能が増えるにつれて独自の実装が増えてしまう
  • ReduxやRecoilと比較して、Context APIは遅いらしい (こちらの記事参照)

ということで、Recoilを採用した。

Pull Request

github.com

実装

導入は以下を参照

recoiljs.org

今回は、アプリ起動時をユーザーID管理について実装した。

実装仕様は以下の通り

  • アプリ起動時にユーザーIDを取得
    • ストレージにユーザーIDがある場合は、ストレージから取得したユーザーIDをステートに設定し、以降の画面で使用する
    • ストレージにユーザーIDが無い場合は、uuidからユーザーIDを作成し、ストレージ + ステートに設定し、以降の画面で使用する

ということ、まず管理するステートの設定を行う

■ src/store/atoms.ts

import { atom } from 'recoil';

export type User = {
  id: string | null;
};

const initialUserState = (): User => ({
  id: null,
});

export const userState = atom<User>({
  key: 'userState',
  default: initialUserState(),
});

次に、取得するステートを設定

■ src/store/selectors.ts

import { selector } from 'recoil';
import { userState } from 'store/atoms';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const userIDState = selector({
  key: 'userID',
  get: ({ get }) => {
    const user = get(userState);

    return user.id;
  },
});

export const existUserID = selector({
  key: 'existUser',
  get: async () => {
    const uid = await AsyncStorage.getItem('USER_ID');
    if (uid) {
      return uid;
    }

    return null;
  },
});

existUserIDでストレージにユーザーIDが存在するか判定して、userIDStateでステートのユーザーIDを取得する。

アプリ起動時の処理を以下のように書いていく。

■ src/WithProvider.tsx

import React, { useEffect, useState, useCallback } from 'react';
import { useRecoilValueLoadable, useSetRecoilState } from 'recoil';
import { existUserID } from 'store/selectors';
import { v4 as uuidv4 } from 'uuid';
import { userState } from 'store/atoms';
import AsyncStorage from '@react-native-async-storage/async-storage';

type State = {
  setup: boolean;
};

const initialState = (): State => ({
  setup: false,
});

const WithProvider = () => {
  const [state, setState] = useState<State>(initialState());
  const setUser = useSetRecoilState(userState);

  const userID = useRecoilValueLoadable(existUserID);

  const setup = useCallback(
    (id: string) => {
      setUser({ id });
      setState((s) => ({ ...s, setup: true }));
    },
    [setUser]
  );

  const initUser = useCallback(async () => {
    const u = uuidv4();
    await AsyncStorage.setItem('USER_ID', u);

    setup(u);
  }, [setup]);

  useEffect(() => {
    if (userID.state === 'hasValue') {
      if (userID.contents) {
        setup(userID.contents);
      } else {
        // ユーザーIDを設定する
        initUser();
      }
    }
  }, [userID, initUser, setup]);

  if (!state.setup) {
    return null;
  }

  return (..略
};

export default WithProvider;

Recoilでの非同期処理はuseRecoilValueLoadableを使用して書くことができます。

  const userID = useRecoilValueLoadable(existUserID);

上記のuserID.stateは以下の状態で値が変化します * loading: 取得前 * hasValue: 取得済み * hasError: エラー発生

実際の値はuserID.contentsに入っている

  useEffect(() => {
    if (userID.state === 'hasValue') {
      if (userID.contents) {
        setup(userID.contents);
      } else {
        // ユーザーIDを設定する
        initUser();
      }
    }
  }, [userID, initUser, setup]);

Recoilのステートの設定はuseSetRecoilStateを使用

  const setUser = useSetRecoilState(userState);

(...略)

  const setup = useCallback(
    (id: string) => {
      setUser({ id });  // ←
      setState((s) => ({ ...s, setup: true }));
    },
    [setUser]
  );

これでステートの更新は完了です。

次に画面で使用する時は以下の通り

import React, { memo, useCallback } from 'react';
import TemplateHome from 'components/templates/Home/Page.tsx';
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import 'dayjs/locale/ja';
import { useRecoilValue } from 'recoil';
import { userIDState } from 'store/selectors';
import { Props as IndexProps } from './';

dayjs.locale('ja');
dayjs.extend(advancedFormat);

type Props = IndexProps & {
  openSettingModal: boolean;
  onCloseSettingModal: () => void;
};

export type ConnectedType = {
  onAddItem: () => void;
};

const Connected: React.FC<Props> = (props) => {
  const userID = useRecoilValue(userIDState);

  const onAddItem = useCallback(() => {
    console.log(userID);
  }, [userID]);

  const onChangeDate = useCallback(() => {}, []);

  const onItem = useCallback(() => {
    props.navigation.navigate('ItemDetail');
  }, [props.navigation]);

  const onMemoir = useCallback(() => {
    props.navigation.navigate('Memoir');
  }, [props.navigation]);

  return (
    <TemplateHome
      date={dayjs().format('YYYY-MM-DD')}
      openSettingModal={props.openSettingModal}
      onAddItem={onAddItem}
      onChangeDate={onChangeDate}
      onCloseSettingModal={props.onCloseSettingModal}
      onItem={onItem}
      onMemoir={onMemoir}
    />
  );
};

export default memo(Connected);

Recoilのステートの取得はuseRecoilValueを使用 ※今回は実装できるかチェックのみなので、console.logに出力のみで終わりにしています。

 const userID = useRecoilValue(userIDState);

  (...略)

  const onAddItem = useCallback(() => {
    console.log(userID);
  }, [userID]);

これで最小限、実装したかった部分は完了です。

まとめ

Recoilはシンプルで使いやすかったので、こちらで今後も実装を進めて行こうと思います。