Просмотр исходного кода

Merge pull request #7336 from weseek/imprv/manual-mutation

imprv: Data mutation
Yuki Takei 3 лет назад
Родитель
Сommit
78ba14bd31

+ 2 - 2
packages/app/src/client/services/page-operation.ts

@@ -6,7 +6,7 @@ import urljoin from 'url-join';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { useCurrentPageId } from '~/stores/context';
 import { useEditingMarkdown, useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 
@@ -179,7 +179,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
 
 export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
   const { mutate: mutateCurrentPageId } = useCurrentPageId();
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);

+ 13 - 46
packages/app/src/components/DescendantsPageList.tsx

@@ -11,15 +11,14 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import {
-  useIsGuestUser, useIsSharedUser, useShowPageLimitationXL,
+  useIsGuestUser, useIsSharedUser,
 } from '~/stores/context';
-import { useIsTrashPage } from '~/stores/page';
 import {
-  usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
+  mutatePageTree,
   useSWRxPageInfoForList, useSWRxPageList,
 } from '~/stores/page-listing';
 
-import { ForceHideMenuItems, MenuItemType } from './Common/Dropdown/PageItemControl';
+import { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
 import PageList from './PageList/PageList';
 import PaginationWrapper from './PaginationWrapper';
 
@@ -37,7 +36,7 @@ const convertToIDataWithMeta = (page: IPageHasId): IDataWithMeta<IPageHasId> =>
   return { data: page };
 };
 
-export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
+const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
 
   const { t } = useTranslation();
 
@@ -52,10 +51,6 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
 
   let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfoForOperation>[] = [];
 
-  // for mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
-
   // initial data
   if (pagingResult != null) {
     // convert without meta at first
@@ -74,23 +69,22 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
       toastSuccess(t('deleted_pages_completely', { path }));
     }
 
-    advancePt();
+    mutatePageTree();
 
     if (onPagesDeleted != null) {
       onPagesDeleted(...args);
     }
-  }, [advancePt, onPagesDeleted, t]);
+  }, [onPagesDeleted, t]);
 
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
     toastSuccess(t('page_has_been_reverted', { path }));
 
-    advancePt();
-    advanceDpl();
+    mutatePageTree();
 
     if (onPagePutBacked != null) {
       onPagePutBacked(path);
     }
-  }, [advanceDpl, advancePt, onPagePutBacked, t]);
+  }, [onPagePutBacked, t]);
 
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
@@ -135,43 +129,18 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
 
 export type DescendantsPageListProps = {
   path: string,
+  limit?: number,
+  forceHideMenuItems?: ForceHideMenuItems,
 }
 
 export const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
-  const { path } = props;
+  const { path, limit, forceHideMenuItems } = props;
 
   const [activePage, setActivePage] = useState(1);
 
   const { data: isSharedUser } = useIsSharedUser();
 
-  const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage);
-
-  if (error != null) {
-    return (
-      <div className="my-5">
-        <div className="text-danger">{error.message}</div>
-      </div>
-    );
-  }
-
-  return (
-    <DescendantsPageListSubstance
-      pagingResult={pagingResult}
-      activePage={activePage}
-      setActivePage={setActivePage}
-      onPagesDeleted={() => mutate()}
-      onPagePutBacked={() => mutate()}
-    />
-  );
-};
-
-export const DescendantsPageListForCurrentPath = (): JSX.Element => {
-
-  const [activePage, setActivePage] = useState(1);
-
-  const { data: isTrashPage } = useIsTrashPage();
-  const { data: limit } = useShowPageLimitationXL();
-  const { data: pagingResult, error, mutate } = useSWRxDescendantsPageListForCurrrentPath(activePage, limit);
+  const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage, limit);
 
   if (error != null) {
     return (
@@ -181,8 +150,6 @@ export const DescendantsPageListForCurrentPath = (): JSX.Element => {
     );
   }
 
-  const forceHideMenuItems = isTrashPage ? [MenuItemType.RENAME] : undefined;
-
   return (
     <DescendantsPageListSubstance
       pagingResult={pagingResult}
@@ -190,7 +157,7 @@ export const DescendantsPageListForCurrentPath = (): JSX.Element => {
       setActivePage={setActivePage}
       forceHideMenuItems={forceHideMenuItems}
       onPagesDeleted={() => mutate()}
+      onPagePutBacked={() => mutate()}
     />
   );
-
 };

+ 2 - 8
packages/app/src/components/DescendantsPageListModal.tsx

@@ -20,15 +20,9 @@ import TimeLineIcon from './Icons/TimeLineIcon';
 
 import styles from './DescendantsPageListModal.module.scss';
 
-const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
-  const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
-  return <DescendantsPageList {...props}/>;
-};
+const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
 
