Add debounce hook (#3524)

* Remove noop
* Add debounce hook
This commit is contained in:
DingDongSoLong4 2023-03-13 04:24:37 +02:00 committed by GitHub
parent 798b3e6dd7
commit bc3730d49f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 180 additions and 181 deletions

View file

@ -53,6 +53,10 @@
"import/namespace": "off",
"import/no-unresolved": "off",
"react/display-name": "off",
"react-hooks/exhaustive-deps": [
"error",
{ "additionalHooks": "^(useDebounce)$" }
],
"react/prop-types": "off",
"react/style-prop-object": [
"error",

View file

@ -1,10 +1,8 @@
import debounce from "lodash-es/debounce";
import cloneDeep from "lodash-es/cloneDeep";
import React, {
HTMLAttributes,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
@ -40,6 +38,7 @@ import {
faRandom,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { useDebounce } from "src/hooks/debounce";
const maxPageSize = 1000;
interface IListFilterProps {
@ -79,13 +78,15 @@ export const ListFilter: React.FC<IListFilterProps> = ({
[filter, onFilterUpdate]
);
// useMemo to prevent debounce from being recreated on every render
const debouncedSearchQueryUpdated = useMemo(
() =>
debounce((value: string) => {
searchQueryUpdated(value);
}, 500),
[searchQueryUpdated]
const searchCallback = useDebounce(
(value: string) => {
const newFilter = cloneDeep(filter);
newFilter.searchTerm = value;
newFilter.currentPage = 1;
onFilterUpdate(newFilter);
},
[filter, onFilterUpdate],
500
);
const intl = useIntl();
@ -145,7 +146,7 @@ export const ListFilter: React.FC<IListFilterProps> = ({
}
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
debouncedSearchQueryUpdated(event.currentTarget.value);
searchCallback(event.currentTarget.value);
setQueryClearShowing(!!event.currentTarget.value);
}

View file

@ -1,5 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import debounce from "lodash-es/debounce";
import { Button, Form } from "react-bootstrap";
import { useIntl } from "react-intl";
@ -7,6 +6,7 @@ import * as GQL from "src/core/generated-graphql";
import { ModalComponent } from "src/components/Shared/Modal";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useScrapePerformerList } from "src/core/StashService";
import { useDebouncedSetState } from "src/hooks/debounce";
const CLASSNAME = "PerformerScrapeModal";
const CLASSNAME_LIST = `${CLASSNAME}-list`;
@ -33,9 +33,7 @@ const PerformerScrapeModal: React.FC<IProps> = ({
const performers = data?.scrapeSinglePerformer ?? [];
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onInputChange = useDebouncedSetState(setQuery, 500);
useEffect(() => inputRef.current?.focus(), []);

View file

@ -1,5 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import debounce from "lodash-es/debounce";
import { Button, Form } from "react-bootstrap";
import { useIntl } from "react-intl";
@ -7,6 +6,7 @@ import * as GQL from "src/core/generated-graphql";
import { ModalComponent } from "src/components/Shared/Modal";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { stashboxDisplayName } from "src/utils/stashbox";
import { useDebouncedSetState } from "src/hooks/debounce";
const CLASSNAME = "PerformerScrapeModal";
const CLASSNAME_LIST = `${CLASSNAME}-list`;
@ -44,9 +44,7 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
const performers = data?.scrapeSinglePerformer ?? [];
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onInputChange = useDebouncedSetState(setQuery, 500);
useEffect(() => inputRef.current?.focus(), []);

View file

@ -3,14 +3,7 @@ import {
faCheckCircle,
faTimesCircle,
} from "@fortawesome/free-solid-svg-icons";
import debounce from "lodash-es/debounce";
import React, {
useState,
useEffect,
useMemo,
useCallback,
useRef,
} from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Spinner } from "react-bootstrap";
import { IUIConfig } from "src/core/config";
import * as GQL from "src/core/generated-graphql";
@ -23,6 +16,7 @@ import {
useConfigureScraping,
useConfigureUI,
} from "src/core/StashService";
import { useDebounce } from "src/hooks/debounce";
import { useToast } from "src/hooks/Toast";
import { withoutTypename } from "src/utils/data";
import { Icon } from "../Shared/Icon";
@ -76,40 +70,34 @@ export const SettingsContext: React.FC = ({ children }) => {
const initialRef = useRef(false);
const [general, setGeneral] = useState<GQL.ConfigGeneralInput>({});
const [pendingGeneral, setPendingGeneral] = useState<
GQL.ConfigGeneralInput | undefined
>();
const [pendingGeneral, setPendingGeneral] =
useState<GQL.ConfigGeneralInput>();
const [updateGeneralConfig] = useConfigureGeneral();
const [iface, setIface] = useState<GQL.ConfigInterfaceInput>({});
const [pendingInterface, setPendingInterface] = useState<
GQL.ConfigInterfaceInput | undefined
>();
const [pendingInterface, setPendingInterface] =
useState<GQL.ConfigInterfaceInput>();
const [updateInterfaceConfig] = useConfigureInterface();
const [defaults, setDefaults] = useState<GQL.ConfigDefaultSettingsInput>({});
const [pendingDefaults, setPendingDefaults] = useState<
GQL.ConfigDefaultSettingsInput | undefined
>();
const [pendingDefaults, setPendingDefaults] =
useState<GQL.ConfigDefaultSettingsInput>();
const [updateDefaultsConfig] = useConfigureDefaults();
const [scraping, setScraping] = useState<GQL.ConfigScrapingInput>({});
const [pendingScraping, setPendingScraping] = useState<
GQL.ConfigScrapingInput | undefined
>();
const [pendingScraping, setPendingScraping] =
useState<GQL.ConfigScrapingInput>();
const [updateScrapingConfig] = useConfigureScraping();
const [dlna, setDLNA] = useState<GQL.ConfigDlnaInput>({});
const [pendingDLNA, setPendingDLNA] = useState<
GQL.ConfigDlnaInput | undefined
>();
const [pendingDLNA, setPendingDLNA] = useState<GQL.ConfigDlnaInput>();
const [updateDLNAConfig] = useConfigureDLNA();
const [ui, setUI] = useState({});
const [pendingUI, setPendingUI] = useState<{} | undefined>();
const [pendingUI, setPendingUI] = useState<{}>();
const [updateUIConfig] = useConfigureUI();
const [updateSuccess, setUpdateSuccess] = useState<boolean | undefined>();
const [updateSuccess, setUpdateSuccess] = useState<boolean>();
const [apiKey, setApiKey] = useState("");
@ -146,13 +134,7 @@ export const SettingsContext: React.FC = ({ children }) => {
setUI(data.configuration.ui);
}, [data, error]);
const resetSuccess = useMemo(
() =>
debounce(() => {
setUpdateSuccess(undefined);
}, 4000),
[]
);
const resetSuccess = useDebounce(() => setUpdateSuccess(undefined), [], 4000);
const onSuccess = useCallback(() => {
setUpdateSuccess(true);
@ -160,24 +142,24 @@ export const SettingsContext: React.FC = ({ children }) => {
}, [resetSuccess]);
// saves the configuration if no further changes are made after a half second
const saveGeneralConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigGeneralInput) => {
try {
setUpdateSuccess(undefined);
await updateGeneralConfig({
variables: {
input,
},
});
const saveGeneralConfig = useDebounce(
async (input: GQL.ConfigGeneralInput) => {
try {
setUpdateSuccess(undefined);
await updateGeneralConfig({
variables: {
input,
},
});
setPendingGeneral(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateGeneralConfig, onSuccess]
setPendingGeneral(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
},
[updateGeneralConfig, onSuccess],
500
);
useEffect(() => {
@ -210,24 +192,24 @@ export const SettingsContext: React.FC = ({ children }) => {
}
// saves the configuration if no further changes are made after a half second
const saveInterfaceConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigInterfaceInput) => {
try {
setUpdateSuccess(undefined);
await updateInterfaceConfig({
variables: {
input,
},
});
const saveInterfaceConfig = useDebounce(
async (input: GQL.ConfigInterfaceInput) => {
try {
setUpdateSuccess(undefined);
await updateInterfaceConfig({
variables: {
input,
},
});
setPendingInterface(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateInterfaceConfig, onSuccess]
setPendingInterface(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
},
[updateInterfaceConfig, onSuccess],
500
);
useEffect(() => {
@ -260,24 +242,24 @@ export const SettingsContext: React.FC = ({ children }) => {
}
// saves the configuration if no further changes are made after a half second
const saveDefaultsConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigDefaultSettingsInput) => {
try {
setUpdateSuccess(undefined);
await updateDefaultsConfig({
variables: {
input,
},
});
const saveDefaultsConfig = useDebounce(
async (input: GQL.ConfigDefaultSettingsInput) => {
try {
setUpdateSuccess(undefined);
await updateDefaultsConfig({
variables: {
input,
},
});
setPendingDefaults(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateDefaultsConfig, onSuccess]
setPendingDefaults(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
},
[updateDefaultsConfig, onSuccess],
500
);
useEffect(() => {
@ -310,24 +292,24 @@ export const SettingsContext: React.FC = ({ children }) => {
}
// saves the configuration if no further changes are made after a half second
const saveScrapingConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigScrapingInput) => {
try {
setUpdateSuccess(undefined);
await updateScrapingConfig({
variables: {
input,
},
});
const saveScrapingConfig = useDebounce(
async (input: GQL.ConfigScrapingInput) => {
try {
setUpdateSuccess(undefined);
await updateScrapingConfig({
variables: {
input,
},
});
setPendingScraping(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateScrapingConfig, onSuccess]
setPendingScraping(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
},
[updateScrapingConfig, onSuccess],
500
);
useEffect(() => {
@ -360,24 +342,24 @@ export const SettingsContext: React.FC = ({ children }) => {
}
// saves the configuration if no further changes are made after a half second
const saveDLNAConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigDlnaInput) => {
try {
setUpdateSuccess(undefined);
await updateDLNAConfig({
variables: {
input,
},
});
const saveDLNAConfig = useDebounce(
async (input: GQL.ConfigDlnaInput) => {
try {
setUpdateSuccess(undefined);
await updateDLNAConfig({
variables: {
input,
},
});
setPendingDLNA(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateDLNAConfig, onSuccess]
setPendingDLNA(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
},
[updateDLNAConfig, onSuccess],
500
);
useEffect(() => {
@ -410,24 +392,24 @@ export const SettingsContext: React.FC = ({ children }) => {
}
// saves the configuration if no further changes are made after a half second
const saveUIConfig = useMemo(
() =>
debounce(async (input: IUIConfig) => {
try {
setUpdateSuccess(undefined);
await updateUIConfig({
variables: {
input,
},
});
const saveUIConfig = useDebounce(
async (input: IUIConfig) => {
try {
setUpdateSuccess(undefined);
await updateUIConfig({
variables: {
input,
},
});
setPendingUI(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateUIConfig, onSuccess]
setPendingUI(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
},
[updateUIConfig, onSuccess],
500
);
useEffect(() => {

View file

@ -1,11 +1,11 @@
import React, { useEffect, useState, useMemo } from "react";
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Button, InputGroup, Form } from "react-bootstrap";
import debounce from "lodash-es/debounce";
import { Icon } from "../Icon";
import { LoadingIndicator } from "../LoadingIndicator";
import { useDirectory } from "src/core/StashService";
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { useDebouncedSetState } from "src/hooks/debounce";
interface IProps {
currentDirectory: string;
@ -20,22 +20,15 @@ export const FolderSelect: React.FC<IProps> = ({
defaultDirectories,
appendButton,
}) => {
const [debouncedDirectory, setDebouncedDirectory] =
useState(currentDirectory);
const { data, error, loading } = useDirectory(debouncedDirectory);
const [directory, setDirectory] = useState(currentDirectory);
const { data, error, loading } = useDirectory(directory);
const intl = useIntl();
const selectableDirectories: string[] = currentDirectory
? data?.directory.directories ?? defaultDirectories ?? []
: defaultDirectories ?? [];
const debouncedSetDirectory = useMemo(
() =>
debounce((input: string) => {
setDebouncedDirectory(input);
}, 250),
[]
);
const debouncedSetDirectory = useDebouncedSetState(setDirectory, 250);
useEffect(() => {
if (currentDirectory === "" && !defaultDirectories && data?.directory.path)
@ -44,7 +37,7 @@ export const FolderSelect: React.FC<IProps> = ({
function setInstant(value: string) {
setCurrentDirectory(value);
setDebouncedDirectory(value);
setDirectory(value);
}
function setDebounced(value: string) {

View file

@ -10,7 +10,6 @@ import Select, {
OptionsOrGroups,
} from "react-select";
import CreatableSelect from "react-select/creatable";
import debounce from "lodash-es/debounce";
import * as GQL from "src/core/generated-graphql";
import {
@ -31,6 +30,7 @@ import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
import { TagPopover } from "../Tags/TagPopover";
import { defaultMaxOptionsShown, IUIConfig } from "src/core/config";
import { useDebouncedSetState } from "src/hooks/debounce";
export type SelectObject = {
id: string;
@ -354,9 +354,7 @@ export const GallerySelect: React.FC<ITitledSelect> = (props) => {
value: g.id,
}));
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onInputChange = useDebouncedSetState(setQuery, 500);
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
const selected = getSelectedItems(selectedItems);
@ -407,9 +405,7 @@ export const SceneSelect: React.FC<ITitledSelect> = (props) => {
value: s.id,
}));
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onInputChange = useDebouncedSetState(setQuery, 500);
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
const selected = getSelectedItems(selectedItems);
@ -459,9 +455,7 @@ export const ImageSelect: React.FC<ITitledSelect> = (props) => {
value: s.id,
}));
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onInputChange = useDebouncedSetState(setQuery, 500);
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
const selected = getSelectedItems(selectedItems);

View file

@ -1,8 +1,8 @@
import React, { useRef, useState } from "react";
import { Overlay, Tooltip } from "react-bootstrap";
import { Placement } from "react-bootstrap/Overlay";
import debounce from "lodash-es/debounce";
import cx from "classnames";
import { useDebounce } from "src/hooks/debounce";
const CLASSNAME = "TruncatedText";
const CLASSNAME_TOOLTIP = `${CLASSNAME}-tooltip`;
@ -25,9 +25,13 @@ export const TruncatedText: React.FC<ITruncatedTextProps> = ({
const [showTooltip, setShowTooltip] = useState(false);
const target = useRef(null);
if (!text) return <></>;
const startShowingTooltip = useDebounce(
() => setShowTooltip(true),
[],
delay
);
const startShowingTooltip = debounce(() => setShowTooltip(true), delay);
if (!text) return <></>;
const handleFocus = (element: HTMLElement) => {
// Check if visible size is smaller than the content size

View file

@ -1,8 +1,9 @@
import { useEffect, useRef, useState } from "react";
import noop from "lodash-es/noop";
const MIN_VALID_INTERVAL = 1000;
function noop() {}
const useInterval = (
callback: () => void,
delay: number | null = 5000

View file

@ -10,7 +10,6 @@ import {
} from "react-bootstrap";
import cx from "classnames";
import Mousetrap from "mousetrap";
import debounce from "lodash-es/debounce";
import { Icon } from "src/components/Shared/Icon";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
@ -45,6 +44,7 @@ import {
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { useDebounce } from "../debounce";
const CLASSNAME = "Lightbox";
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
@ -197,8 +197,9 @@ export const LightboxComponent: React.FC<IProps> = ({
}
}, [isSwitchingPage, images, index]);
const disableInstantTransition = debounce(
const disableInstantTransition = useDebounce(
() => setInstantTransition(false),
[],
400
);

View file

@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import { DebounceSettings } from "lodash-es";
import debounce, { DebouncedFunc } from "lodash-es/debounce";
import React, { useCallback } from "react";
export function useDebounce<T extends (...args: any) => any>(
fn: T,
deps: React.DependencyList,
wait?: number,
options?: DebounceSettings
): DebouncedFunc<T> {
return useCallback(debounce(fn, wait, options), [...deps, wait, options]);
}
// Convenience hook for use with state setters
export function useDebouncedSetState<S>(
fn: React.Dispatch<React.SetStateAction<S>>,
wait?: number,
options?: DebounceSettings
): DebouncedFunc<React.Dispatch<React.SetStateAction<S>>> {
return useDebounce(fn, [], wait, options);
}