The weeb for the next gen discord boat - Wamellow wamellow.com
bot discord
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf: react hooks dependencies and improve rendering performance (#31)

authored by

Luna Seemann and committed by
GitHub
23567237 bbeefce6

+157 -102
+9 -4
app/(home)/page.tsx
··· 66 66 }); 67 67 68 68 export default async function Home() { 69 - const topGuilds = await fetch(`${process.env.NEXT_PUBLIC_API}/top-guilds`, defaultFetchOptions) 69 + const topGuildsPromise = fetch(`${process.env.NEXT_PUBLIC_API}/top-guilds`, defaultFetchOptions) 70 70 .then((res) => res.json()) 71 - .catch(() => null) as ApiV1TopguildsGetResponse[] | null; 71 + .catch(() => null) as Promise<ApiV1TopguildsGetResponse[] | null>; 72 + 73 + const heads = await headers(); 74 + const isEmbedded = heads.get("sec-fetch-dest") === "iframe"; 72 75 73 - const isEmbedded = (await headers()).get("sec-fetch-dest") === "iframe"; 76 + const topGuilds = await topGuildsPromise; 74 77 75 78 return ( 76 79 <div className="flex items-center flex-col w-full"> ··· 710 713 711 714 </article> 712 715 713 - <Faq /> 716 + <Suspense fallback={<Skeleton className="w-full h-96" isLoading={true} />}> 717 + <Faq /> 718 + </Suspense> 714 719 715 720 <Comment 716 721 username="Luna’s Grandpa <3"
+1 -1
app/(home)/status/side.component.tsx
··· 24 24 () => /^\d{15,20}$/.test(guildId) 25 25 ? getClusterId(guildId || "", status.clusters.length) 26 26 : null, 27 - [guildId] 27 + [guildId, status.clusters.length] 28 28 ); 29 29 30 30 useEffect(() => {
+2 -1
app/(home)/text-to-speech/use-history.ts
··· 81 81 82 82 loadHistory(); 83 83 84 + const cache = urlCache.current; 84 85 return () => { 85 86 mounted = false; 86 87 dbRef.current?.close(); 87 - for (const [, url] of urlCache.current) URL.revokeObjectURL(url); 88 + for (const [, url] of cache) URL.revokeObjectURL(url); 88 89 }; 89 90 }, [isIndexDbSupported]); 90 91
+3 -3
app/dashboard/[guildId]/custom-commands/page.tsx
··· 48 48 return params.toString(); 49 49 }, [search]); 50 50 51 - const setTagId = (id: string) => { 51 + const setTagId = useCallback((id: string) => { 52 52 router.push(pathname + "?" + createQueryString("id", id)); 53 - }; 53 + }, [router, pathname, createQueryString]); 54 54 55 55 useEffect(() => { 56 56 if (!Array.isArray(data)) return; 57 57 if (data && !tag && data[0]) setTagId(data[0].id); 58 - }, [data]); 58 + }, [data, tag, setTagId]); 59 59 60 60 if (error || (data && "message" in data)) { 61 61 return (
+1 -1
app/dashboard/[guildId]/dailyposts/create.component.tsx
··· 37 37 const [channelId, setChannelId] = useState<string | null>(null); 38 38 39 39 const date = useMemo(() => new Date(), []); 40 - const hoursArray = useMemo(() => generateHourArray(date), []); 40 + const hoursArray = useMemo(() => generateHourArray(date), [date]); 41 41 42 42 return (<> 43 43 {style === Style.Compact
+1 -1
app/dashboard/[guildId]/dailyposts/page.tsx
··· 24 24 const params = useParams(); 25 25 26 26 const date = useMemo(() => new Date(), []); 27 - const hoursArray = useMemo(() => generateHourArray(date), []); 27 + const hoursArray = useMemo(() => generateHourArray(date), [date]); 28 28 29 29 const url = `/guilds/${params.guildId}/modules/dailyposts` as const; 30 30 const {
+19 -16
app/dashboard/[guildId]/greeting/passport/complete-setup.tsx
··· 4 4 import type { ApiEdit } from "@/lib/api/hook"; 5 5 import { type ApiV1GuildsModulesPassportGetResponse, GuildFlags } from "@/typings"; 6 6 import { createSelectableItems } from "@/utils/create-selectable-items"; 7 - import { useEffect, useState } from "react"; 7 + import { useState } from "react"; 8 8 9 9 enum ModalType { 10 10 None = 0, ··· 23 23 data, 24 24 edit 25 25 }: Props) { 26 - const [modal, setModal] = useState<ModalType>(ModalType.None); 26 + const [dismissed, setDismissed] = useState<ModalType>(ModalType.None); 27 27 const [roleId, setRoleId] = useState<string | null>(null); 28 28 29 - const enabled = (guild!.flags & GuildFlags.PassportEnabled) !== 0; 30 - 31 - useEffect(() => { 32 - if (!enabled) return; 29 + const enabled = guild ? (guild.flags & GuildFlags.PassportEnabled) !== 0 : false; 33 30 31 + let activeModal = ModalType.None; 32 + if (enabled) { 34 33 if (!data.successRoleId) { 35 - setModal(ModalType.VerifiedRole); 36 - return; 34 + activeModal = ModalType.VerifiedRole; 35 + } else if (data.punishment === 2 && !data.punishmentRoleId) { 36 + activeModal = ModalType.PunishmentRole; 37 37 } 38 + } 38 39 39 - if (data.punishment === 2 && !data.punishmentRoleId) { 40 - setModal(ModalType.PunishmentRole); 41 - } 42 - }, [data]); 40 + if (activeModal === ModalType.None && dismissed !== ModalType.None) { 41 + setDismissed(ModalType.None); 42 + } 43 + 44 + const showVerified = activeModal === ModalType.VerifiedRole && dismissed !== ModalType.VerifiedRole; 45 + const showPunishment = activeModal === ModalType.PunishmentRole && dismissed !== ModalType.PunishmentRole; 43 46 44 47 return (<> 45 48 <Modal 46 49 title="Verified role" 47 50 className="overflow-visible!" 48 - isOpen={Boolean(guild) && modal === ModalType.VerifiedRole} 49 - onClose={() => setModal(ModalType.None)} 51 + isOpen={Boolean(guild) && showVerified} 52 + onClose={() => setDismissed(ModalType.VerifiedRole)} 50 53 onSubmit={() => { 51 54 return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guild?.id}/modules/passport`, { 52 55 method: "PATCH", ··· 74 77 <Modal 75 78 title="Punishment role" 76 79 className="overflow-visible!" 77 - isOpen={Boolean(guild) && modal === ModalType.PunishmentRole} 78 - onClose={() => setModal(ModalType.None)} 80 + isOpen={Boolean(guild) && showPunishment} 81 + onClose={() => setDismissed(ModalType.PunishmentRole)} 79 82 onSubmit={() => { 80 83 return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guild?.id}/modules/passport`, { 81 84 method: "PATCH",
+24 -17
app/dashboard/[guildId]/layout.tsx
··· 19 19 20 20 function useGuildData<T extends unknown[]>( 21 21 url: string, 22 + guildId: string | string[] | undefined, 22 23 onLoad: (data: T, error: boolean) => void 23 24 ) { 24 25 return useQuery( 25 26 url, 26 27 () => getData<T>(url), 27 28 { 28 - enabled: Boolean(guildStore((g) => g)?.id), 29 + enabled: Boolean(guildId), 29 30 onSettled: (data) => { 30 31 const isError = !data || "message" in data; 31 32 onLoad(isError ? [] as unknown as T : data, isError); ··· 45 46 46 47 const [loaded, setLoaded] = useState<string[]>([]); 47 48 48 - const guild = guildStore((g) => g); 49 + const guildId = guildStore((g) => g?.id); 50 + const guildName = guildStore((g) => g?.name); 51 + const guildIcon = guildStore((g) => g?.icon); 52 + const guildMemberCount = guildStore((g) => g?.memberCount); 49 53 50 54 const session = useMemo(() => cookies.get("session"), [cookies]); 51 55 ··· 65 69 66 70 useGuildData<ApiV1GuildsChannelsGetResponse[]>( 67 71 `${url}/channels`, 72 + params.guildId, 68 73 (data) => { 69 - guildStore.setState({ ...guild, channels: data }); 70 - setLoaded((loaded) => [...loaded, "channels"]); 74 + guildStore.setState((state) => ({ ...state, channels: data })); 75 + setLoaded((loaded) => loaded.includes("channels") ? loaded : [...loaded, "channels"]); 71 76 } 72 77 ); 73 78 74 79 useGuildData<ApiV1GuildsRolesGetResponse[]>( 75 80 `${url}/roles`, 81 + params.guildId, 76 82 (data) => { 77 - guildStore.setState({ ...guild, roles: data }); 78 - setLoaded((loaded) => [...loaded, "roles"]); 83 + guildStore.setState((state) => ({ ...state, roles: data })); 84 + setLoaded((loaded) => loaded.includes("roles") ? loaded : [...loaded, "roles"]); 79 85 } 80 86 ); 81 87 82 88 useGuildData<ApiV1GuildsEmojisGetResponse[]>( 83 89 `${url}/emojis`, 90 + params.guildId, 84 91 (data) => { 85 - guildStore.setState({ ...guild, emojis: data }); 86 - setLoaded((loaded) => [...loaded, "emojis"]); 92 + guildStore.setState((state) => ({ ...state, emojis: data })); 93 + setLoaded((loaded) => loaded.includes("emojis") ? loaded : [...loaded, "emojis"]); 87 94 } 88 95 ); 89 96 ··· 91 98 92 99 useEffect(() => { 93 100 if (!data || "message" in data) return; 94 - guildStore.setState(data); 101 + guildStore.setState((state) => ({ ...state, ...data })); 95 102 }, [data]); 96 103 97 104 return ( 98 105 <div className="flex flex-col w-full"> 99 - {guild?.name && ( 106 + {guildName && ( 100 107 <Head> 101 - <title>{guild.name}{"'"}s Dashboard</title> 108 + <title>{guildName}{"'"}s Dashboard</title> 102 109 </Head> 103 110 )} 104 111 ··· 116 123 117 124 <div className="text-lg flex gap-5"> 118 125 <Skeleton 119 - isLoading={!guild?.id} 126 + isLoading={!guildId} 120 127 className="rounded-xl size-14 ring-offset-(--background-rgb) ring-2 ring-offset-2 ring-violet-400/40 shrink-0 relative top-1 left-1" 121 128 > 122 129 <ImageReduceMotion 123 130 alt="this server's icon" 124 131 className="rounded-xl" 125 - url={`https://cdn.discordapp.com/icons/${guild?.id}/${guild?.icon}`} 132 + url={`https://cdn.discordapp.com/icons/${guildId}/${guildIcon}`} 126 133 size={128} 127 134 /> 128 135 </Skeleton> ··· 134 141 </div> 135 142 : 136 143 <div className="flex flex-col mt-[6px]"> 137 - <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium">{guild?.name || "Unknown Server"}</div> 138 - <div className="text-xs font-semibold flex items-center gap-1"> <HiUsers /> {intl.format(guild?.memberCount || 0)}</div> 144 + <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium">{guildName || "Unknown Server"}</div> 145 + <div className="text-xs font-semibold flex items-center gap-1"> <HiUsers /> {intl.format(guildMemberCount || 0)}</div> 139 146 </div> 140 147 } 141 148 </div> ··· 186 193 } 187 194 ]} 188 195 url={`/dashboard/${params.guildId}`} 189 - disabled={!guild || Boolean(error)} 196 + disabled={!guildId || Boolean(error)} 190 197 /> 191 198 </Suspense> 192 199 ··· 211 218 </>} 212 219 /> 213 220 : 214 - (guild && loaded.length === 3) ? children : <></> 221 + (guildId && loaded.length === 3) ? children : <></> 215 222 } 216 223 217 224 </div>
+9 -8
app/dashboard/[guildId]/notifications/style.component.tsx
··· 11 11 import { State } from "@/utils/captcha"; 12 12 import { cn } from "@/utils/cn"; 13 13 import Link from "next/link"; 14 - import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 14 + import { useCallback, useMemo, useRef, useState } from "react"; 15 15 import { HiOutlineUpload, HiPencil, HiSparkles, HiX } from "react-icons/hi"; 16 16 17 17 const ALLOWED_FILE_TYPES = ["image/png", "image/jpeg", "image/webp"]; ··· 157 157 const [avatar, setAvatar] = useState<ArrayBuffer | string | null>(avatarUrl); 158 158 const [error, setError] = useState<string | null>(null); 159 159 160 - useEffect( 161 - () => { 162 - if (!isOpen) return; 163 - setAvatar(avatarUrl); 164 - }, 165 - [isOpen, avatarUrl] 166 - ); 160 + const [prevIsOpen, setPrevIsOpen] = useState(isOpen); 161 + if (isOpen && !prevIsOpen) { 162 + setPrevIsOpen(true); 163 + setName(username); 164 + setAvatar(avatarUrl); 165 + } else if (!isOpen && prevIsOpen) { 166 + setPrevIsOpen(false); 167 + } 167 168 168 169 const renderable = useMemo( 169 170 () => !avatar || typeof avatar === "string"
+12 -11
app/dashboard/[guildId]/style.component.tsx
··· 10 10 import { State } from "@/utils/captcha"; 11 11 import { cn } from "@/utils/cn"; 12 12 import Image from "next/image"; 13 - import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 13 + import { useCallback, useMemo, useRef, useState } from "react"; 14 14 import { HiOutlineUpload, HiPencil, HiX } from "react-icons/hi"; 15 15 16 16 const ALLOWED_FILE_TYPES = ["image/jpg", "image/jpeg", "image/png", "image/webp", "image/gif", "image/apng"]; ··· 153 153 const [banner, setBanner] = useState<ArrayBuffer | string | null>(bannerUrl); 154 154 const [error, setError] = useState<string | null>(null); 155 155 156 - useEffect( 157 - () => { 158 - if (!isOpen) return; 159 - setAvatar(avatarUrl); 160 - setBanner(bannerUrl); 161 - }, 162 - [isOpen, avatarUrl, bannerUrl] 163 - ); 156 + const [prevIsOpen, setPrevIsOpen] = useState(isOpen); 157 + if (isOpen && !prevIsOpen) { 158 + setPrevIsOpen(true); 159 + setName(username); 160 + setAvatar(avatarUrl); 161 + setBanner(bannerUrl); 162 + } else if (!isOpen && prevIsOpen) { 163 + setPrevIsOpen(false); 164 + } 164 165 165 166 const render = useCallback( 166 167 (buf: ArrayBuffer | string | null) => !buf || typeof buf === "string" ··· 169 170 [] 170 171 ); 171 172 172 - const renderableAvatar = useMemo(() => render(avatar), [avatar]); 173 - const renderableBanner = useMemo(() => render(banner), [banner]); 173 + const renderableAvatar = useMemo(() => render(avatar), [avatar, render]); 174 + const renderableBanner = useMemo(() => render(banner), [banner, render]); 174 175 175 176 return (<> 176 177 <Modal<ApiV1GuildsStylePatchResponse>
+1 -1
app/dashboard/[guildId]/tts.component.tsx
··· 22 22 return g; 23 23 }); 24 24 }, 25 - [guild] 25 + [] 26 26 ); 27 27 28 28 return (
+1 -1
app/provider.tsx
··· 32 32 ); 33 33 34 34 if (!path.startsWith("/dashboard/")) guildStore.setState(undefined); 35 - }, [path]); 35 + }, [path, cookies]); 36 36 37 37 return ( 38 38 <TooltipProvider>
+1 -1
components/click-outside.tsx
··· 24 24 return () => { 25 25 document.removeEventListener("click", handleDocumentClick); 26 26 }; 27 - }, []); 27 + }, [onClose]); 28 28 29 29 return <></>; 30 30 }
+7 -6
components/counter.tsx
··· 1 1 "use client"; 2 2 3 3 import Link from "next/link"; 4 - import { useEffect, useRef, useState } from "react"; 4 + import { useCallback, useEffect, useRef, useState } from "react"; 5 5 import CountUp, { type CountUpProps } from "react-countup"; 6 6 import { HiInformationCircle } from "react-icons/hi"; 7 7 ··· 11 11 const [isInViewport, setIsInViewport] = useState(false); 12 12 const componentRef = useRef<HTMLDivElement | null>(null); 13 13 14 - const handleIntersection = (entries: IntersectionObserverEntry[]) => { 14 + const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => { 15 15 const [entry] = entries; 16 16 setIsInViewport(entry.isIntersecting); 17 - }; 17 + }, []); 18 18 19 19 useEffect(() => { 20 + const node = componentRef.current; 20 21 const observer = new IntersectionObserver(handleIntersection, { 21 22 threshold: 0.5 22 23 }); 23 24 24 - if (componentRef.current) observer.observe(componentRef.current); 25 + if (node) observer.observe(node); 25 26 26 27 return () => { 27 - if (componentRef.current) observer.unobserve(componentRef.current); 28 + if (node) observer.unobserve(node); 28 29 }; 29 - }, []); 30 + }, [handleIntersection]); 30 31 31 32 return ( 32 33 <span ref={componentRef} className={props.className || ""}>
+1 -1
components/dashboard/lists/hook.ts
··· 39 39 40 40 const setItemId = useCallback( 41 41 (id: string) => router.push(`${pathname}?${createQueryString("id", id)}`), 42 - [router] 42 + [router, pathname, createQueryString] 43 43 ); 44 44 45 45 const editItem = useCallback(
+11 -6
components/inputs/image-url-input.tsx
··· 65 65 const [debouncedUrl, setDebouncedUrl] = useState(value); 66 66 const debounceRef = useRef<NodeJS.Timeout | null>(null); 67 67 68 + const [prevValue, setPrevValue] = useState(value); 69 + if (value !== prevValue) { 70 + setPrevValue(value); 71 + if (value?.length) { 72 + setImageState(ImageState.Loading); 73 + } else { 74 + setImageState(ImageState.Idle); 75 + setDebouncedUrl(value); 76 + } 77 + } 78 + 68 79 // Debounce URL changes before attempting to load image 69 80 useEffect(() => { 70 81 if (debounceRef.current) { ··· 73 84 74 85 // If empty, update immediately 75 86 if (!value?.length) { 76 - debounceRef.current = setTimeout(() => { 77 - setDebouncedUrl(value); 78 - setImageState(ImageState.Idle); 79 - }, 0); 80 87 return () => { 81 88 if (debounceRef.current) { 82 89 clearTimeout(debounceRef.current); 83 90 } 84 91 }; 85 92 } 86 - 87 - setImageState(ImageState.Loading); 88 93 89 94 debounceRef.current = setTimeout(() => { 90 95 setDebouncedUrl(value);
+1 -1
components/ui/audio-player.tsx
··· 101 101 id: url, 102 102 src: url 103 103 }); 104 - }, [url]); 104 + }, [url, player]); 105 105 106 106 return null; 107 107 }
-3
eslint.config.mjs
··· 40 40 ...next.configs.recommended.rules, 41 41 ...next.configs["core-web-vitals"].rules, 42 42 43 - // Fixes 44 43 "react/prop-types": "off", 45 - "react-hooks/exhaustive-deps": "off", 46 - "react-hooks/preserve-manual-memoization": "off", 47 44 48 45 // stylistic Rules 49 46 "@stylistic/array-bracket-newline": ["error", "consistent"],
+1 -1
lib/api/hook.ts
··· 24 24 [key]: value 25 25 } as T)); 26 26 }, 27 - [data] 27 + [data, queryClient, url] 28 28 ); 29 29 30 30 if (data && typeof data === "object" && "message" in data && typeof data.message === "string") {
+18 -1
next.config.ts
··· 5 5 output: "standalone", 6 6 reactStrictMode: true, 7 7 experimental: { 8 - turbopackFileSystemCacheForDev: true 8 + turbopackFileSystemCacheForDev: true, 9 + optimizePackageImports: [ 10 + "lucide-react", 11 + "react-icons", 12 + "@radix-ui/react-accordion", 13 + "@radix-ui/react-avatar", 14 + "@radix-ui/react-checkbox", 15 + "@radix-ui/react-dialog", 16 + "@radix-ui/react-dropdown-menu", 17 + "@radix-ui/react-popover", 18 + "@radix-ui/react-scroll-area", 19 + "@radix-ui/react-separator", 20 + "@radix-ui/react-slider", 21 + "@radix-ui/react-slot", 22 + "@radix-ui/react-switch", 23 + "@radix-ui/react-tabs", 24 + "@radix-ui/react-tooltip" 25 + ] 9 26 }, 10 27 images: { 11 28 localPatterns: [
+16 -4
utils/captcha.ts
··· 12 12 const [error, setError] = useState<string | null>(null); 13 13 14 14 const button = useRef<HTMLButtonElement | null>(null); 15 + const stateRef = useRef(state); 16 + 17 + useEffect(() => { 18 + stateRef.current = state; 19 + }, [state]); 15 20 16 21 useEffect(() => { 17 22 if (!userId) return; 18 23 const { init } = gt4init(); 24 + let clickHandler: (() => void) | null = null; 25 + const btn = button.current; 19 26 20 27 init( 21 28 { ··· 29 36 30 37 // @ts-expect-error GeeTest types suck 31 38 function handlerForBind(captcha) { 32 - const btn = button.current; 33 39 let isReady = false; 34 40 35 41 captcha.onReady(() => { 36 42 isReady = true; 37 43 }); 38 44 39 - btn?.addEventListener("click", () => { 40 - if (!isReady || state === State.Success) return; 45 + clickHandler = () => { 46 + if (!isReady || stateRef.current === State.Success) return; 41 47 42 48 setState(State.Idle); 43 49 setError(null); 44 50 45 51 captcha.showCaptcha(); 46 - }); 52 + }; 53 + 54 + btn?.addEventListener("click", clickHandler); 47 55 48 56 captcha.onSuccess(async () => { 49 57 setState(State.Loading); ··· 85 93 setState(State.Idle); 86 94 }); 87 95 } 96 + 97 + return () => { 98 + if (clickHandler) btn?.removeEventListener("click", clickHandler); 99 + }; 88 100 }, [path, userId]); 89 101 90 102 return { state, error, button };
+18 -13
utils/input.ts
··· 41 41 42 42 const endpoint = options.endpoint || options.url; 43 43 const k = options.k || options.dataName; 44 + const { onSave, transform, manual, debounceMs, defaultState } = options; 44 45 45 - const defaultStateKey = JSON.stringify(options.defaultState); 46 - useEffect(() => { 47 - setValue(options.defaultState); 48 - setSavedValue(options.defaultState); 46 + const defaultStateKey = JSON.stringify(defaultState); 47 + const [prevDefaultStateKey, setPrevDefaultStateKey] = useState(defaultStateKey); 48 + if (defaultStateKey !== prevDefaultStateKey) { 49 + setPrevDefaultStateKey(defaultStateKey); 50 + setValue(defaultState); 51 + setSavedValue(defaultState); 52 + } 49 53 54 + useEffect(() => { 50 55 return () => { 51 56 if (timeout.current) { 52 57 clearTimeout(timeout.current); ··· 58 63 debounceRef.current = null; 59 64 } 60 65 }; 61 - }, [defaultStateKey]); 66 + }, []); 62 67 63 68 const save = useCallback( 64 69 async (val?: T) => { 65 70 const valueToSave = val === undefined ? value : val; 66 - options.onSave?.(valueToSave); 71 + onSave?.(valueToSave); 67 72 setSavedValue(valueToSave); 68 73 69 74 if (!endpoint || !k) return; ··· 83 88 "Content-Type": "application/json" 84 89 }, 85 90 body: JSON.stringify(k.includes(".") 86 - ? { [k.split(".")[0]]: { [k.split(".")[1]]: options.transform?.(valueToSave) ?? valueToSave } } 87 - : { [k]: options.transform?.(valueToSave) ?? valueToSave } 91 + ? { [k.split(".")[0]]: { [k.split(".")[1]]: transform?.(valueToSave) ?? valueToSave } } 92 + : { [k]: transform?.(valueToSave) ?? valueToSave } 88 93 ) 89 94 }) 90 95 .catch((error) => String(error)); ··· 108 113 setState(InputState.Success); 109 114 timeout.current = setTimeout(() => setState(InputState.Idle), 1_000 * 8); 110 115 }, 111 - [options.onSave, endpoint, k, options.transform, value] 116 + [onSave, endpoint, k, transform, value] 112 117 ); 113 118 114 119 const update = useCallback( 115 120 (val: T) => { 116 121 setValue(val); 117 122 118 - if (options.manual) return; 123 + if (manual) return; 119 124 120 125 if (debounceRef.current) { 121 126 clearTimeout(debounceRef.current); 122 127 } 123 128 124 - if (options.debounceMs) { 125 - debounceRef.current = setTimeout(() => save(val), options.debounceMs); 129 + if (debounceMs) { 130 + debounceRef.current = setTimeout(() => save(val), debounceMs); 126 131 } else { 127 132 save(val); 128 133 } 129 134 }, 130 - [options.manual, options.debounceMs, save] 135 + [manual, debounceMs, save] 131 136 ); 132 137 133 138 return {