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はシンプルで使いやすかったので、こちらで今後も実装を進めて行こうと思います。