fix(core): speed up navigation performance (#8794)

add some memo...
This commit is contained in:
EYHN
2024-11-12 13:22:59 +00:00
parent f4abe39689
commit 17c247af53
6 changed files with 97 additions and 74 deletions
@@ -9,7 +9,7 @@ import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { selectAtom } from 'jotai/utils'; import { selectAtom } from 'jotai/utils';
import type { MouseEventHandler } from 'react'; import type { MouseEventHandler } from 'react';
import { useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { CollectionListItem } from './collections/collection-list-item'; import { CollectionListItem } from './collections/collection-list-item';
import { PageListItem } from './docs/page-list-item'; import { PageListItem } from './docs/page-list-item';
@@ -240,7 +240,7 @@ export const PageListItemRenderer = (item: ListItem) => {
); );
}; };
export const CollectionListItemRenderer = (item: ListItem) => { export const CollectionListItemRenderer = memo((item: ListItem) => {
const props = useAtomValue(listsPropsAtom); const props = useAtomValue(listsPropsAtom);
const { selectionActive } = useAtomValue(selectionStateAtom); const { selectionActive } = useAtomValue(selectionStateAtom);
const collection = item as CollectionMeta; const collection = item as CollectionMeta;
@@ -252,7 +252,9 @@ export const CollectionListItemRenderer = (item: ListItem) => {
})} })}
/> />
); );
}; });
CollectionListItemRenderer.displayName = 'CollectionListItemRenderer';
export const TagListItemRenderer = (item: ListItem) => { export const TagListItemRenderer = (item: ListItem) => {
const props = useAtomValue(listsPropsAtom); const props = useAtomValue(listsPropsAtom);
@@ -1,9 +1,9 @@
import { RadioGroup, type RadioItem } from '@affine/component'; import { RadioGroup, type RadioItem } from '@affine/component';
import type { AllPageFilterOption } from '@affine/core/components/atoms'; import type { AllPageFilterOption } from '@affine/core/components/atoms';
import { allPageFilterSelectAtom } from '@affine/core/components/atoms'; import { allPageFilterSelectAtom } from '@affine/core/components/atoms';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useService, WorkspaceService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -14,26 +14,25 @@ export const WorkspaceModeFilterTab = ({
}: { }: {
activeFilter: AllPageFilterOption; activeFilter: AllPageFilterOption;
}) => { }) => {
const workspace = useService(WorkspaceService).workspace;
const t = useI18n(); const t = useI18n();
const [value, setValue] = useState(activeFilter); const [value, setValue] = useState(activeFilter);
const [filterMode, setFilterMode] = useAtom(allPageFilterSelectAtom); const [filterMode, setFilterMode] = useAtom(allPageFilterSelectAtom);
const { jumpToCollections, jumpToTags, jumpToPage } = useNavigateHelper(); const workbenchService = useService(WorkbenchService);
const handleValueChange = useCallback( const handleValueChange = useCallback(
(value: AllPageFilterOption) => { (value: AllPageFilterOption) => {
switch (value) { switch (value) {
case 'collections': case 'collections':
jumpToCollections(workspace.id); workbenchService.workbench.openCollections();
break; break;
case 'tags': case 'tags':
jumpToTags(workspace.id); workbenchService.workbench.openTags();
break; break;
case 'docs': case 'docs':
jumpToPage(workspace.id, 'all'); workbenchService.workbench.openAll();
break; break;
} }
}, },
[jumpToCollections, jumpToPage, jumpToTags, workspace] [workbenchService.workbench]
); );
useEffect(() => { useEffect(() => {
@@ -1,4 +1,3 @@
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { import {
AddPageButton, AddPageButton,
AppDownloadButton, AppDownloadButton,
@@ -23,7 +22,6 @@ import {
} from '@affine/core/modules/explorer'; } from '@affine/core/modules/explorer';
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags'; import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk'; import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
import type { Doc } from '@blocksuite/affine/store'; import type { Doc } from '@blocksuite/affine/store';
@@ -35,17 +33,11 @@ import {
SettingsIcon, SettingsIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import type { Workspace } from '@toeverything/infra'; import type { Workspace } from '@toeverything/infra';
import { import { useLiveData, useService, useServices } from '@toeverything/infra';
useLiveData, import type { ReactElement } from 'react';
useService, import { memo, useCallback } from 'react';
useServices,
WorkspaceService,
} from '@toeverything/infra';
import type { MouseEvent, ReactElement } from 'react';
import { useCallback } from 'react';
import { WorkbenchService } from '../../modules/workbench'; import { WorkbenchService } from '../../modules/workbench';
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
import { WorkspaceNavigator } from '../workspace-selector'; import { WorkspaceNavigator } from '../workspace-selector';
import { import {
quickSearch, quickSearch,
@@ -72,42 +64,43 @@ export type RootAppSidebarProps = {
}; };
}; };
const AllDocsButton = () => {
const t = useI18n();
const { workbenchService } = useServices({
WorkbenchService,
});
const workbench = workbenchService.workbench;
const allPageActive = useLiveData(
workbench.location$.selector(location => location.pathname === '/all')
);
return (
<MenuLinkItem icon={<AllDocsIcon />} active={allPageActive} to={'/all'}>
<span data-testid="all-pages">
{t['com.affine.workspaceSubPath.all']()}
</span>
</MenuLinkItem>
);
};
/** /**
* This is for the whole affine app sidebar. * This is for the whole affine app sidebar.
* This component wraps the app sidebar in `@affine/component` with logic and data. * This component wraps the app sidebar in `@affine/component` with logic and data.
* *
*/ */
export const RootAppSidebar = (): ReactElement => { export const RootAppSidebar = memo((): ReactElement => {
const { workbenchService, workspaceService, cMDKQuickSearchService } = const { workbenchService, cMDKQuickSearchService } = useServices({
useServices({ WorkbenchService,
WorkspaceService, CMDKQuickSearchService,
WorkbenchService, });
CMDKQuickSearchService,
});
const currentWorkspace = workspaceService.workspace;
const t = useI18n(); const t = useI18n();
const globalDialogService = useService(GlobalDialogService); const globalDialogService = useService(GlobalDialogService);
const workspaceDialogService = useService(WorkspaceDialogService); const workspaceDialogService = useService(WorkspaceDialogService);
const workbench = workbenchService.workbench; const workbench = workbenchService.workbench;
const currentPath = useLiveData(
workbench.location$.map(location => location.pathname)
);
const onOpenQuickSearchModal = useCallback(() => { const onOpenQuickSearchModal = useCallback(() => {
cMDKQuickSearchService.toggle(); cMDKQuickSearchService.toggle();
}, [cMDKQuickSearchService]); }, [cMDKQuickSearchService]);
const allPageActive = currentPath === '/all';
const pageHelper = usePageHelper(currentWorkspace.docCollection);
const onClickNewPage = useAsyncCallback(
async (e?: MouseEvent) => {
pageHelper.createPage(undefined, isNewTabTrigger(e) ? 'new-tab' : true);
track.$.navigationPanel.$.createDoc();
},
[pageHelper]
);
const onOpenSettingModal = useCallback(() => { const onOpenSettingModal = useCallback(() => {
globalDialogService.open('setting', { globalDialogService.open('setting', {
activeTab: 'appearance', activeTab: 'appearance',
@@ -169,13 +162,9 @@ export const RootAppSidebar = (): ReactElement => {
data-event-props="$.navigationPanel.$.quickSearch" data-event-props="$.navigationPanel.$.quickSearch"
onClick={onOpenQuickSearchModal} onClick={onOpenQuickSearchModal}
/> />
<AddPageButton onClick={onClickNewPage} /> <AddPageButton />
</div> </div>
<MenuLinkItem icon={<AllDocsIcon />} active={allPageActive} to={'/all'}> <AllDocsButton />
<span data-testid="all-pages">
{t['com.affine.workspaceSubPath.all']()}
</span>
</MenuLinkItem>
<AppSidebarJournalButton /> <AppSidebarJournalButton />
<MenuItem <MenuItem
data-testid="slider-bar-workspace-setting-button" data-testid="slider-bar-workspace-setting-button"
@@ -220,6 +209,6 @@ export const RootAppSidebar = (): ReactElement => {
</SidebarContainer> </SidebarContainer>
</AppSidebar> </AppSidebar>
); );
}; });
RootAppSidebar.displayName = 'memo(RootAppSidebar)'; RootAppSidebar.displayName = 'memo(RootAppSidebar)';
@@ -1,24 +1,35 @@
import { IconButton } from '@affine/component'; import { IconButton } from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import { useService, WorkspaceService } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import type React from 'react'; import type React from 'react';
import type { MouseEventHandler } from 'react'; import { type MouseEvent, useCallback } from 'react';
import * as styles from './index.css'; import * as styles from './index.css';
interface AddPageButtonProps { interface AddPageButtonProps {
onClick?: MouseEventHandler;
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
} }
const sideBottom = { side: 'bottom' as const }; const sideBottom = { side: 'bottom' as const };
export function AddPageButton({ export function AddPageButton({ className, style }: AddPageButtonProps) {
onClick, const workspaceService = useService(WorkspaceService);
className, const currentWorkspace = workspaceService.workspace;
style, const pageHelper = usePageHelper(currentWorkspace.docCollection);
}: AddPageButtonProps) {
const onClickNewPage = useCallback(
(e?: MouseEvent) => {
pageHelper.createPage(undefined, isNewTabTrigger(e) ? 'new-tab' : true);
track.$.navigationPanel.$.createDoc();
},
[pageHelper]
);
const t = useI18n(); const t = useI18n();
return ( return (
@@ -28,8 +39,8 @@ export function AddPageButton({
data-testid="sidebar-new-page-button" data-testid="sidebar-new-page-button"
style={style} style={style}
className={clsx([styles.root, className])} className={clsx([styles.root, className])}
onClick={onClick} onClick={onClickNewPage}
onAuxClick={onClick} onAuxClick={onClickNewPage}
> >
<PlusIcon /> <PlusIcon />
</IconButton> </IconButton>
@@ -89,6 +89,34 @@ interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {
dropEffect?: ExplorerTreeNodeDropEffect; dropEffect?: ExplorerTreeNodeDropEffect;
} }
/**
* specific rename modal for explorer tree node,
* Separate it into a separate component to prevent re-rendering the entire component when width changes.
*/
const ExplorerTreeNodeRenameModal = ({
setRenaming,
handleRename,
rawName,
}: {
setRenaming: (renaming: boolean) => void;
handleRename: (newName: string) => void;
rawName: string | undefined;
}) => {
const appSidebarService = useService(AppSidebarService).sidebar;
const sidebarWidth = useLiveData(appSidebarService.width$);
return (
<RenameModal
open
width={sidebarWidth - 32}
onOpenChange={setRenaming}
onRename={handleRename}
currentName={rawName ?? ''}
>
<div className={styles.itemRenameAnchor} />
</RenameModal>
);
};
export const ExplorerTreeNode = ({ export const ExplorerTreeNode = ({
children, children,
icon: Icon, icon: Icon,
@@ -126,9 +154,6 @@ export const ExplorerTreeNode = ({
const [lastInGroup, setLastInGroup] = useState(false); const [lastInGroup, setLastInGroup] = useState(false);
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const appSidebarService = useService(AppSidebarService).sidebar;
const sidebarWidth = useLiveData(appSidebarService.width$);
const { emoji, name } = useMemo(() => { const { emoji, name } = useMemo(() => {
if (!extractEmojiAsIcon || !rawName) { if (!extractEmojiAsIcon || !rawName) {
return { return {
@@ -379,16 +404,12 @@ export const ExplorerTreeNode = ({
</div> </div>
</div> </div>
{renameable && ( {renameable && renaming && (
<RenameModal <ExplorerTreeNodeRenameModal
open={!!renaming} setRenaming={setRenaming}
width={sidebarWidth - 32} handleRename={handleRename}
onOpenChange={setRenaming} rawName={rawName}
onRename={handleRename} />
currentName={rawName ?? ''}
>
<div className={styles.itemRenameAnchor} />
</RenameModal>
)} )}
</div> </div>
); );
@@ -1,6 +1,7 @@
export { View as WorkbenchView } from './entities/view'; export { View as WorkbenchView } from './entities/view';
export { Workbench } from './entities/workbench'; export { Workbench } from './entities/workbench';
export { ViewScope } from './scopes/view'; export { ViewScope } from './scopes/view';
export { ViewService } from './services/view';
export { WorkbenchService } from './services/workbench'; export { WorkbenchService } from './services/workbench';
export { useBindWorkbenchToBrowserRouter } from './view/browser-adapter'; export { useBindWorkbenchToBrowserRouter } from './view/browser-adapter';
export { useIsActiveView } from './view/use-is-active-view'; export { useIsActiveView } from './view/use-is-active-view';