-const PageTimeline = (): JSX.Element => {
-  const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
-  return <PageTimeline />;
-};
+const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
 
 export const DescendantsPageListModal = (): JSX.Element => {
   const { t } = useTranslation();

+ 2 - 2
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -24,7 +24,7 @@ import {
   usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
@@ -200,7 +200,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const router = useRouter();
 
   const { data: shareLinkId } = useShareLinkId();
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
   const { data: currentPathname } = useCurrentPathname();
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');

+ 11 - 4
packages/app/src/components/NotFoundPage.tsx

@@ -3,19 +3,26 @@ import React, { useMemo } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
+import { DescendantsPageList } from './DescendantsPageList';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import { PageTimeline } from './PageTimeline';
 
-const NotFoundPage = (): JSX.Element => {
+
+type NotFoundPageProps = {
+  path: string,
+}
+
+const NotFoundPage = (props: NotFoundPageProps): JSX.Element => {
   const { t } = useTranslation();
 
+  const { path } = props;
+
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
         Icon: PageListIcon,
-        Content: DescendantsPageListForCurrentPath,
+        Content: () => <DescendantsPageList path={path} />,
         i18n: t('page_list'),
         index: 0,
       },
@@ -26,7 +33,7 @@ const NotFoundPage = (): JSX.Element => {
         index: 1,
       },
     };
-  }, [t]);
+  }, [path, t]);
 
   return (
     <div className="d-edit-none">

+ 1 - 8
packages/app/src/components/Page/PageContents.tsx

@@ -1,14 +1,11 @@
 import React, { useEffect } from 'react';
 
-import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
 import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useCurrentPathname } from '~/stores/context';
-import { useEditingMarkdown } from '~/stores/editor';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { registerGrowiFacade } from '~/utils/growi-facade';
@@ -23,11 +20,7 @@ const logger = loggerFactory('growi:Page');
 export const PageContents = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const { data: currentPathname } = useCurrentPathname();
-  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
-
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { data: currentPage } = useSWRxCurrentPage();
   const updateStateAfterSave = useUpdateStateAfterSave(currentPage?._id);
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();

+ 4 - 4
packages/app/src/components/Page/PageView.tsx

@@ -69,15 +69,15 @@ export const PageView = (props: Props): JSX.Element => {
       return <NotCreatablePage />;
     }
     if (isNotFound) {
-      return <NotFoundPage />;
+      return <NotFoundPage path={pagePath} />;
     }
-  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound]);
+  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound, pagePath]);
 
   const sideContents = !isNotFound && !isNotCreatable
     ? (
       <PageSideContents page={page} />
     )
-    : <></>;
+    : null;
 
   const footerContents = !isIdenticalPathPage && !isNotFound && page != null
     ? (
@@ -91,7 +91,7 @@ export const PageView = (props: Props): JSX.Element => {
         <PageContentFooter page={page} />
       </>
     )
-    : <></>;
+    : null;
 
   const isUsersHomePagePath = isUsersHomePage(pagePath);
 

+ 10 - 10
packages/app/src/components/PageEditor.tsx

@@ -7,7 +7,7 @@ import nodePath from 'path';
 
 
 import {
-  IPageHasId, PageGrant, pathUtils,
+  IPageHasId, pathUtils,
 } from '@growi/core';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
@@ -25,15 +25,16 @@ import {
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsNotFound, useIsIndentSizeForced,
 } from '~/stores/context';
 import {
-  useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
+  useCurrentIndentSize, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
   useIsConflict,
   useEditingMarkdown,
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
-import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { usePageTreeTermManager } from '~/stores/page-listing';
-import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import {
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+} from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
@@ -74,7 +75,8 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: pageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
@@ -90,7 +92,6 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
-  const { advance: advancePt } = usePageTreeTermManager();
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -209,7 +210,7 @@ const PageEditor = React.memo((): JSX.Element => {
       );
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-      advancePt();
+      mutatePageTree();
 
       return page;
     }
@@ -227,8 +228,7 @@ const PageEditor = React.memo((): JSX.Element => {
       return null;
     }
 
-  // eslint-disable-next-line max-len
-  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId, advancePt]);
+  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {

+ 11 - 10
packages/app/src/components/PageEditorByHackmd.tsx

@@ -24,8 +24,10 @@ import {
 import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
-import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { usePageTreeTermManager } from '~/stores/page-listing';
+import {
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+} from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
   EditorMode,
@@ -64,12 +66,12 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: grantData } = useSelectedGrant();
   const { data: hackmdUri } = useHackmdUri();
   const saveOrUpdate = useSaveOrUpdate();
-  const { advance: advancePt } = usePageTreeTermManager();
 
   const { returnPathForURL } = pathUtils;
 
   // pageData
-  const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
+  const { data: pageData } = useSWRxCurrentPage();
+  const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
   const revision = pageData?.revision;
 
   const [isInitialized, setIsInitialized] = useState(false);
@@ -131,7 +133,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         mutateIsHackmdDraftUpdatingInRealtime(false);
 
         // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-        advancePt();
+        mutatePageTree();
       }
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
@@ -141,7 +143,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
     }
   // eslint-disable-next-line max-len
