Skip to content

Commit e09b32b

Browse files
committed
refactor custom hooks; update linters
1 parent 9501190 commit e09b32b

9 files changed

+52
-40
lines changed

.eslintrc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
{
22
"parser": "@typescript-eslint/parser",
33
"extends": [
4-
"eslint:recommended",
5-
"plugin:@typescript-eslint/recommended",
64
"plugin:react/recommended",
7-
"prettier"
5+
"plugin:@typescript-eslint/recommended"
86
],
97
"plugins": [
10-
"prettier",
8+
"@typescript-eslint",
119
"react-hooks",
12-
"@typescript-eslint"
10+
"prettier"
1311
],
1412
"env": {
1513
"browser": true,
@@ -19,6 +17,8 @@
1917
},
2018
"rules": {
2119
"react/prop-types": 0,
20+
"react-hooks/rules-of-hooks": "error",
21+
"react-hooks/exhaustive-deps": "warn",
2222
"semi": "off",
2323
"sort-keys": "off",
2424
"global-require": "off",
@@ -32,13 +32,14 @@
3232
},
3333
"parserOptions": {
3434
"sourceType": "module",
35+
"ecmaVersion": 2020,
3536
"ecmaFeatures": {
36-
"impliedStrict": true,
3737
"jsx": true
3838
}
3939
},
4040
"settings": {
4141
"react": {
42+
"pragma": "React",
4243
"version": "detect"
4344
}
4445
}

src/Select.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import type {
5151
import type { FixedSizeList } from 'react-window';
5252
import styled, { css, ThemeProvider, type DefaultTheme } from 'styled-components';
5353
import { Menu, Value, AriaLiveRegion, AutosizeInput, IndicatorIcons } from './components';
54-
import { useDebounce, useLatestRef, useCallbackRef, useMenuOptions, useMountEffect, useUpdateEffect, useMenuPositioner } from './hooks';
54+
import { useDebounce, useLatestRef, useCallbackRef, useMenuOptions, useMountEffect, useUpdateEffect, useMenuPosition } from './hooks';
5555
import { isBoolean, isFunction, isPlainObject, mergeDeep, suppressEvent, normalizeValue, isTouchDevice, isArrayWithLength } from './utils';
5656

5757
type SelectProps = Readonly<{
@@ -308,7 +308,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
308308
);
309309

310310
// Custom hook abstraction that handles calculating menuHeightCalc (defaults to menuMaxHeight) / handles executing callbacks/logic on menuOpen state change.
311-
const [menuStyleTop, menuHeightCalc] = useMenuPositioner(
311+
const { menuStyleTop, menuHeightCalc } = useMenuPosition(
312312
menuRef,
313313
controlRef,
314314
menuOpen,
@@ -320,7 +320,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
320320
onMenuOpen,
321321
onMenuClose,
322322
menuScrollDuration,
323-
scrollMenuIntoView,
323+
scrollMenuIntoView
324324
);
325325

326326
const blurInput = (): void => inputRef.current?.blur();
@@ -402,7 +402,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
402402
}
403403
}
404404
}),
405-
[getOptionValueFn, getOptionLabelFn, openMenuAndFocusOption]
405+
[menuOpenRef, getOptionValueFn, getOptionLabelFn, openMenuAndFocusOption]
406406
);
407407

