前回の記事でサーバー側の実装が完了したので、今回はアプリ側を対応していきます。
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>
);
動作
実装の動作は以下のようになりました