memoirにRecoilを導入してみた
Recoilを採用した理由
ペペロミアでは画面を跨いでのステートは全てContext APIを使用していたが、以下の理由で若干悩ましい点があった。
- Context APIだと自由に書けてしまい、実装機能が増えるにつれて独自の実装が増えてしまう
- ReduxやRecoilと比較して、Context APIは遅いらしい (こちらの記事参照)
ということで、Recoilを採用した。
Pull Request
実装
導入は以下を参照
今回は、アプリ起動時をユーザー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はシンプルで使いやすかったので、こちらで今後も実装を進めて行こうと思います。