From 3992aa5dc04601af46f8f426a4792105cb3ec113 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 10 Nov 2023 10:51:21 +0500 Subject: [PATCH] web: load tag & color title lazily --- .../src/components/route-container/index.tsx | 13 ++- apps/web/src/components/tag/index.tsx | 2 +- apps/web/src/hooks/use-promise.ts | 84 +++++++++++++++++++ apps/web/src/navigation/routes.tsx | 31 ++++--- 4 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/hooks/use-promise.ts diff --git a/apps/web/src/components/route-container/index.tsx b/apps/web/src/components/route-container/index.tsx index fd8a65439..474a8e89a 100644 --- a/apps/web/src/components/route-container/index.tsx +++ b/apps/web/src/components/route-container/index.tsx @@ -23,6 +23,7 @@ import { ArrowLeft, Menu, Search, Plus } from "../icons"; import { useStore } from "../../stores/app-store"; import useMobile from "../../hooks/use-mobile"; import { navigate } from "../../navigation"; +import usePromise from "../../hooks/use-promise"; export type RouteContainerButtons = { search?: { @@ -40,7 +41,7 @@ export type RouteContainerButtons = { export type RouteContainerProps = { type: string; - title?: string; + title?: string | (() => Promise); buttons?: RouteContainerButtons; }; function RouteContainer(props: PropsWithChildren) { @@ -56,7 +57,11 @@ function RouteContainer(props: PropsWithChildren) { export default RouteContainer; function Header(props: RouteContainerProps) { - const { title, buttons, type } = props; + const { buttons, type } = props; + const titlePromise = usePromise( + () => (typeof props.title === "string" ? props.title : props.title?.()), + [props.title] + ); const toggleSideMenu = useStore((store) => store.toggleSideMenu); const isMobile = useMobile(); @@ -84,9 +89,9 @@ function Header(props: RouteContainerProps) { size={30} /> )} - {title && ( + {titlePromise.status === "fulfilled" && titlePromise.value && ( - {title} + {titlePromise.value} )} diff --git a/apps/web/src/components/tag/index.tsx b/apps/web/src/components/tag/index.tsx index 29067ea14..d04f33f76 100644 --- a/apps/web/src/components/tag/index.tsx +++ b/apps/web/src/components/tag/index.tsx @@ -41,7 +41,7 @@ function Tag(props: TagProps) { item={item} isCompact title={ - + {"#"} diff --git a/apps/web/src/hooks/use-promise.ts b/apps/web/src/hooks/use-promise.ts new file mode 100644 index 000000000..04898e804 --- /dev/null +++ b/apps/web/src/hooks/use-promise.ts @@ -0,0 +1,84 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { DependencyList, useEffect, useState } from "react"; + +export type PromiseResult = PromisePendingResult | PromiseSettledResult; + +export interface PromisePendingResult { + status: "pending"; +} + +/** + * Function that creates a promise, takes a signal to abort fetch requests. + */ +export type PromiseFactoryFn = (signal: AbortSignal) => T | Promise; + +/** + * Takes a function that creates a Promise and returns its pending, fulfilled, or rejected result. + * + * ```ts + * const result = usePromise(() => fetch('/api/products')) + * ``` + * + * Also takes a list of dependencies, when the dependencies change the promise is recreated. + * + * ```ts + * const result = usePromise(() => fetch(`/api/products/${id}`), [id]) + * ``` + * + * Can abort a fetch request, a [signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) is provided from the factory function to do so. + * + * ```ts + * const result = usePromise(signal => fetch(`/api/products/${id}`, { signal }), [id]) + * ``` + * + * @param factory Function that creates the promise. + * @param deps If present, promise will be recreated if the values in the list change. + */ +export default function usePromise( + factory: PromiseFactoryFn, + deps: DependencyList = [] +): PromiseResult { + const [result, setResult] = useState>({ status: "pending" }); + + useEffect(() => { + if (result.status !== "pending") { + setResult({ status: "pending" }); + } + + const controller = new AbortController(); + const { signal } = controller; + + async function handlePromise() { + const [promiseResult] = await Promise.allSettled([factory(signal)]); + + if (!signal.aborted) { + setResult(promiseResult); + } + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handlePromise(); + + return () => controller.abort(); + }, deps); + + return result; +} diff --git a/apps/web/src/navigation/routes.tsx b/apps/web/src/navigation/routes.tsx index a6123a729..1bf5efabb 100644 --- a/apps/web/src/navigation/routes.tsx +++ b/apps/web/src/navigation/routes.tsx @@ -36,7 +36,7 @@ import { CREATE_BUTTON_MAP } from "../common"; type RouteResult = { key: string; type: "notes" | "notebooks" | "reminders" | "trash" | "tags" | "search"; - title?: string; + title?: string | (() => Promise); component: React.ReactNode; props?: any; buttons?: RouteContainerButtons; @@ -171,14 +171,15 @@ const routes = defineRoutes({ } }), "/tags/:tagId": ({ tagId }) => { - const tag = db.tags.tag(tagId); - if (!tag) return false; - const { id, title } = tag; - notestore.setContext({ type: "tag", value: id }); + notestore.setContext({ type: "tag", id: tagId }); return defineRoute({ key: "notes", type: "notes", - title: `#${title}`, + title: async () => { + const tag = await db.tags.tag(tagId); + if (!tag) return; + return `#${tag.title}`; + }, component: Notes, buttons: { create: CREATE_BUTTON_MAP.notes, @@ -187,28 +188,26 @@ const routes = defineRoutes({ onClick: () => navigate("/tags") }, search: { - title: `Search #${title} notes` + title: `Search notes` } } }); }, "/colors/:colorId": ({ colorId }) => { - const color = db.colors.color(colorId); - if (!color) { - navigate("/"); - return false; - } - const { id, title } = color; - notestore.setContext({ type: "color", value: id }); + notestore.setContext({ type: "color", id: colorId }); return defineRoute({ key: "notes", type: "notes", - title: title, + title: async () => { + const color = await db.colors.color(colorId); + if (!color) return; + return `${color.title}`; + }, component: Notes, buttons: { create: CREATE_BUTTON_MAP.notes, search: { - title: `Search ${title} colored notes` + title: `Search notes` } } });