グローバルステータス管理の悩み-ワッフルカードリスト(feat.context)



ワッフルカードサービスが気になるなら!
ワッフルカードの表示サービス:https://waffle-card.com/
ワッフル寄宿センター見学:https://github.com/waffle-card
😱 ワッフルカードのリスト効率が低下
ワッフルカードのホームページには3つのラベルがあります.
  • 오늘의 카드:ワッフルカードのリストをすべてレンダリングします.
  • 나의 카드:ワッフルカードをレンダリング.
  • 관심 카드:レンダリングされたワッフルカードのリスト.

  • ホームページを最初に実装する場合、ラベルを1つ押すたびに、各ラベルは独自のネットワーク要求を発行し、対応するラベルのワッフルカードリストデータを受信することによって実装されます.そして、와플카드 리스트状態を更新した後、Propで子供に伝えます.
    ビデオをよく見ると、ラベルをクリックするたびにネットワークリクエストが発行され、ロードされた螺旋線がレンダリングされます.

    上記の構成の問題点は次のとおりです.
  • 複雑な支柱に穴をあけ、素子間の構造を複雑にする.
  • 関心事項は分離していない.すなわち、ステータス管理およびネットワーク要求をコンポーネントで管理し、コードが不完全である.
  • ラベルを交換したりページから飛び出したりするたびに、ネットワーク要求が非効率的に発行される.
  • 一番大切なのは毎回ロードされるので使い勝手が悪い.
  • 😎 ワッフルリストの構造とパフォーマンスの向上
    上記の問題を解決するために、設計を次の構造に変更します.
  • 와플카드 리스트をグローバル状態に分ける.
  • 홈페이지와플카드 리스트ショップにデータを要求する.
  • 와플카드 리스트店舗にキャッシュされたデータがあれば、キャッシュされたデータを返します.
  • キャッシュされていないデータの場合와플카드 리스트ストアからネットワーク要求があり、受信したデータをストアに格納してキャッシュし、データを返す.

  • その結果,ラベルをクリックしたりページから飛び出したりするたびにキャッシュされたデータを優先的に利用し,盲目的なネットワーク要求を回避することができる.
    また、ステータスをグローバルファイルとして使用することで、コンポーネントの興味を分離し、Propドリルを回避して再構築のメリットを得ることができます.
    最終的には、より高速なレンダリングが行われ、ユーザーの可用性が向上します.

    ラベルを押すたびにロードが見えず、ページを離れて戻ってくるたびにキャッシュされたデータをうまく利用できます.
    ワッフルカードのグローバルステータス管理コードの表示
    前の記事で述べたrecoilのキャッシュの問題でcontextを使用してカスタマイズし実現しました.
    まずコード全体を置いてから始めます.次の説明では、追加コードを削除し、コアコードのみについて説明します.
    import { createContext, useCallback, useContext, useState } from 'react';
    import { waffleCardApi } from '@/apis';
    import { userState } from '@/recoils';
    import { useRecoilValue } from 'recoil';
    import { WaffleCardType } from '@/types';
    
    const cachedWaffleCards: { [type: string]: WaffleCardType[] | null } = {
      total: null,
      my: null,
      like: null,
    };
    
    const WaffleCardsStateContext = createContext<WaffleCardType[] | null>([]);
    const WaffleCardsDispatchContext = createContext<{
      setWaffleCardsByType: (type: string, waffleCards: WaffleCardType[]) => void;
      refreshWaffleCards: (type?: string) => void;
    }>({
      setWaffleCardsByType: () => {
        return;
      },
      refreshWaffleCards: () => {
        return;
      },
    });
    
    interface WaffleCardsProviderProps {
      children: React.ReactElement | React.ReactElement[];
    }
    
    export const WaffleCardsProvider = ({ children }: WaffleCardsProviderProps) => {
      const [waffleCards, setWaffleCards] = useState<WaffleCardType[] | null>([]);
      const user = useRecoilValue(userState);
    
      const setWaffleCardsByType = useCallback(
        async (type, options = {}) => {
          if (!user && type !== 'total') {
            setWaffleCards(() => []);
            return;
          }
    
          if (options?.cached && cachedWaffleCards[type]) {
            setWaffleCards(() => cachedWaffleCards[type]);
            return;
          }
    
          const waffleCardsCommand: {
            [command: string]: () => Promise<WaffleCardType[]>;
          } = {
            total: async () => {
              const { data: waffleCards } = await waffleCardApi.getWaffleCards();
              return waffleCards;
            },
            my: async () => {
              if (cachedWaffleCards.total && user) {
                return cachedWaffleCards.total.filter(
                  waffleCard => waffleCard.user.id === user.id,
                );
              }
    
              const { data: waffleCards } = await waffleCardApi.getMyWaffleCard();
              return waffleCards;
            },
            like: async () => {
              if (cachedWaffleCards.total && user) {
                return cachedWaffleCards.total.filter(waffleCard =>
                  waffleCard.likeUserIds.includes(user.id),
                );
              }
    
              const { data: waffleCards } =
                await waffleCardApi.getMyLikedWaffleCards();
              return waffleCards;
            },
          };
    
          try {
            const waffleCards = await waffleCardsCommand[type]();
    
            cachedWaffleCards[type] = [...waffleCards];
    
            setWaffleCards(() => [...waffleCards]);
          } catch (error: any) {
            console.error(`in WaffleCards Recoil: ${error.message}`);
            return [];
          }
        },
        [user],
      );
    
      const refreshWaffleCards = async (type = 'total') => {
        Object.keys(cachedWaffleCards).forEach(async type => {
          cachedWaffleCards[type] = null;
        });
    
        await setWaffleCardsByType(type);
      };
    
      return (
        <WaffleCardsStateContext.Provider value={waffleCards}>
          <WaffleCardsDispatchContext.Provider
            value={{
              setWaffleCardsByType,
              refreshWaffleCards,
            }}
          >
            {children}
          </WaffleCardsDispatchContext.Provider>
        </WaffleCardsStateContext.Provider>
      );
    };
    
    export const useWaffleCardsState = () => useContext(WaffleCardsStateContext);
    export const useWaffleCardsDispatch = () =>
      useContext(WaffleCardsDispatchContext);
    まず、ストレージロール内のオブジェクトを作成してデータをキャッシュします.
    const cachedWaffleCards: { [type: string]: WaffleCardType[] | null } = {
      total: null,
      my: null,
      like: null,
    };
    ワッフルカードカタログはラベルによって3種類に分けてキャッシュされます.
    そしてWaffleCardsProviderの骨格をつかむ
    export const WaffleCardsProvider = ({ children }: WaffleCardsProviderProps) => {
      const [waffleCards, setWaffleCards] = useState<WaffleCardType[] | null>([]);
    
      const setWaffleCardsByType = async (type, options = {}) => {
      };
    
      const refreshWaffleCards = async (type = 'total') => {
      };
    
      return (
        <WaffleCardsStateContext.Provider value={waffleCards}>
          <WaffleCardsDispatchContext.Provider
            value={{
              setWaffleCardsByType,
              refreshWaffleCards,
            }}
          >
            {children}
          </WaffleCardsDispatchContext.Provider>
        </WaffleCardsStateContext.Provider>
      );
    };
    WaffleCardsProvider内部には2つのコアメソッドがある.setWaffleCardsByType
  • waffleCards更新状態の関数

  • タイプとオプション({cached: boolean})を因子とする.

  • タイプに応じて対応するネットワークリクエストを発行した後、cachedWaffleCardsオブジェクトに格納し、waffleCardsステータスを更新する.
  • {cached: true}が選択されている場合は、キャッシュされたデータを優先的にチェックし、キャッシュされたデータがあれば、ネットワーク要求を行わず、既存のcachedWaffleCardsオブジェクトに格納されているデータを利用して更新waffleCardsステータスを更新する.キャッシュされたデータがない場合は、ネットワーク要求が行われます.
  • refreshWaffleCardsステータスを更新するための関数ですネットワークリクエスト、更新、更新ステータスの削除、リクエストに使用する関数以下に示す2つの方法を実装する.
    export const WaffleCardsProvider = ({ children }: WaffleCardsProviderProps) => {
      const [waffleCards, setWaffleCards] = useState<WaffleCardType[] | null>([]);
    
      const setWaffleCardsByType = async (type, options = {}) => {
        // 만약 {cached: boolean}이면 캐싱된 데이터로 waffleCards 상태 업데이트
        if (options?.cached && cachedWaffleCards[type]) {
           setWaffleCards(() => cachedWaffleCards[type]);
           return;
        }
        
        // type별로 네트워 요청을 분기처리 하기 위한 객체
        const waffleCardsCommand = {
          total: async () => {
            const { data: waffleCards } = await waffleCardApi.getWaffleCards();
            return waffleCards;
          },
          my: async () => {
            const { data: waffleCards } = await waffleCardApi.getMyWaffleCard();
            return waffleCards;
          },
          like: async () => {
            const { data: waffleCards } =
                  await waffleCardApi.getMyLikedWaffleCards();
            return waffleCards;
          },
        };
    
        // type별로 네트워크 요청후 waffleCards 상태 업데이트
        try {
          const waffleCards = await waffleCardsCommand[type]();
          cachedWaffleCards[type] = [...waffleCards];
          setWaffleCards(() => [...waffleCards]);
        } catch (error: any) {
          console.error(`in WaffleCards Recoil: ${error.message}`);
          return [];
        }
      };
    
      // 캐싱된 데이터를 삭제하고 네트워크 요청을 하여  waffleCards 상태를 최신으로 갱신
      const refreshWaffleCards = async (type = 'total') => {
        Object.keys(cachedWaffleCards).forEach(async type => {
          cachedWaffleCards[type] = null;
        });
    
        await setWaffleCardsByType(type);
      };
    
      return (
        <WaffleCardsStateContext.Provider value={waffleCards}>
          <WaffleCardsDispatchContext.Provider
            value={{
              setWaffleCardsByType,
              refreshWaffleCards,
            }}
          >
            {children}
          </WaffleCardsDispatchContext.Provider>
        </WaffleCardsStateContext.Provider>
      );
    };
    recoilのようなクリーンな実装ではないが,元のターゲットショップでデータに関するネットワークリクエストを管理しキャッシュの実装を完了した.
    振り返る
    contextの最も致命的な欠点は固定的なパターンがないことだと思います.Reduxはフラックスモードという固定モードがあり,誰がコードを記述してもコード量が大きくても理解は難しくないと考えている.(もちろん...私はまだ...)
    しかしcontextには固定的なパターンがないため,著者の主観が強く溶け込み,うっかりすると他人に理解されにくい可能性がある.リダイレクトモードはuseredcherを用いて実現できる人もいれば,userStateを単純に用いて実現できる人もいれば,外部モジュールを組み合わせることで実現できる人もいる.
    私のコードを振り返ると、固定的なパターンはないと思います.まだパターンに対する経験や知識がないので、私が考えている論理だけを体現しているような気がします.
    次回は反応式照会を利用したいと思います.