-  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime, advancePt]);
+  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime]);
 
   // set handler to save and reload Page
   useEffect(() => {
@@ -264,7 +266,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       mutateTagsInfo();
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-      advancePt();
+      mutatePageTree();
 
       mutateIsEnabledUnsavedWarning(false);
 
@@ -276,9 +278,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error.message);
     }
-  }, [
-    currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
-    saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, advancePt, mutateIsEnabledUnsavedWarning, t]);
+  // eslint-disable-next-line max-len
+  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t]);
 
   /**
    * onChange event of HackmdEditor handler

+ 3 - 2
packages/app/src/components/PageStatusAlert.tsx

@@ -9,7 +9,7 @@ import {
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
 } from '~/stores/hackmd';
 import { useConflictDiffModal } from '~/stores/modal';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
@@ -39,7 +39,8 @@ export const PageStatusAlert = (): JSX.Element => {
   const { data: remoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
-  const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
+  const { data: pageData } = useSWRxCurrentPage();
+  const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
   const revision = pageData?.revision;
 
   const refreshPage = useCallback(async() => {

+ 8 - 8
packages/app/src/components/PrivateLegacyPages.tsx

@@ -18,7 +18,7 @@ import { useCurrentUser } from '~/stores/context';
 import {
   ILegacyPrivatePage, usePrivateLegacyPagesMigrationModal,
 } from '~/stores/modal';
-import { usePageTreeTermManager, useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import { mutatePageTree, useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import {
   useSWRxSearch,
 } from '~/stores/search';
@@ -213,13 +213,12 @@ const PrivateLegacyPages = (): JSX.Element => {
   });
 
   const { data: migrationStatus, mutate: mutateMigrationStatus } = useSWRxV5MigrationStatus();
-  const { advance: advancePt } = usePageTreeTermManager();
 
   const searchInvokedHandler = useCallback((_keyword: string) => {
     mutateMigrationStatus();
     setKeyword(_keyword);
     setOffset(0);
-  }, []);
+  }, [mutateMigrationStatus]);
 
   const { open: openModal, close: closeModal } = usePrivateLegacyPagesMigrationModal();
   const { data: socket } = useGlobalSocket();
@@ -245,7 +244,7 @@ const PrivateLegacyPages = (): JSX.Element => {
       socket?.off(SocketEventName.PageMigrationSuccess);
       socket?.off(SocketEventName.PageMigrationError);
     };
-  }, [socket]);
+  }, [socket, t]);
 
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
     const instance = searchPageBaseRef.current;
@@ -315,10 +314,10 @@ const PrivateLegacyPages = (): JSX.Element => {
         closeModal();
         mutateMigrationStatus();
         mutate();
-        advancePt();
+        mutatePageTree();
       },
     );
-  }, [data, mutate, openModal, closeModal, mutateMigrationStatus]);
+  }, [data, openModal, t, closeModal, mutateMigrationStatus, mutate]);
 
   const pagingSizeChangedHandler = useCallback((pagingSize: number) => {
     setOffset(0);
@@ -381,7 +380,8 @@ const PrivateLegacyPages = (): JSX.Element => {
         {isAdmin && renderOpenModalButton()}
       </div>
     );
-  }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isControlEnabled, selectAllCheckboxChangedHandler, t]);
+  // eslint-disable-next-line max-len
+  }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isAdmin, isControlEnabled, renderOpenModalButton, selectAllCheckboxChangedHandler, t]);
 
   const searchControl = useMemo(() => {
     return (
@@ -455,7 +455,7 @@ const PrivateLegacyPages = (): JSX.Element => {
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             setOpenConvertModal(false);
             mutate();
-            advancePt();
+            mutatePageTree();
           }
           catch (errs) {
             if (errs.length === 1) {

+ 2 - 4
packages/app/src/components/PutbackPageModal.jsx

@@ -7,9 +7,8 @@ import {
 } from 'reactstrap';
 
 import { apiPost } from '~/client/util/apiv1-client';
-import { PathAlreadyExistsError } from '~/server/models/errors';
 import { usePutBackPageModal } from '~/stores/modal';
-import { usePageInfoTermManager } from '~/stores/page';
+import { mutateAllPageInfo } from '~/stores/page';
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
@@ -17,7 +16,6 @@ const PutBackPageModal = () => {
   const { t } = useTranslation();
 
   const { data: pageDataToRevert, close: closePutBackPageModal } = usePutBackPageModal();
-  const { advance: advancePi } = usePageInfoTermManager();
   const { isOpened, page } = pageDataToRevert;
   const { pageId, path } = page;
   const onPutBacked = pageDataToRevert.opts?.onPutBacked;
@@ -43,7 +41,7 @@ const PutBackPageModal = () => {
         page_id: pageId,
         recursively,
       });
-      advancePi();
+      mutateAllPageInfo();
 
       if (onPutBacked != null) {
         onPutBacked(response.page.path);

+ 2 - 5
packages/app/src/components/SearchPage/SearchPageBase.tsx

@@ -11,7 +11,7 @@ import { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsSearchServiceConfigured, useIsSearchServiceReachable } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
-import { usePageTreeTermManager } from '~/stores/page-listing';
+import { mutatePageTree } from '~/stores/page-listing';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
@@ -228,9 +228,6 @@ export const usePageDeleteModalForBulkDeletion = (
 
   const { open: openDeleteModal } = usePageDeleteModal();
 
-  // for PageTree mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-
   return () => {
     if (data == null) {
       return;
@@ -260,7 +257,7 @@ export const usePageDeleteModalForBulkDeletion = (
         else {
           toastSuccess(t('deleted_pages_completely', { path }));
         }
-        advancePt();
+        mutatePageTree();
 
         if (onDeleted != null) {
           onDeleted(...args);

+ 14 - 19
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -18,9 +18,9 @@ import { useCurrentUser, useIsContainerFluid } from '~/stores/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
-import { useDescendantsPageListForCurrentPathTermManager, usePageTreeTermManager } from '~/stores/page-listing';
+import { mutatePageList, mutatePageTree } from '~/stores/page-listing';
 import { useSearchResultOptions } from '~/stores/renderer';
-import { useFullTextSearchTermManager } from '~/stores/search';
+import { mutateSearching } from '~/stores/search';
 
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
@@ -91,11 +91,6 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const [isRevisionLoaded, setRevisionLoaded] = useState(false);
   const [isPageCommentLoaded, setPageCommentLoaded] = useState(false);
 
-  // for mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceFts } = useFullTextSearchTermManager();
-  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
-
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
     const scrollElement = scrollElementRef.current;
@@ -167,23 +162,23 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       toastSuccess(t('duplicated_pages', { fromPath }));
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
     };
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
-  }, [advanceDpl, advanceFts, advancePt, openDuplicateModal, t]);
+  }, [openDuplicateModal, t]);
 
   const renameItemClickedHandler = useCallback((pageToRename: IPageToRenameWithMeta) => {
     const renamedHandler: OnRenamedFunction = (path) => {
       toastSuccess(t('renamed_pages', { path }));
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
     };
     openRenameModal(pageToRename, { onRenamed: renamedHandler });
-  }, [advanceDpl, advanceFts, advancePt, openRenameModal, t]);
+  }, [openRenameModal, t]);
 
   const onDeletedHandler: OnDeletedFunction = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
     if (typeof pathOrPathsToDelete !== 'string') {
@@ -197,10 +192,10 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     else {
       toastSuccess(t('deleted_pages', { path }));
     }
-    advancePt();
-    advanceFts();
-    advanceDpl();
-  }, [advanceDpl, advanceFts, advancePt, t]);
+    mutatePageTree();
+    mutateSearching();
+    mutatePageList();
+  }, [t]);
 
   const deleteItemClickedHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });

+ 15 - 19
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -11,10 +11,9 @@ import {
   IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
 } from '~/interfaces/page';
 import { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser } from '~/stores/context';
-import { useSWRxPageInfoForList, usePageTreeTermManager } from '~/stores/page-listing';
-import { useFullTextSearchTermManager } from '~/stores/search';
+import { mutatePageTree, useSWRxPageInfoForList } from '~/stores/page-listing';
+import { mutateSearching } from '~/stores/search';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageListItemL } from '../PageList/PageListItemL';
@@ -44,10 +43,6 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
   const { data: isGuestUser } = useIsGuestUser();
   const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet, null, true, true);
 
-  // for mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceFts } = useFullTextSearchTermManager();
-
   const itemsRef = useRef<(ISelectable|null)[]>([]);
 
   // publish selectAll()
@@ -95,20 +90,21 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
   }
 
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  const duplicatedHandler : OnDuplicatedFunction = (fromPath, toPath) => {
+  const duplicatedHandler = useCallback((fromPath, toPath) => {
     toastSuccess(t('duplicated_pages', { fromPath }));
 
-    advancePt();
-    advanceFts();
-  };
+    mutatePageTree();
+    mutateSearching();
+  }, [t]);
 
-  const renamedHandler: OnRenamedFunction = (path) => {
+  const renamedHandler = useCallback((path) => {
     toastSuccess(t('renamed_pages', { path }));
 
-    advancePt();
-    advanceFts();
-  };
-  const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
+    mutatePageTree();
+    mutateSearching();
+  }, [t]);
+
+  const deletedHandler = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
     if (typeof pathOrPathsToDelete !== 'string') {
       return;
     }
@@ -121,9 +117,9 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     else {
       toastSuccess(t('deleted_pages', { path }));
     }
-    advancePt();
-    advanceFts();
-  };
+    mutatePageTree();
+    mutateSearching();
+  }, [t]);
 
   return (
     <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">

+ 17 - 21
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -16,11 +16,11 @@ import { useIsEnabledAttachTitleHeader } from '~/stores/context';
 import {
   IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
 } from '~/stores/modal';
-import { useCurrentPagePath, usePageInfoTermManager, useSWRxCurrentPage } from '~/stores/page';
+import { mutateAllPageInfo, useCurrentPagePath, useSWRMUTxCurrentPage } from '~/stores/page';
 import {
-  usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage, useDescendantsPageListForCurrentPathTermManager,
+  useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
 } from '~/stores/page-listing';
-import { useFullTextSearchTermManager } from '~/stores/search';
+import { mutateSearching } from '~/stores/search';
 import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
@@ -117,11 +117,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
 
   // for mutation
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceFts } = useFullTextSearchTermManager();
-  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
-  const { advance: advancePi } = usePageInfoTermManager();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
   const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
 
@@ -151,27 +147,27 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   }, [socket, ptDescCountMap, updatePtDescCountMap]);
 
   const onRenamed = useCallback((fromPath: string | undefined, toPath: string) => {
-    advancePt();
-    advanceFts();
-    advanceDpl();
+    mutatePageTree();
+    mutateSearching();
+    mutatePageList();
 
     if (currentPagePath === fromPath || currentPagePath === toPath) {
       mutateCurrentPage();
     }
-  }, [advanceDpl, advanceFts, advancePt, currentPagePath, mutateCurrentPage]);
+  }, [currentPagePath, mutateCurrentPage]);
 
   const onClickDuplicateMenuItem = useCallback((pageToDuplicate: IPageForPageDuplicateModal) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       toastSuccess(t('duplicated_pages', { fromPath }));
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
     };
 
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
-  }, [advanceDpl, advanceFts, advancePt, openDuplicateModal, t]);
+  }, [openDuplicateModal, t]);
 
   const onClickDeleteMenuItem = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
@@ -188,10 +184,10 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
         toastSuccess(t('deleted_pages', { path }));
       }
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
-      advancePi();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
+      mutateAllPageInfo();
 
       if (currentPagePath === pathOrPathsToDelete) {
         mutateCurrentPage();
@@ -200,7 +196,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     };
 
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
-  }, [advanceDpl, advanceFts, advancePi, advancePt, currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
+  }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
 
   // ***************************  Scroll on init ***************************
   const scrollOnInit = useCallback(() => {

+ 24 - 6
packages/app/src/components/TrashPageList.tsx

@@ -1,6 +1,7 @@
-import React, { FC, useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 
 import { toastSuccess } from '~/client/util/apiNotification';
 import {
@@ -9,13 +10,18 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { useShowPageLimitationXL } from '~/stores/context';
 import { useEmptyTrashModal } from '~/stores/modal';
-import { useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList } from '~/stores/page-listing';
+import { useSWRxPageInfoForList, useSWRxPageList } from '~/stores/page-listing';
 
+import { MenuItemType } from './Common/Dropdown/PageItemControl';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
+import { DescendantsPageListProps } from './DescendantsPageList';
 import EmptyTrashButton from './EmptyTrashButton';
 import PageListIcon from './Icons/PageListIcon';
 
+
+const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
+
+
 const convertToIDataWithMeta = (page) => {
   return { data: page };
 };
@@ -23,7 +29,7 @@ const convertToIDataWithMeta = (page) => {
 const useEmptyTrashButton = () => {
 
   const { data: limit } = useShowPageLimitationXL();
-  const { data: pagingResult, mutate: mutatePageLists } = useSWRxDescendantsPageListForCurrrentPath(1, limit);
+  const { data: pagingResult, mutate: mutatePageLists } = useSWRxPageList('/trash', 1, limit);
   const { t } = useTranslation();
   const { open: openEmptyTrashModal } = useEmptyTrashModal();
 
@@ -59,7 +65,19 @@ const useEmptyTrashButton = () => {
   return emptyTrashButton;
 };
 
-export const TrashPageList: FC = () => {
+const DescendantsPageListForTrash = (): JSX.Element => {
+  const { data: limit } = useShowPageLimitationXL();
+
+  return (
+    <DescendantsPageList
+      path="/trash"
+      limit={limit}
+      forceHideMenuItems={[MenuItemType.RENAME]}
+    />
+  );
+};
+
+export const TrashPageList = (): JSX.Element => {
   const { t } = useTranslation();
   const emptyTrashButton = useEmptyTrashButton();
 
@@ -67,7 +85,7 @@ export const TrashPageList: FC = () => {
     return {
       pagelist: {
         Icon: PageListIcon,
-        Content: DescendantsPageListForCurrentPath,
+        Content: DescendantsPageListForTrash,
         i18n: t('page_list'),
         index: 0,
       },

+ 1 - 1
packages/app/src/stores/editor.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from 'react';
 
 import { Nullable, withUtils, SWRResponseWithUtils } from '@growi/core';
-import useSWR, { SWRResponse } from 'swr';
+import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';

+ 1 - 2
packages/app/src/stores/middlewares/user.ts

@@ -1,8 +1,7 @@
 import { Middleware, SWRHook } from 'swr';
 
-import { IUserHasId } from '~/interfaces/user';
-
 import { apiv3Put } from '~/client/util/apiv3-client';
+import { IUserHasId } from '~/interfaces/user';
 
 export const checkAndUpdateImageUrlCached: Middleware = (useSWRNext: SWRHook) => {
   return (key, fetcher, config) => {

+ 64 - 55
packages/app/src/stores/page-listing.tsx

@@ -1,5 +1,7 @@
+import assert from 'assert';
+
 import { Nullable, HasObjectId } from '@growi/core';
-import useSWR, { SWRResponse } from 'swr';
+import useSWR, { Arguments, mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 
@@ -13,8 +15,6 @@ import {
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
 } from '../interfaces/page-listing-results';
 
-import { useCurrentPagePath } from './page';
-import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {
   const findAll = true;
@@ -43,48 +43,40 @@ export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHa
     },
   );
 };
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const useSWRxPageList = (
-    path: string | null, pageNumber?: number, termNumber?: number, limit?: number,
-): SWRResponse<IPagingResult<IPageHasId>, Error> => {
-
-  let key: [string, number|undefined] | null;
-  // if path not exist then the key is null
-  if (path == null) {
-    key = null;
-  }
-  else {
-    const pageListPath = `/pages/list?path=${path}&page=${pageNumber ?? 1}`;
-    // if limit exist then add it as query string
-    const requestPath = limit == null ? pageListPath : `${pageListPath}&limit=${limit}`;
-    key = [requestPath, termNumber];
-  }
 
-  return useSWR(
-    key,
-    ([endpoint]) => apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint).then((response) => {
-      return {
-        items: response.data.pages,
-        totalCount: response.data.totalCount,
-        limit: response.data.limit,
-      };
-    }),
+export const mutatePageList = async(): Promise<void[]> => {
+  return mutate(
+    key => Array.isArray(key) && key[0] === '/pages/list',
   );
 };
 
-export const useDescendantsPageListForCurrentPathTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'descendantsPageListForCurrentPathTermNumber');
-};
-
-export const useSWRxDescendantsPageListForCurrrentPath = (pageNumber?: number, limit?:number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: termNumber } = useDescendantsPageListForCurrentPathTermManager();
-
-  const path = currentPagePath == null || termNumber == null
-    ? null
-    : currentPagePath;
-
-  return useSWRxPageList(path, pageNumber, termNumber, limit);
+export const useSWRxPageList = (
+    path: string | null, pageNumber?: number, limit?: number,
+): SWRResponse<IPagingResult<IPageHasId>, Error> => {
+  return useSWR(
+    path == null
+      ? null
+      : ['/pages/list', path, pageNumber, limit],
+    ([endpoint, path, pageNumber, limit]) => {
+      const args = Object.assign(
+        { path, page: pageNumber ?? 1 },
+        // if limit exist then add it as query string
+        (limit != null) ? { limit } : {},
+      );
+
+      return apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint, args)
+        .then((response) => {
+          return {
+            items: response.data.pages,
+            totalCount: response.data.totalCount,
+            limit: response.data.limit,
+          };
+        });
+    },
+    {
+      keepPreviousData: true,
+    },
+  );
 };
 
 
@@ -133,10 +125,6 @@ export const useSWRxPageInfoForList = (
   };
 };
 
-export const usePageTreeTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'pageTreeTermManager');
-};
-
 export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
   return useSWRImmutable(
     '/page-listing/root',
@@ -145,27 +133,41 @@ export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
         rootPage: response.data.rootPage,
       };
     }),
+    {
+      keepPreviousData: true,
+    },
   );
 };
 
+const MUTATION_ID_FOR_PAGETREE = 'pageTree';
+const keyMatcherForPageTree = (key: Arguments): boolean => {
+  return Array.isArray(key) && key[0] === MUTATION_ID_FOR_PAGETREE;
+};
+export const mutatePageTree = async(): Promise<undefined[]> => {
+  return mutate(keyMatcherForPageTree);
+};
+
 export const useSWRxPageAncestorsChildren = (
     path: string | null,
 ): SWRResponse<AncestorsChildrenResult, Error> => {
-  const { data: termNumber } = usePageTreeTermManager();
+  const key = path ? [MUTATION_ID_FOR_PAGETREE, '/page-listing/ancestors-children', path] : null;
+
+  // take care of the degration
+  // see: https://github.com/weseek/growi/pull/7038
 
-  // HACKME: Consider using global mutation from useSWRConfig and not to use term number -- 2022/12/08 @hakumizuki
-  const prevTermNumber = termNumber ? termNumber - 1 : 0;
-  const prevSWRRes = useSWRImmutable(path ? [`/page-listing/ancestors-children?path=${path}`, prevTermNumber] : null);
+  if (key != null) {
+    assert(keyMatcherForPageTree(key));
+  }
 
   return useSWRImmutable(
-    path ? [`/page-listing/ancestors-children?path=${path}`, termNumber] : null,
-    ([endpoint]) => apiv3Get(endpoint).then((response) => {
+    key,
+    ([, endpoint, path]) => apiv3Get(endpoint, { path }).then((response) => {
       return {
         ancestorsChildren: response.data.ancestorsChildren,
       };
     }),
     {
-      fallbackData: prevSWRRes.data, // avoid data to be undefined due to the termNumber to change
+      keepPreviousData: true,
     },
   );
 };
@@ -173,15 +175,22 @@ export const useSWRxPageAncestorsChildren = (
 export const useSWRxPageChildren = (
     id?: string | null,
 ): SWRResponse<ChildrenResult, Error> => {
-  const { data: termNumber } = usePageTreeTermManager();
+  const key = id ? [MUTATION_ID_FOR_PAGETREE, '/page-listing/children', id] : null;
+
+  if (key != null) {
+    assert(keyMatcherForPageTree(key));
+  }
 
   return useSWR(
-    id ? [`/page-listing/children?id=${id}`, termNumber] : null,
-    ([endpoint]) => apiv3Get(endpoint).then((response) => {
+    key,
+    ([, endpoint, id]) => apiv3Get(endpoint, { id }).then((response) => {
       return {
         children: response.data.children,
       };
     }),
+    {
+      keepPreviousData: true,
+    },
   );
 };
 

+ 63 - 60
packages/app/src/stores/page.tsx

@@ -1,11 +1,12 @@
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
 
 import type {
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
 } from '@growi/core';
 import { isClient, pagePathUtils } from '@growi/core';
-import useSWR, { Key, SWRConfiguration, SWRResponse } from 'swr';
+import useSWR, { mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
+import useSWRMutation, { SWRMutationResponse } from 'swr/mutation';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
@@ -20,57 +21,31 @@ import { IPageTagsInfo } from '../interfaces/tag';
 import {
   useCurrentPageId, useCurrentPathname, useShareLinkId, useIsGuestUser,
 } from './context';
-import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 const { isPermalink: _isPermalink } = pagePathUtils;
 
-export const useSWRxPage = (
-    pageId?: string|null,
-    shareLinkId?: string,
-    revisionId?: string,
-    initialData?: IPagePopulatedToShowRevision|null,
-    config?: SWRConfiguration,
-): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
-
-  const swrResponse = useSWRImmutable(
-    pageId != null ? ['/page', pageId, shareLinkId, revisionId] : null,
-    // TODO: upgrade SWR to v2 and use useSWRMutation
-    //        in order to avoid complicated fetcher settings
-    Object.assign({
-      fetcher: ([endpoint, pageId, shareLinkId, revisionId]: [string, string, string|undefined, string|undefined]) => {
-        return apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
-          .then(result => result.data.page)
-          .catch((errs) => {
-            if (!Array.isArray(errs)) { throw Error('error is not array') }
-            const statusCode = errs[0].status;
-            if (statusCode === 403 || statusCode === 404) {
-              // for NotFoundPage
-              return null;
-            }
-            throw Error('failed to get page');
-          });
-      },
-    }, config ?? {}),
-  );
+
+export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
+  const key = 'currentPage';
 
   useEffect(() => {
     if (initialData !== undefined) {
-      swrResponse.mutate(initialData);
+      mutate(key, initialData, {
+        optimisticData: initialData,
+        populateCache: true,
+        revalidate: false,
+      });
     }
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [initialData]); // Only depends on `initialData`
+  }, [initialData, key]);
 
-  return swrResponse;
+  return useSWR(key, null, {
+    keepPreviousData: true,
+  });
 };
 
-export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
-  return useSWR(
-    path != null ? ['/page', path] : null,
-    ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
-  );
-};
+export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToShowRevision|null> => {
+  const key = 'currentPage';
 
-export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: shareLinkId } = useShareLinkId();
 
@@ -82,20 +57,34 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|nu
     revisionId = requestRevisionId != null ? requestRevisionId : undefined;
   }
 
-  const swrResult = useSWRxPage(
-    currentPageId, shareLinkId, revisionId,
-    initialData,
-    // overwrite fetcher if the current page is share link
-    shareLinkId == null
-      ? undefined
-      : {
-        fetcher: () => null,
-      },
+  return useSWRMutation(
+    key,
+    async() => {
+      return apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', { pageId: currentPageId, shareLinkId, revisionId })
+        .then(result => result.data.page)
+        .catch((errs) => {
+          if (!Array.isArray(errs)) { throw Error('error is not array') }
+          const statusCode = errs[0].status;
+          if (statusCode === 403 || statusCode === 404) {
+            // for NotFoundPage
+            return null;
+          }
+          throw Error('failed to get page');
+        });
+    },
+    {
+      populateCache: true,
+      revalidate: false,
+    },
   );
-
-  return swrResult;
 };
 
+export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
+  return useSWR(
+    path != null ? ['/page', path] : null,
+    ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
+  );
+};
 
 export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTagsInfo | undefined, Error> => {
   const { data: shareLinkId } = useShareLinkId();
@@ -108,27 +97,41 @@ export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTags
   );
 };
 
-export const usePageInfoTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'pageInfoTermNumber');
+export const mutateAllPageInfo = (): Promise<void[]> => {
+  return mutate(
+    key => Array.isArray(key) && key[0] === '/page/info',
+  );
 };
 
 export const useSWRxPageInfo = (
     pageId: string | null | undefined,
     shareLinkId?: string | null,
     initialData?: IPageInfoForEntity,
-): SWRResponse<IPageInfo | IPageInfoForOperation, Error> => {
-
-  const { data: termNumber } = usePageInfoTermManager();
+): SWRResponse<IPageInfo | IPageInfoForOperation> => {
 
   // assign null if shareLinkId is undefined in order to identify SWR key only by pageId
   const fixedShareLinkId = shareLinkId ?? null;
 
+  const key = useMemo(() => {
+    return pageId != null ? ['/page/info', pageId, fixedShareLinkId] : null;
+  }, [fixedShareLinkId, pageId]);
+
   const swrResult = useSWRImmutable(
-    pageId != null && termNumber != null ? ['/page/info', pageId, fixedShareLinkId, termNumber] : null,
-    ([endpoint, pageId, shareLinkId]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
+    key,
+    ([endpoint, pageId, shareLinkId]: [string, string, string|null]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
     { fallbackData: initialData },
   );
 
+  useEffect(() => {
+    if (initialData !== undefined) {
+      mutate(key, initialData, {
+        optimisticData: initialData,
+        populateCache: true,
+        revalidate: false,
+      });
+    }
+  }, [initialData, key]);
+
   return swrResult;
 };
 

+ 9 - 12
packages/app/src/stores/search.tsx

@@ -1,16 +1,9 @@
-import { SWRResponse } from 'swr';
+import { mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 
-import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
-
-
-export const useFullTextSearchTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'fullTextSearchTermNumber');
-};
-
 
 export type ISearchConfigurations = {
   limit: number,
@@ -49,11 +42,15 @@ const createSearchQuery = (keyword: string, includeTrashPages: boolean, includeU
   return query;
 };
 
+export const mutateSearching = async(): Promise<void[]> => {
+  return mutate(
+    key => Array.isArray(key) && key[0] === '/search',
+  );
+};
+
 export const useSWRxSearch = (
-    keyword: string | null, nqName: string | null, configurations: ISearchConfigurations, disableTermManager = false,
+    keyword: string | null, nqName: string | null, configurations: ISearchConfigurations,
 ): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
-  const { data: termNumber } = useFullTextSearchTermManager(disableTermManager);
-
   const {
     limit, offset, sort, order, includeTrashPages, includeUserPages,
   } = configurations;
@@ -71,7 +68,7 @@ export const useSWRxSearch = (
   const isKeywordValid = keyword != null && keyword.length > 0;
 
   const swrResult = useSWRImmutable(
-    isKeywordValid ? ['/search', keyword, fixedConfigurations, termNumber] : null,
+    isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
     ([endpoint, , fixedConfigurations]) => {
       const {
         limit, offset, sort, order,

+ 0 - 25
packages/app/src/stores/use-static-swr.tsx

@@ -33,28 +33,3 @@ export function useStaticSWR<Data, Error>(
 
   return swrResponse;
 }
-
-
-const ADVANCE_DELAY_MS = 800;
-
-export type ITermNumberManagerUtil = {
-  advance(): void,
-}
-
-export const useTermNumberManager = (key: Key) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  const swrResult = useStaticSWR<number, Error>(key, undefined, { fallbackData: 0 });
-
-  return {
-    ...swrResult,
-    advance: () => {
-      const { data: currentNum } = swrResult;
-      if (currentNum == null) {
-        return;
-      }
-
-      setTimeout(() => {
-        swrResult.mutate(currentNum + 1);
-      }, ADVANCE_DELAY_MS);
-    },
-  };
-};