diff --git a/packages/create-react/src/components/views/CellTowersView.tsx b/packages/create-react/src/components/views/CellTowersView.tsx index 0f1f530..af4a396 100644 --- a/packages/create-react/src/components/views/CellTowersView.tsx +++ b/packages/create-react/src/components/views/CellTowersView.tsx @@ -10,7 +10,7 @@ import { Card } from '../Card'; import { Layers } from '../Layers'; import { FormulaWidget } from '../widgets/FormulaWidget'; import { CategoryWidget } from '../widgets/CategoryWidget'; -import { useDebouncedState } from '../../hooks'; +import { useDebouncedState } from '../../hooks/useDebouncedState'; import { AppContext } from '../../context'; const MAP_VIEW = new MapView({ repeat: true }); diff --git a/packages/create-react/src/components/views/PopulationView.tsx b/packages/create-react/src/components/views/PopulationView.tsx index 5a72b07..1c94edc 100644 --- a/packages/create-react/src/components/views/PopulationView.tsx +++ b/packages/create-react/src/components/views/PopulationView.tsx @@ -9,6 +9,7 @@ import { Legend } from '../legends/Legend'; import { Layers } from '../Layers'; import { Card } from '../Card'; import { AppContext } from '../../context'; +import { useDebouncedState } from '../../hooks/useDebouncedState'; const MAP_VIEW = new MapView({ repeat: true }); const MAP_STYLE = @@ -31,7 +32,7 @@ const POP_COLORS: AccessorFunction = colorContinuous({ export default function PopulationView() { const { accessToken, apiBaseUrl } = useContext(AppContext); const [attributionHTML, setAttributionHTML] = useState(''); - const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); + const [viewState, setViewState] = useDebouncedState(INITIAL_VIEW_STATE, 200); /**************************************************************************** * Sources (https://deck.gl/docs/api-reference/carto/data-sources) diff --git a/packages/create-react/src/components/widgets/CategoryWidget.tsx b/packages/create-react/src/components/widgets/CategoryWidget.tsx index d9dfbb9..24caeb5 100644 --- a/packages/create-react/src/components/widgets/CategoryWidget.tsx +++ b/packages/create-react/src/components/widgets/CategoryWidget.tsx @@ -15,7 +15,7 @@ import { WidgetStatus, numberFormatter, } from '../../utils'; -import { useToggleFilter } from '../../hooks'; +import { useToggleFilter } from '../../hooks/useToggleFilter'; const { IN } = FilterType; diff --git a/packages/create-react/src/hooks.ts b/packages/create-react/src/hooks.ts deleted file mode 100644 index 6641cc1..0000000 --- a/packages/create-react/src/hooks.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { - addFilter, - Filter, - FilterType, - getFilter, - removeFilter, -} from '@carto/api-client'; -import { useState, useEffect, useRef } from 'react'; - -type Timeout = ReturnType; - -/** - * A debounced alternative to `useState`, API-equivalent except for the - * addition of `delay` as the second parameter to the hook. - * - * Example: - * ``` - * const [value, setValue] = useDebouncedState(0, 200); - * ``` - * - * When `setValue` is called, state does not change immediately. Instead - * the hook waits (`delay` milliseconds) before flushing changes to state. - * Any additional calls to `setValue` will reset the timer, such that state - * is not updated until `delay` milliseconds have passed since the last - * change. - */ -export function useDebouncedState( - initialValue: T, - delay: number, -): [T, (state: T) => void] { - const [debouncedValue, setDebouncedValue] = useState(initialValue); - - // Last value passed, will be assigned to debouncedValue after debounce. - const pendingValueRef = useRef(initialValue); - - // Active timeout, if any. - const setValueTimeoutRef = useRef(null); - - // Component-scoped `setValue` function, with debounce. - const setValueRef = useRef((value: T) => { - if (setValueTimeoutRef.current) { - clearTimeout(setValueTimeoutRef.current); - setValueTimeoutRef.current = null; - } - - pendingValueRef.current = value; - setValueTimeoutRef.current = setTimeout(() => { - setDebouncedValue(pendingValueRef.current); - setValueTimeoutRef.current = null; - }, delay); - }); - - // When component unmounts, cancel any active timeout. - useEffect(() => { - return () => { - if (setValueTimeoutRef.current) { - clearTimeout(setValueTimeoutRef.current); - setValueTimeoutRef.current = null; - } - }; - }, []); - - return [debouncedValue, setValueRef.current]; -} - -/** - * A debounced alternative to `useEffect`. - */ -export function useDebouncedEffect( - callback: () => void | (() => void), - delay = 200, - deps: unknown[] = [], -) { - const firstTimeRef = useRef(true); - const clearRef = useRef<(() => void) | void>(undefined); - - useEffect( - () => { - if (firstTimeRef.current) { - firstTimeRef.current = false; - return; - } - - const timeout = setTimeout(() => { - if (clearRef.current && typeof clearRef.current === 'function') { - clearRef.current(); - } - clearRef.current = callback(); - }, delay); - - return () => clearTimeout(timeout); - }, - // TODO: Should 'callback' really be omitted? - // eslint-disable-next-line react-hooks/exhaustive-deps - [delay, ...deps], - ); -} - -export type ToggleFilterProps = { - column: string; - owner: string; - filters?: Record; - onChange?: (filters: Record) => void; -}; - -export function useToggleFilter({ - column, - owner, - filters, - onChange, -}: ToggleFilterProps): (category: string) => void { - const { IN } = FilterType; - - return function onToggleFilter(category: string): void { - if (!filters || !onChange) return; - - const filter = getFilter(filters, { column, type: IN, owner }); - - let values: string[]; - - if (!filter) { - values = [category]; - } else if ((filter.values as string[]).includes(category)) { - values = (filter.values as string[]).filter( - (v: string) => v !== category, - ); - } else { - values = [...(filter.values as string[]), category]; - } - - if (values.length > 0) { - filters = addFilter(filters, { - column, - type: IN, - owner, - values: values, - }); - } else { - filters = removeFilter(filters, { column, owner }); - } - - onChange({ ...filters }); - }; -} diff --git a/packages/create-react/src/hooks/useDebouncedEffect.ts b/packages/create-react/src/hooks/useDebouncedEffect.ts new file mode 100644 index 0000000..a476c82 --- /dev/null +++ b/packages/create-react/src/hooks/useDebouncedEffect.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef } from 'react'; + +/** + * A debounced alternative to `useEffect`. + */ +export function useDebouncedEffect( + callback: () => void | (() => void), + delay = 200, + deps: unknown[] = [], +) { + const firstTimeRef = useRef(true); + const clearRef = useRef<(() => void) | void>(undefined); + + useEffect( + () => { + if (firstTimeRef.current) { + firstTimeRef.current = false; + return; + } + + const timeout = setTimeout(() => { + if (clearRef.current && typeof clearRef.current === 'function') { + clearRef.current(); + } + clearRef.current = callback(); + }, delay); + + return () => clearTimeout(timeout); + }, + // TODO: Should 'callback' really be omitted? + // eslint-disable-next-line react-hooks/exhaustive-deps + [delay, ...deps], + ); +} diff --git a/packages/create-react/src/hooks/useDebouncedState.ts b/packages/create-react/src/hooks/useDebouncedState.ts new file mode 100644 index 0000000..2ad31b5 --- /dev/null +++ b/packages/create-react/src/hooks/useDebouncedState.ts @@ -0,0 +1,57 @@ +import { useState, useEffect, useRef } from 'react'; + +type Timeout = ReturnType; + +/** + * A debounced alternative to `useState`, API-equivalent except for the + * addition of `delay` as the second parameter to the hook. + * + * Example: + * ``` + * const [value, setValue] = useDebouncedState(0, 200); + * ``` + * + * When `setValue` is called, state does not change immediately. Instead + * the hook waits (`delay` milliseconds) before flushing changes to state. + * Any additional calls to `setValue` will reset the timer, such that state + * is not updated until `delay` milliseconds have passed since the last + * change. + */ +export function useDebouncedState( + initialValue: T, + delay: number, +): [T, (state: T) => void] { + const [debouncedValue, setDebouncedValue] = useState(initialValue); + + // Last value passed, will be assigned to debouncedValue after debounce. + const pendingValueRef = useRef(initialValue); + + // Active timeout, if any. + const setValueTimeoutRef = useRef(null); + + // Component-scoped `setValue` function, with debounce. + const setValueRef = useRef((value: T) => { + if (setValueTimeoutRef.current) { + clearTimeout(setValueTimeoutRef.current); + setValueTimeoutRef.current = null; + } + + pendingValueRef.current = value; + setValueTimeoutRef.current = setTimeout(() => { + setDebouncedValue(pendingValueRef.current); + setValueTimeoutRef.current = null; + }, delay); + }); + + // When component unmounts, cancel any active timeout. + useEffect(() => { + return () => { + if (setValueTimeoutRef.current) { + clearTimeout(setValueTimeoutRef.current); + setValueTimeoutRef.current = null; + } + }; + }, []); + + return [debouncedValue, setValueRef.current]; +} diff --git a/packages/create-react/src/hooks/useToggleFilter.ts b/packages/create-react/src/hooks/useToggleFilter.ts new file mode 100644 index 0000000..80ec317 --- /dev/null +++ b/packages/create-react/src/hooks/useToggleFilter.ts @@ -0,0 +1,54 @@ +import { + addFilter, + Filter, + FilterType, + getFilter, + removeFilter, +} from '@carto/api-client'; + +export type ToggleFilterProps = { + column: string; + owner: string; + filters?: Record; + onChange?: (filters: Record) => void; +}; + +export function useToggleFilter({ + column, + owner, + filters, + onChange, +}: ToggleFilterProps): (category: string) => void { + const { IN } = FilterType; + + return function onToggleFilter(category: string): void { + if (!filters || !onChange) return; + + const filter = getFilter(filters, { column, type: IN, owner }); + + let values: string[]; + + if (!filter) { + values = [category]; + } else if ((filter.values as string[]).includes(category)) { + values = (filter.values as string[]).filter( + (v: string) => v !== category, + ); + } else { + values = [...(filter.values as string[]), category]; + } + + if (values.length > 0) { + filters = addFilter(filters, { + column, + type: IN, + owner, + values: values, + }); + } else { + filters = removeFilter(filters, { column, owner }); + } + + onChange({ ...filters }); + }; +} diff --git a/packages/create-vue/src/components/widgets/CategoryWidget.vue b/packages/create-vue/src/components/widgets/CategoryWidget.vue index f75231e..754955f 100644 --- a/packages/create-vue/src/components/widgets/CategoryWidget.vue +++ b/packages/create-vue/src/components/widgets/CategoryWidget.vue @@ -17,7 +17,7 @@ import { WidgetStatus, numberFormatter, } from '../../utils'; -import { useToggleFilter } from '../../hooks'; +import { useToggleFilter } from '../../hooks/useToggleFilter'; const props = withDefaults( defineProps<{ diff --git a/packages/create-vue/src/hooks.ts b/packages/create-vue/src/hooks/useToggleFilter.ts similarity index 100% rename from packages/create-vue/src/hooks.ts rename to packages/create-vue/src/hooks/useToggleFilter.ts