ソースを参照

WIP impl pagination with useSWRInfinite

Yuki Takei 2 年 前
コミット
b161382d82

+ 3 - 2
packages/remark-lsx/src/components/Lsx.tsx

@@ -81,9 +81,10 @@ const LsxSubstance = React.memo(({
       return <></>;
     }
 
-    const nodeTree = generatePageNodeTree(prefix, data.pages);
+    const nodeTree = generatePageNodeTree(prefix, data.flatMap(d => d.pages));
+    const basisViewersCount = data.at(-1)?.toppageViewersCount;
 
-    return <LsxListView nodeTree={nodeTree} lsxContext={lsxContext} basisViewersCount={data.toppageViewersCount} />;
+    return <LsxListView nodeTree={nodeTree} lsxContext={lsxContext} basisViewersCount={basisViewersCount} />;
   }, [data, lsxContext, prefix]);
 
   return (

+ 9 - 0
packages/remark-lsx/src/interfaces/api.ts

@@ -1,3 +1,5 @@
+import { IPageHasId } from '@growi/core';
+
 export type LsxApiOptions = {
   depth?: string,
   filter?: string,
@@ -12,3 +14,10 @@ export type LsxApiParams = {
   limit?: number,
   options?: LsxApiOptions,
 }
+
+export type LsxApiResponseData = {
+  pages: IPageHasId[],
+  cursor: number,
+  total: number,
+  toppageViewersCount: number,
+}

+ 3 - 3
packages/remark-lsx/src/server/routes/list-pages/generate-base-query.ts

@@ -1,8 +1,8 @@
-import { IPage, IUser } from '@growi/core';
+import type { IPageHasId, IUser } from '@growi/core';
 import { model } from 'mongoose';
 import type { Document, Query } from 'mongoose';
 
-export type PageQuery = Query<IPage[], Document>;
+export type PageQuery = Query<IPageHasId[], Document>;
 
 export type PageQueryBuilder = {
   query: PageQuery,
@@ -11,7 +11,7 @@ export type PageQueryBuilder = {
 };
 
 export const generateBaseQuery = async(pagePath: string, user: IUser): Promise<PageQueryBuilder> => {
-  const Page = model<IPage>('Page');
+  const Page = model<IPageHasId>('Page');
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   const PageAny = Page as any;
 

+ 6 - 2
packages/remark-lsx/src/server/routes/list-pages/index.spec.ts

@@ -3,6 +3,8 @@ import type { Request, Response } from 'express';
 import createError from 'http-errors';
 import { mock } from 'vitest-mock-extended';
 
+import { LsxApiResponseData } from '../../../interfaces/api';
+
 import type { PageQuery } from './generate-base-query';
 
 import { listPages } from '.';
@@ -84,11 +86,13 @@ describe('listPages', () => {
       expect(mocks.addNumConditionMock).toHaveBeenCalledOnce();
       expect(mocks.addSortConditionMock).toHaveBeenCalledOnce();
       expect(resMock.status).toHaveBeenCalledOnce();
-      expect(resStatusMock.send).toHaveBeenCalledWith({
+      const expectedResponseData: LsxApiResponseData = {
         pages: [pageMock],
+        cursor: 1,
         total: 9,
         toppageViewersCount: 99,
-      });
+      };
+      expect(resStatusMock.send).toHaveBeenCalledWith(expectedResponseData);
     });
 
     it('returns 500 HTTP response when an unexpected error occured', async() => {

+ 10 - 3
packages/remark-lsx/src/server/routes/list-pages/index.ts

@@ -5,7 +5,7 @@ import escapeStringRegexp from 'escape-string-regexp';
 import type { Request, Response } from 'express';
 import createError, { isHttpError } from 'http-errors';
 
-import { LsxApiParams } from '../../../interfaces/api';
+import type { LsxApiParams, LsxApiResponseData } from '../../../interfaces/api';
 
 import { addDepthCondition } from './add-depth-condition';
 import { addNumCondition } from './add-num-condition';
@@ -99,14 +99,21 @@ export const listPages = async(req: Request & { user: IUser }, res: Response): P
       query = addExceptCondition(query, pagePath, options.except);
     }
 
+    // get total num before adding num/sort conditions
+    const total = await query.clone().count();
+
     // num
     query = addNumCondition(query, offset, limit);
     // sort
     query = addSortCondition(query, options?.sort, options?.reverse);
 
     const pages = await query.exec();
-    const total = await query.clone().count();
-    return res.status(200).send({ pages, total, toppageViewersCount });
+    const cursor = (offset ?? 0) + pages.length;
+
+    const responseData: LsxApiResponseData = {
+      pages, cursor, total, toppageViewersCount,
+    };
+    return res.status(200).send(responseData);
   }
   catch (error) {
     if (isHttpError(error)) {

+ 28 - 21
packages/remark-lsx/src/stores/lsx/lsx.ts

@@ -1,35 +1,40 @@
-import type { IPageHasId } from '@growi/core';
 import axios from 'axios';
-import useSWR, { SWRResponse } from 'swr';
+import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 
-import { LsxApiParams } from '../../interfaces/api';
+import type { LsxApiParams, LsxApiResponseData } from '../../interfaces/api';
 
 import { parseNumOption } from './parse-num-option';
 
-type LsxResponse = {
-  pages: IPageHasId[],
-  total: number,
-  toppageViewersCount: number,
-}
 
-export const useSWRxLsx = (pagePath: string, options?: Record<string, string|undefined>, isImmutable?: boolean): SWRResponse<LsxResponse, Error> => {
-  return useSWR(
-    ['/_api/lsx', pagePath, options, isImmutable],
-    async([endpoint, pagePath, options]) => {
-      try {
-        // parse num option
-        const offsetAndLimit = options?.num != null
-          ? parseNumOption(options.num)
-          : null;
-        delete options?.num;
+export const useSWRxLsx = (
+    pagePath: string, options?: Record<string, string|undefined>, isImmutable?: boolean,
+): SWRInfiniteResponse<LsxApiResponseData, Error> => {
+
+  // parse num option
+  const initialOffsetAndLimit = options?.num != null
+    ? parseNumOption(options.num)
+    : null;
+  delete options?.num;
 
+  return useSWRInfinite(
+    (pageIndex, previousPageData) => {
+      if (previousPageData != null && previousPageData.pages.length === 0) return null;
+
+      if (pageIndex === 0 || previousPageData == null) {
+        return ['/_api/lsx', pagePath, options, initialOffsetAndLimit?.offset, initialOffsetAndLimit?.limit, isImmutable];
+      }
+
+      return ['/_api/lsx', pagePath, options, previousPageData.cursor, initialOffsetAndLimit?.limit, isImmutable];
+    },
+    async([endpoint, pagePath, options, offset, limit]) => {
+      try {
         const params: LsxApiParams = {
           pagePath,
-          offset: offsetAndLimit?.offset,
-          limit: offsetAndLimit?.limit,
+          offset,
+          limit,
           options,
         };
-        const res = await axios.get<LsxResponse>(endpoint, { params });
+        const res = await axios.get<LsxApiResponseData>(endpoint, { params });
         return res.data;
       }
       catch (err) {
@@ -44,6 +49,8 @@ export const useSWRxLsx = (pagePath: string, options?: Record<string, string|und
       revalidateIfStale: !isImmutable,
       revalidateOnFocus: !isImmutable,
       revalidateOnReconnect: !isImmutable,
+      revalidateFirstPage: false,
+      revalidateAll: false,
     },
   );
 };

+ 1 - 0
packages/remark-lsx/vite.client.config.ts

@@ -29,6 +29,7 @@ export default defineConfig({
         'next/link',
         'unified',
         'swr',
+        /^swr\/.*/,
         /^hast-.*/,
         /^unist-.*/,
         /^@growi\/.*/,