🦄

⚛️React, Recoilでカスタムフックを作ってRecoilを隠匿する

2021/02/21に公開

課題

初めてrecoilを使うが、component側でrecoilを使っている事を意識したくない。

カスタムフックの作成

  1. component側でrecoilをimportしない
  2. カスタムフックでstateに関する関数を呼び出せるようにする

最小構成

定義側

hooks/userState
import React from 'react';
import { atom, useRecoilState } from 'recoil';
import { User } from '@/types';

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

export const useUser = () => {
  const [user, setUser] = useRecoilState(userState);

  return {
    state: user,
    set: setUser,
  };
};

component側

import React, { FC } from 'react';
import { useUser } from '@/hooks/userState';

const UserName: FC = () => {
  const { state } = useUser();
  
  return <p>{state.name ?? 'No Data'}</p>
}

Recoilのselectorを使って計算結果を返却する

年齢などのデータから計算した値が欲しい場合にselectorは活躍してくれます。
atomの変更が有ればサブスクライブしているので自動で書き換えを行います。

hooks/userState
import { useCallback} from 'react';
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
import { calcAge } from '@/utils'
import { User } from '@/types';

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

const userAgeSelector = selector({
  key: 'userState/userAgeSelector',
  get: ({ get }) => {
    const user = get(userState);
    if (!user) return null;
    // 生年月日のDateから年齢を計算する関数がある前提
    const age: number = calcAge(user.birthday);
    return age;
  },
});


export const useUser = () => {
  const [user, setUser] = useRecoilState(userState);
  const age: number = useRecoilValue(userAgeSelector);

  return {
    state: user,
    set: setUser,
    age
  };
};

引数を与えて計算結果を返すgetterの作成

定義側

hooks/userState
import { useCallback} from 'react';
import { atom, useRecoilState } from 'recoil';
import format from 'date-fns/format';
import { User } from '@/types';

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

export const useUser = () => {
  const [user, setUser] = useRecoilState(userState);

  // 誕生日を指定のフォーマットに変換した文字列を返却する
  const getStrBirthday = useCallback(
    (strFormat?: string = "YYYY年M月d日") => {
     return format(user.birthday, strFormat);
    },
    [user.birthday]
  );

  return {
    state: user,
    set: setUser,
    getStrBirthday
  };
};

component側

import React, { FC, useMemo } from 'react';
import { useUser } from '@/hooks/userState';

const UserName: FC = () => {
  const { state, getStrBirthday } = useUser();
  const birthbay = useMemo(
    () => getStrBirthday("YYYY/M/d/")
    , [getStrBirthday]
  );
  
  return (
    <div>
      <p>{state.name ?? 'No Data'}</p>
      <p>誕生日: {birthbay}</p>
    </div>
  )
}

Promiseを返すgetterを作成する

定義側

hooks/userState
import { useCallback} from 'react';
import { atom, useRecoilState } from 'recoil';
import { fetchArticlesByUserId } from '@/api'
import { User, Article } from '@/types';

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

export const useUser = () => {
  const [user, setUser] = useRecoilState(userState);
  
  // こんな関数が欲しいかどうかは置いといて、例として見てください。てへ
  const fetchMyArticles = useCallback(
    async () => {
     const options = { limit: 10 };
      const articles: Article[] = await fetchArticlesByUserId(
        user.id,
        options
      );
      return articles;
    },
    [user.id]
  );

  return {
    state: user,
    set: setUser,
    fetchMyArticles
  };
};

component側

import React, { FC, useState, useEffect } from 'react';
import { useUser } from '@/hooks/userState';
import { Article } from '@/types';

const initialArticles: Article[] = [];

const UserName: FC = () => {
  const { state, fetchMyArticles } = useUser();
  const [articles, setArticles] = useState(initialArticles)
  
  useEffect(() => {
    fetchMyArticles().then((data) => setArticles(data));
  }, [fetchMyArticles]);

  return <div>
    <p>{state.name ?? 'No Data'}</p>

    // 表示に関してはテキトーに書きました。
    {articles.map((article) => (
     <p>{article.body}</p> 
    ))}
  </div>
}

Discussion