前回の記事でサーバー側の実装が完了したので、今回はアプリ側を対応していきます。
Pull Request
実装
まず、ページング用のCustom Hooksを作成
■ src/hooks/useItemsInPeriodPaging.tsx
import { useState, useEffect, useCallback } from 'react'; import { ItemsInPeriodQuery as Query, ItemsInPeriodQueryHookResult as QueryHookResult, } from 'queries/api/index'; export type Item = NonNullable<EdgesNode<Query['itemsInPeriod']>>; export type ItemsInPeriodPageInfo = PageInfo<Query['itemsInPeriod']>; type Props = QueryHookResult; type Option = { merge?: boolean; }; const useItemsInPeriodPaging = ( props: Props, option: Option = { merge: false } ) => { const { data, loading } = props; const [nodes, setNodes] = useState<Item[]>([]); const pageInfo = getPageInfo(data); useEffect(() => { if (!data?.itemsInPeriod?.edges) return; if (loading) return; if (option.merge) { const tmp = new Set(); setNodes((s) => [...s, ...getNodes(data)].filter((n) => !tmp.has(n.id) && tmp.add(n.id)) ); } else { setNodes(getNodes(data)); } }, [data, loading, option.merge]); const reset = useCallback(() => { setNodes([]); }, []); return { items: nodes, pageInfo: pageInfo, reset, }; }; export default useItemsInPeriodPaging; const getPageInfo = (data: Query | undefined) => data?.itemsInPeriod?.pageInfo || ({ endCursor: '', hasNextPage: false, } as ItemsInPeriodPageInfo); const getNodes = (data: Query | undefined) => (data?.itemsInPeriod?.edges || []).map((w) => w?.node) as Item[];
props.data(GraphQLのResponseの値)に変更があったらnodesに値をマージして追加
const { data, loading } = props; const [nodes, setNodes] = useState<Item[]>([]); const pageInfo = getPageInfo(data); useEffect(() => { if (!data?.itemsInPeriod?.edges) return; if (loading) return; if (option.merge) { const tmp = new Set(); setNodes((s) => [...s, ...getNodes(data)].filter((n) => !tmp.has(n.id) && tmp.add(n.id)) ); } else { setNodes(getNodes(data)); } }, [data, loading, option.merge]);
上記のCustom Hooksを以下のように使用
■ src/components/pages/Memoir/Connected.tsx
import React, { memo, useCallback, useState } from 'react'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; import 'dayjs/locale/ja'; import { useItemsInPeriodQuery } from 'queries/api/index'; import useItemsInPeriodPaging from 'hooks/useItemsInPeriodPaging'; import Plain from './Plain'; dayjs.locale('ja'); dayjs.extend(advancedFormat); type Props = { startDate: string; endDate: string; }; export type State = { after: string | null; }; export type ConnectedType = { startDate: string; endDate: string; onItem: () => void; onLoadMore: (after: string | null) => void; }; const initialState = () => ({ after: '', }); const Connected: React.FC<Props> = (props) => { const [state, setState] = useState<State>(initialState()); const queryResult = useItemsInPeriodQuery({ variables: { input: { startDate: props.startDate, endDate: props.endDate, first: 8, after: state.after, }, }, }); const { items, pageInfo } = useItemsInPeriodPaging(queryResult, { merge: true, }); const onLoadMore = useCallback((after: string | null) => { setState((s) => ({ ...s, after, })); }, []); const onItem = useCallback(() => {}, []); return ( <Plain startDate={props.startDate} endDate={props.endDate} items={items} pageInfo={pageInfo} onLoadMore={onLoadMore} loading={queryResult.loading} error={queryResult.error} onItem={onItem} /> ); }; export default memo(Connected);
onLoadMoreが実行されると、variablesのafterが更新されて、次のカーソル位置のデータが取得され、上記のCustom Hooksでマージされた値を返す方式なっている
const onLoadMore = useCallback((after: string | null) => { setState((s) => ({ ...s, after, })); }, []);
インフィニティスクロールの実装なのでonLoadMore実行はFlatListのonEndReachedの設定。 pageInfo.hasNextPage == true(次のページが存在する)場合にonLoadMoreを発火させてページングを実装
■ src/components/organisms/Memoir/DateCards.tsx
const handleLoadMore = useCallback(() => { if (!props.pageInfo.hasNextPage) return; if (props.loading) return; props.onLoadMore(props?.pageInfo.endCursor); }, [props]); return ( <View style={styles.root}> <FlatList<RenderedItem> keyExtractor={(_, index) => `search_${index}`} data={data} renderItem={renderItemCall} ListFooterComponent={<ListFooterComponent loading={props.loading} />} onEndReachedThreshold={0.8} onEndReached={handleLoadMore} /> </View> );
動作
実装の動作は以下のようになりました