Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 44 additions & 15 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
import {buildConfig} from '@ver0/eslint-config';
import {fixupPluginRules} from '@eslint/compat';
import {defineConfig} from 'eslint/config';
import javascript from '@ver0/eslint-config/javascript.js';
import typescript from '@ver0/eslint-config/typescript.js';
import react from '@ver0/eslint-config/react.js';
import browser from '@ver0/eslint-config/browser.js';
import json from '@ver0/eslint-config/json.js';
import markdown from '@ver0/eslint-config/markdown.js';
import vitest from '@ver0/eslint-config/vitest.js';
import prettier from '@ver0/eslint-config/prettier.js';

/** @typedef {import('eslint').Linter} Linter */
/** @type {Linter.Config[]} */
const config = [
const config = defineConfig(
{
ignores: ['.claude', '.idea', 'node_modules', 'dist', 'coverage', 'CHANGELOG.md'],
},
...buildConfig({
globals: 'browser',
prettier: true,
typescript: true,
json: true,
markdown: true,
react: true,
vitest: true,
}),
javascript,
typescript,
react,
browser,
...json,
markdown,
vitest,
prettier,
{
// eslint 10 removed context.getFilename(), but eslint-plugin-react's
// version auto-detection ('detect', set by xo-react) still calls it and
// crashes; an explicit version skips the detection path.
settings: {
react: {
version: '19.2',
},
},
},
{
files: ['**/*.ts'],
rules: {
// Render-time ref reads/writes are the core mechanism of many hooks
// in this library (useSyncedRef, useFirstMountState, usePrevious...).
'react-hooks/refs': 'off',
},
},
{
files: ['**/*.test.ts'],
rules: {
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/strict-void-return': 'off',
'vitest/expect-expect': [
'error',
{
Expand All @@ -45,6 +70,10 @@ const config = [
'@typescript-eslint/no-deprecated': 'off',
},
},
];
);

export default config;
// fixupPluginRules restores context APIs removed in eslint 10 --
// eslint-plugin-react 7.x (via xo-react) still relies on them.
export default config.map((cfg) =>
cfg.plugins?.react ? {...cfg, plugins: {...cfg.plugins, react: fixupPluginRules(cfg.plugins.react)}} : cfg,
);
25 changes: 23 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,43 @@
}
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.1",
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@eslint/json": "^1.0.1",
"@eslint/markdown": "^7.5.1",
"@types/confusing-browser-globals": "^1",
"@types/js-cookie": "^3.0.6",
"@types/node": "^26.1.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@ver0/eslint-config": "^1.3.6",
"@ver0/eslint-config": "^1.7.1",
"@ver0/eslint-formatter-gha": "^1.0.1",
"@ver0/react-hooks-testing": "^1.0.3",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.37.0",
"@vitest/eslint-plugin": "^1.6.9",
"confusing-browser-globals": "^1.0.11",
"eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8",
"eslint-config-xo": "^0.50.0",
"eslint-config-xo-react": "^0.29.0",
"eslint-config-xo-typescript": "^9.0.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-no-use-extend-native": "^0.7.2",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unicorn": "^63.0.0",
"globals": "^17.4.0",
"js-cookie": "^3.0.8",
"jsdom": "^29.1.1",
"prettier": "^3.8.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"rimraf": "^6.1.3",
"semantic-release": "^25.0.8",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"vitest": "^3.2.4"
},
"packageManager": "yarn@4.10.2"
Expand Down
2 changes: 1 addition & 1 deletion src/useCustomCompareMemo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ export const useCustomCompareMemo = <T, Deps extends DependencyList>(
dependencies.current = deps;
}

// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/use-memo
return useMemo<T>(factory, dependencies.current);
};
3 changes: 1 addition & 2 deletions src/useEventListener/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import {hasOwnProperty, off, on} from '../util/misc.js';
export function useEventListener<T extends EventTarget>(
target: RefObject<T | null> | T | null,
...params:
| Parameters<T['addEventListener']>
| [string, EventListenerOrEventListenerObject | ((...args: any[]) => any), ...any]
Parameters<T['addEventListener']> | [string, EventListenerOrEventListenerObject | ((...args: any[]) => any), ...any]
): void {
const isMounted = useIsMounted();

Expand Down
8 changes: 4 additions & 4 deletions src/useFunctionalState/index.ssr.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {renderHookServer as renderHook} from '@ver0/react-hooks-testing';
import {describe, expect, it} from 'vitest';
import {useFunctionalState} from '../index.js';
import {expectResultValue} from '../util/testing/test-helpers.js';

describe('useFunctionalState', () => {
it('should be defined', () => {
Expand All @@ -14,9 +15,8 @@ describe('useFunctionalState', () => {

it('should return proper values', async () => {
const {result} = await renderHook(() => useFunctionalState(1));
if (result.value !== undefined) {
expect(result.value[1]).toBeInstanceOf(Function);
expect(result.value[0]).toBeInstanceOf(Function);
}
const value = expectResultValue(result);
expect(value[1]).toBeInstanceOf(Function);
expect(value[0]).toBeInstanceOf(Function);
});
});
2 changes: 1 addition & 1 deletion src/useMediatedState/index.dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('useMediatedState', () => {

it('should pass initial sate through mediator', async () => {
const {result} = await renderHook(() =>
useMediatedState('a123', (value: string) => value.replaceAll(/[^a-z]+/gi, '')),
useMediatedState('a123', (value: string) => value.replaceAll(/[^a-z]+/giv, '')),
);
const value = expectResultValue(result);

Expand Down
29 changes: 16 additions & 13 deletions src/usePermission/index.dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ describe('usePermission', () => {
async () =>
new Promise((resolve) => {
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
resolve({state: 'prompt', addEventListener() {}, removeEventListener() {}} as PermissionStatus);
resolve({state: 'prompt', addEventListener() {}, removeEventListener() {}});
}, 1);
}),
);
Expand Down Expand Up @@ -68,21 +67,25 @@ describe('usePermission', () => {
});

it('should update hook state on permission state change', async () => {
const createStatus = () => {
const status = {
state: 'prompt',
addEventListener(_n: any, listener: any) {
status.state = 'granted';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return
setTimeout(() => listener(), 1);
},
removeEventListener() {},
};

return status;
};

querySpy.mockImplementation(
async () =>
new Promise((resolve) => {
setTimeout(() => {
const status = {
state: 'prompt',
addEventListener(_n: any, listener: any) {
status.state = 'granted';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return
setTimeout(() => listener(), 1);
},
removeEventListener() {},
};

resolve(status);
resolve(createStatus());
}, 1);
}),
);
Expand Down
3 changes: 3 additions & 0 deletions src/usePermission/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export function usePermission(descriptor: PermissionDescriptor): UsePermissionSt
useEffect(() => {
const unmount: RefObject<(() => void) | null> = {current: null};

// Synchronous set is deliberate -- 'requested' must be observable before
// the permissions query resolves.
// eslint-disable-next-line react-hooks/set-state-in-effect
setState('requested');

// eslint-disable-next-line @typescript-eslint/no-floating-promises,promise/catch-or-return
Expand Down
5 changes: 2 additions & 3 deletions src/useQueue/index.ssr.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {renderHookServer as renderHook} from '@ver0/react-hooks-testing';
import {describe, expect, it} from 'vitest';
import {useQueue} from '../index.js';
import {expectResultValue} from '../util/testing/test-helpers.js';

describe('useQueue', () => {
it('should be defined', () => {
Expand All @@ -14,8 +15,6 @@ describe('useQueue', () => {

it('should return an object', async () => {
const {result} = await renderHook(() => useQueue());
if (result.value !== undefined) {
expect(result.value).toBeInstanceOf(Object);
}
expect(expectResultValue(result)).toBeInstanceOf(Object);
});
});
22 changes: 11 additions & 11 deletions src/useRerender/index.ssr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {act, renderHookServer as renderHook} from '@ver0/react-hooks-testing';
import {useRef} from 'react';
import {describe, expect, it} from 'vitest';
import {useRerender} from '../index.js';
import {expectResultValue} from '../util/testing/test-helpers.js';

describe('useRerender', () => {
it('should be defined', () => {
Expand All @@ -16,16 +17,15 @@ describe('useRerender', () => {
return [rerender, ++cnt.current] as const;
});

if (result.value !== undefined) {
expect(result.value[1]).toBe(1);
await act(async () => {
result.value[0]();
});
expect(result.value[1]).toBe(1);
await act(async () => {
result.value[0]();
});
expect(result.value[1]).toBe(1);
}
const value = expectResultValue(result);
expect(value[1]).toBe(1);
await act(async () => {
value[0]();
});
expect(value[1]).toBe(1);
await act(async () => {
value[0]();
});
expect(value[1]).toBe(1);
});
});
4 changes: 2 additions & 2 deletions src/useStorageValue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@ export function useStorageValue<
// Make actions static so developers can pass methods further
const staticActions = useMemo(
() => ({
set: ((v) => {
set(v: NextState<Type, UseStorageValueValue<Type, Default, Initialize>>) {
actions.current.set(v);
}) as typeof actions.current.set,
},
remove() {
actions.current.delete();
},
Expand Down
6 changes: 2 additions & 4 deletions src/useValidator/index.ssr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ describe('useValidator', () => {

it('should return undefined validity on first render', async () => {
const {result} = await renderHook(() => useValidator(() => ({isValid: true}), []));
if (result.value !== undefined) {
const value = expectResultValue(result);
expect(value[0].isValid).toBeUndefined();
}
const value = expectResultValue(result);
expect(value[0].isValid).toBeUndefined();
});

it('should not call validator on first render', async () => {
Expand Down
3 changes: 1 addition & 2 deletions src/util/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export function on<T extends EventTarget>(
export function off<T extends EventTarget>(
object: T | null,
...args:
| Parameters<T['removeEventListener']>
| [string, EventListenerOrEventListenerObject | CallableFunction, ...any]
Parameters<T['removeEventListener']> | [string, EventListenerOrEventListenerObject | CallableFunction, ...any]
): void {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
object?.removeEventListener(...(args as Parameters<HTMLElement['removeEventListener']>));
Expand Down
2 changes: 1 addition & 1 deletion src/util/testing/setup/vibrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import {vi} from 'vitest';

const vibrateMock = vi.fn<typeof navigator.vibrate>(() => true);

globalThis.navigator.vibrate = vibrateMock as typeof navigator.vibrate;
globalThis.navigator.vibrate = vibrateMock;
Loading
Loading