408408
/**
@@ -423,7 +423,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
423423
onChangeEvtValue.current = false;
424424
onSearchChangeFn(debouncedInputValue);
425425
}
426-
}, [onSearchChangeFn, debouncedInputValue]);
426+
}, [onSearchChangeFn, onSearchChangeIsFn, debouncedInputValue]);
427427

428428
/**
429429
* useUpdateEffect
@@ -439,7 +439,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
439439

440440
onOptionChangeFn(normalSelectedOpts);
441441
}
442-
}, [onOptionChangeFn, isMulti, selectedOption]);
442+
}, [onOptionChangeFn, onOptionChangeIsFn, isMulti, selectedOption]);
443443

444444
/**
445445
* useUpdateEffect
@@ -463,7 +463,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
463463
}
464464

465465
prevMenuOptionsLength.current = curLength;
466-
}, [async, options, menuOptions]);
466+
}, [async, options, menuOpenRef, menuOptions]);
467467

468468
const selectOptionFromFocused = (): void => {
469469
const { index, ...menuOpt } = focusedOption;

src/hooks/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ export { default as useMountEffect } from './useMountEffect';
44
export { default as useMenuOptions } from './useMenuOptions';
55
export { default as useCallbackRef } from './useCallbackRef';
66
export { default as useUpdateEffect } from './useUpdateEffect';
7-
export { default as useMenuPositioner } from './useMenuPositioner';
7+
export { default as useMenuPosition } from './useMenuPosition';

src/hooks/useCallbackRef.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,21 @@ import type { CallbackFn } from '../types';
22
import { useEffect, useRef, useCallback } from 'react';
33

44
/**
5-
* Hook that converts a callback to a ref to avoid triggering re-renders when
6-
* passed as a prop or avoid re-executing effects when passed as a dependency
5+
* Creates a stable callback function that has access to the latest
6+
* state and can be used within event handlers and effect callbacks.
77
*
88
* @param callback the callback to write to ref object
99
*/
1010
const useCallbackRef = <T extends CallbackFn>(callback?: T): T => {
11-
const callbackRef = useRef(callback);
11+
const ref = useRef(callback);
1212

1313
useEffect(() => {
14-
callbackRef.current = callback;
14+
ref.current = callback;
1515
});
1616

17-
return useCallback(
18-
((...args) => {
19-
return callbackRef.current?.(...args);
20-
}) as T,
21-
[]
22-
);
17+
return useCallback<CallbackFn>((...args) => {
18+
return ref.current?.(...args);
19+
}, []) as T;
2320
};
2421

2522
export default useCallbackRef;

