aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/hooks/usePaginatedArticles.ts
blob: 56098d7736923f464d104e0b110594e541efe09b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import { useCallback, useEffect, useState } from "react";
import { useClient } from "urql";
import type {
	GetReadArticlesQuery,
	GetUnreadArticlesQuery,
} from "../graphql/generated/graphql";
import {
	GetReadArticlesDocument,
	GetUnreadArticlesDocument,
} from "../graphql/generated/graphql";

type ArticleType =
	| GetUnreadArticlesQuery["unreadArticles"]["articles"][number]
	| GetReadArticlesQuery["readArticles"]["articles"][number];

interface UsePaginatedArticlesOptions {
	isReadView: boolean;
	feedId: string | null;
}

interface UsePaginatedArticlesResult {
	articles: ArticleType[];
	hasNextPage: boolean;
	loading: boolean;
	loadingMore: boolean;
	loadMore: () => void;
	error: Error | null;
}

export function usePaginatedArticles({
	isReadView,
	feedId,
}: UsePaginatedArticlesOptions): UsePaginatedArticlesResult {
	const client = useClient();
	const [articles, setArticles] = useState<ArticleType[]>([]);
	const [hasNextPage, setHasNextPage] = useState(false);
	const [endCursor, setEndCursor] = useState<string | null>(null);
	const [loading, setLoading] = useState(true);
	const [loadingMore, setLoadingMore] = useState(false);
	const [error, setError] = useState<Error | null>(null);

	const fetchArticles = useCallback(
		async (after: string | null, append: boolean) => {
			const variables: Record<string, unknown> = {};
			if (feedId) variables.feedId = feedId;
			if (after) variables.after = after;

			let connection: {
				articles: ArticleType[];
				pageInfo: { hasNextPage: boolean; endCursor?: string | null };
			} | null = null;

			if (isReadView) {
				const result = await client
					.query(GetReadArticlesDocument, variables, {
						additionalTypenames: ["Article"],
					})
					.toPromise();
				if (result.error) {
					setError(new Error(result.error.message));
					return;
				}
				connection = result.data?.readArticles ?? null;
			} else {
				const result = await client
					.query(GetUnreadArticlesDocument, variables, {
						additionalTypenames: ["Article"],
					})
					.toPromise();
				if (result.error) {
					setError(new Error(result.error.message));
					return;
				}
				connection = result.data?.unreadArticles ?? null;
			}

			if (connection) {
				setArticles((prev) =>
					append ? [...prev, ...connection.articles] : connection.articles,
				);
				setHasNextPage(connection.pageInfo.hasNextPage);
				setEndCursor(connection.pageInfo.endCursor ?? null);
				setError(null);
			}
		},
		[client, isReadView, feedId],
	);

	// Reset and fetch on feedId or view change
	useEffect(() => {
		setArticles([]);
		setEndCursor(null);
		setHasNextPage(false);
		setLoading(true);
		setError(null);
		fetchArticles(null, false).finally(() => setLoading(false));
	}, [fetchArticles]);

	const loadMore = useCallback(() => {
		if (!hasNextPage || loadingMore) return;
		setLoadingMore(true);
		fetchArticles(endCursor, true).finally(() => setLoadingMore(false));
	}, [fetchArticles, endCursor, hasNextPage, loadingMore]);

	return { articles, hasNextPage, loading, loadingMore, loadMore, error };
}