mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
perf: Prefetch references on hover (#9722)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { LocationDescriptor } from "history";
|
||||
import { observer } from "mobx-react";
|
||||
import { EditIcon, TrashIcon } from "outline-icons";
|
||||
import { useRef } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
@@ -20,6 +20,7 @@ import { documentHistoryPath } from "~/utils/routeHelpers";
|
||||
import { EventItem, lineStyle } from "./EventListItem";
|
||||
import Facepile from "./Facepile";
|
||||
import Text from "./Text";
|
||||
import useClickIntent from "~/hooks/useClickIntent";
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
@@ -43,7 +44,7 @@ const RevisionListItem = ({ item, document, ...rest }: Props) => {
|
||||
ref.current?.focus();
|
||||
};
|
||||
|
||||
const prefetchRevision = async () => {
|
||||
const prefetchRevision = useCallback(async () => {
|
||||
if (!document.isDeleted && !item.deletedAt && !revisionLoadedRef.current) {
|
||||
if (isLatestRevision) {
|
||||
return;
|
||||
@@ -51,7 +52,10 @@ const RevisionListItem = ({ item, document, ...rest }: Props) => {
|
||||
await revisions.fetch(item.id, { force: true });
|
||||
revisionLoadedRef.current = true;
|
||||
}
|
||||
};
|
||||
}, [document.isDeleted, item.deletedAt, isLatestRevision, revisions]);
|
||||
|
||||
const { handleMouseEnter, handleMouseLeave } =
|
||||
useClickIntent(prefetchRevision);
|
||||
|
||||
let meta, icon, to: LocationDescriptor | undefined;
|
||||
|
||||
@@ -134,7 +138,8 @@ const RevisionListItem = ({ item, document, ...rest }: Props) => {
|
||||
</StyledEventBoundary>
|
||||
) : undefined
|
||||
}
|
||||
onMouseEnter={prefetchRevision}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { s } from "@shared/styles";
|
||||
import { isMobile } from "@shared/utils/browser";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { UnreadBadge } from "~/components/UnreadBadge";
|
||||
import useUnmount from "~/hooks/useUnmount";
|
||||
import useClickIntent from "~/hooks/useClickIntent";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
import Disclosure from "./Disclosure";
|
||||
import NavLink, { Props as NavLinkProps } from "./NavLink";
|
||||
@@ -62,8 +62,8 @@ function SidebarLink(
|
||||
}: Props,
|
||||
ref: React.RefObject<HTMLAnchorElement>
|
||||
) {
|
||||
const timer = React.useRef<number>();
|
||||
const theme = useTheme();
|
||||
const { handleMouseEnter, handleMouseLeave } = useClickIntent(onClickIntent);
|
||||
const style = React.useMemo(
|
||||
() => ({
|
||||
paddingLeft: `${(depth || 0) * 16 + 12}px`,
|
||||
@@ -80,28 +80,6 @@ function SidebarLink(
|
||||
[theme.text, theme.sidebarActiveBackground, style]
|
||||
);
|
||||
|
||||
const handleMouseEnter = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
|
||||
if (onClickIntent) {
|
||||
timer.current = window.setTimeout(onClickIntent, 100);
|
||||
}
|
||||
}, [onClickIntent]);
|
||||
|
||||
const handleMouseLeave = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useUnmount(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from "react";
|
||||
import useUnmount from "./useUnmount";
|
||||
|
||||
/**
|
||||
* Hook to handle click intent logic with mouse enter/leave events.
|
||||
* Sets a timer on mouse enter to call the intent callback after a delay,
|
||||
* and clears the timer on mouse leave or component unmount.
|
||||
*/
|
||||
export default function useClickIntent(
|
||||
onClickIntent?: () => void,
|
||||
delay = 100
|
||||
) {
|
||||
const timer = React.useRef<number>();
|
||||
|
||||
const handleMouseEnter = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
|
||||
if (onClickIntent) {
|
||||
timer.current = window.setTimeout(onClickIntent, delay);
|
||||
}
|
||||
}, [onClickIntent, delay]);
|
||||
|
||||
const handleMouseLeave = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useUnmount(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
});
|
||||
|
||||
return { handleMouseEnter, handleMouseLeave };
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import Document from "~/models/Document";
|
||||
import Flex from "~/components/Flex";
|
||||
import { SidebarContextType } from "~/components/Sidebar/components/SidebarContext";
|
||||
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||
import useClickIntent from "~/hooks/useClickIntent";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type Props = {
|
||||
shareId?: string;
|
||||
@@ -60,6 +63,12 @@ function ReferenceListItem({
|
||||
sidebarContext,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { documents } = useStores();
|
||||
const prefetchDocument = useCallback(async () => {
|
||||
await documents.prefetchDocument(document.id);
|
||||
}, [documents, document.id]);
|
||||
const { handleMouseEnter, handleMouseLeave } =
|
||||
useClickIntent(prefetchDocument);
|
||||
const { icon, color } = document;
|
||||
const isEmoji = determineIconType(icon) === IconType.Emoji;
|
||||
const title =
|
||||
@@ -67,6 +76,8 @@ function ReferenceListItem({
|
||||
|
||||
return (
|
||||
<DocumentLink
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
to={{
|
||||
pathname: shareId
|
||||
? sharedDocumentPath(shareId, document.url)
|
||||
|
||||
Reference in New Issue
Block a user