src/hooks/useMenuOptions.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const useMenuOptions = (
3232
): MenuOption[] => {
3333
const getIsOptionDisabledFn = useCallbackRef(getIsOptionDisabled || FUNCTIONS.isOptionDisabled);
3434
const getFilterOptionStringFn = useCallbackRef(getFilterOptionString || FUNCTIONS.optionFilter);
35-
const hideSelectedOptionsOrDefault = isBoolean(hideSelectedOptions) ? hideSelectedOptions : isMulti;
35+
const hideSelectedOptsOrDefault = isBoolean(hideSelectedOptions) ? hideSelectedOptions : isMulti;
3636
const searchValue = !async ? debouncedInputValue : ''; // prevent recomputing on input mutations in async mode
3737

3838
const menuOptions = useMemo<MenuOption[]>(() => {
@@ -44,7 +44,9 @@ const useMenuOptions = (
4444
if (!matchVal) return true;
4545
const filterVal = getFilterOptionStringFn(option);
4646
const normalFilterVal = trimAndFormatFilterStr(filterVal, filterIgnoreCase, filterIgnoreAccents);
47-
return isFilterMatchAny ? normalFilterVal.includes(matchVal) : normalFilterVal.startsWith(matchVal);
47+
return isFilterMatchAny
48+
? normalFilterVal.includes(matchVal)
49+
: normalFilterVal.startsWith(matchVal);
4850
};
4951

5052
const parseMenuOption = (data: OptionData): MenuOption | undefined => {
@@ -53,7 +55,7 @@ const useMenuOptions = (
5355
const isDisabled = getIsOptionDisabledFn(data);
5456
const isSelected = selectedValues.includes(value);
5557
const menuOption: MenuOption = { data, value, label, isDisabled, isSelected };
56-
return (!isOptionFilterMatch(menuOption) || (hideSelectedOptionsOrDefault && isSelected))
58+
return !isOptionFilterMatch(menuOption) || (hideSelectedOptsOrDefault && isSelected)
5759
? undefined
5860
: menuOption;
5961
};
@@ -74,7 +76,7 @@ const useMenuOptions = (
7476
filterIgnoreAccents,
7577
getIsOptionDisabledFn,
7678
getFilterOptionStringFn,
77-
hideSelectedOptionsOrDefault
79+
hideSelectedOptsOrDefault
7880
]);
7981

8082
return menuOptions;

src/hooks/useMenuPositioner.ts renamed to src/hooks/useMenuPosition.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
import type { CallbackFn } from '../types';
21
import useLatestRef from './useLatestRef';
2+
import type { CallbackFn } from '../types';
33
import useCallbackRef from './useCallbackRef';
44
import useUpdateEffect from './useUpdateEffect';
55
import { MenuPositionEnum } from '../constants';
66
import { useState, useRef, type RefObject } from 'react';
77
import { calculateMenuTop, menuFitsBelowControl, scrollMenuIntoViewOnOpen } from '../utils';
88

9+
type MenuPosition = Readonly<{
10+
menuStyleTop?: string;
11+
menuHeightCalc: number;
12+
}>;
13+
914
/**
1015
* Handle calculating and maintaining the menuHeight used by react-window.
1116
* Handle scroll animation and callback execution when menuOpen = true.
1217
* Handle resetting menuHeight back to the menuHeightDefault and callback execution when menuOpen = false.
1318
* Use ref to track if the menuHeight was resized, and if so, set the menu height back to default (avoids uncessary renders) with call to setMenuHeight.
1419
* Handle determining where to place the menu in relation to control - when menuPosition = 'top' or menuPosition = 'bottom' and there is not sufficient space below control, place on top.
1520
*/
16-
const useMenuPositioner = (
21+
const useMenuPosition = (
1722
menuRef: RefObject<HTMLElement | null>,
1823
controlRef: RefObject<HTMLElement | null>,
1924
menuOpen: boolean,
@@ -26,7 +31,7 @@ const useMenuPositioner = (
2631
onMenuClose?: CallbackFn,
2732
menuScrollDuration?: number,
2833
scrollMenuIntoView?: boolean
29-
): [string | undefined, number] => {
34+
): MenuPosition => {
3035
const isMenuTopPosition =
3136
menuPosition === MenuPositionEnum.TOP ||
3237
(menuPosition === MenuPositionEnum.AUTO && !menuFitsBelowControl(menuRef.current));
@@ -65,18 +70,23 @@ const useMenuPositioner = (
6570
}, [
6671
menuRef,
6772
menuOpen,
73+
shouldScrollRef,
6874
menuHeightDefault,
6975
scrollMenuIntoView,
7076
menuScrollDuration,
7177
onMenuOpenFn,
7278
onMenuCloseFn
7379
]);
7480

75-
// Calculated menu height passed react-window; calculate MenuWrapper div 'top' style prop if menu is positioned above control
81+
// calculate menu height for react-window
82+
// calculate MenuWrapper el 'top' css prop (if menu is positioned above control)
7683
const menuHeightCalc = Math.min(menuHeight, menuOptionsLength * menuItemSize);
7784
const menuStyleTop = isMenuTopPosition ? calculateMenuTop(menuHeightCalc, menuRef.current, controlRef.current) : undefined;
7885

79-
return [menuStyleTop, menuHeightCalc];
86+
return {
87+
menuStyleTop,
88+
menuHeightCalc
89+
};
8090
};
8191

82-
export default useMenuPositioner;
92+
export default useMenuPosition;

src/hooks/useMountEffect.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useEffect, type EffectCallback } from 'react';
66
* @param effect the effect to execute
77
*/
88
const useMountEffect = (effect: EffectCallback): void => {
9+
// eslint-disable-next-line react-hooks/exhaustive-deps
910
useEffect(effect, []);
1011
};
1112

src/hooks/useUpdateEffect.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { useRef, useEffect } from 'react';
1+
import {useRef, useEffect, type EffectCallback, type DependencyList} from 'react';
22

33
/**
4-
* Run an effect only on updates.
5-
* Skip the first effect execution that occurrs on initial mount.
4+
* `React.useEffect` that will not run on the first render.
65
*
76
* @param effect the effect to execute
87
* @param deps the dependency list
98
*/
10-
const useUpdateEffect: typeof useEffect = (effect, deps): void => {
9+
const useUpdateEffect = (effect: EffectCallback, deps?: DependencyList): void => {
1110
const isFirstRender = useRef(true);
1211

1312
useEffect(() => {
@@ -16,7 +15,8 @@ const useUpdateEffect: typeof useEffect = (effect, deps): void => {
1615
} else {
1716
return effect();
1817
}
18+
// eslint-disable-next-line react-hooks/exhaustive-deps
1919
}, deps);
2020
};
2121

22-
export default useUpdateEffect;
22+
export default useUpdateEffect;

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"compilerOptions": {
33
"jsx": "react",
4-
"target": "ES2020",
4+
"target": "ESNext",
55
"module": "ESNext",
6+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
67
"moduleResolution": "node",
78
"strict": true,
89
"skipLibCheck": true,

0 commit comments

Comments
 (0)