diff --git a/eslint.config.js b/eslint.config.js index 93c1f26bd8..d7ec962737 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,12 @@ -import reactX from '@eslint-react/eslint-plugin'; +import eslintReact from '@eslint-react/eslint-plugin'; import markdown from '@eslint/markdown'; import vitest from '@vitest/eslint-plugin'; import jestDom from 'eslint-plugin-jest-dom'; +import reactDom from 'eslint-plugin-react-dom'; import reactHooks from 'eslint-plugin-react-hooks'; +import reactNamingConvention from 'eslint-plugin-react-naming-convention'; +import reactRsc from 'eslint-plugin-react-rsc'; +import reactWebApi from 'eslint-plugin-react-web-api'; import sonarjs from 'eslint-plugin-sonarjs'; import testingLibrary from 'eslint-plugin-testing-library'; import { defineConfig, globalIgnores } from 'eslint/config'; @@ -24,7 +28,11 @@ export default defineConfig([ plugins: { // @ts-expect-error 'react-hooks': reactHooks, - 'react-x': reactX, + '@eslint-react': eslintReact, + '@eslint-react/rsc': reactRsc, + '@eslint-react/dom': reactDom, + '@eslint-react/web-api': reactWebApi, + '@eslint-react/naming-convention': reactNamingConvention, sonarjs, '@typescript-eslint': tseslint.plugin }, @@ -276,12 +284,127 @@ export default defineConfig([ 'react-hooks/unsupported-syntax': 1, 'react-hooks/use-memo': 1, - // React Hooks Extra - // https://eslint-react.xyz/ - 'react-x/no-unnecessary-use-callback': 1, - 'react-x/no-unnecessary-use-memo': 1, - 'react-x/no-unnecessary-use-prefix': 1, - 'react-x/prefer-use-state-lazy-initialization': 1, + // ESLint React + // https://www.eslint-react.xyz/docs/rules/overview + /* +// copy all the rules from the rules table for easy pasting +function getRules(id, prefix) { + return ( + Iterator.from( + document + // select rules table + .querySelector(`#${id} ~ *:has(table) > table`) + // select all rule links + .querySelectorAll('tr a') + ) + // map link to rule declaration + .map((a) => `'@eslint-react/${prefix}${a.textContent}': 1,`) + ); +} +copy( + Iterator.from([ + getRules('x-rules', ''), + getRules('rsc-rules', 'rsc/'), + getRules('dom-rules', 'dom/'), + getRules('web-api-rules', 'web-api/'), + getRules('naming-convention-rules', 'naming-convention/'), + ]) + .flatMap((x) => x) + .toArray() + .join('\n') +); + */ + '@eslint-react/component-hook-factories': 1, + '@eslint-react/error-boundaries': 1, + '@eslint-react/exhaustive-deps': 1, + '@eslint-react/jsx-dollar': 1, + '@eslint-react/jsx-key-before-spread': 1, + '@eslint-react/jsx-no-comment-textnodes': 1, + '@eslint-react/jsx-no-duplicate-props': 1, + '@eslint-react/jsx-shorthand-boolean': 1, + '@eslint-react/jsx-shorthand-fragment': 1, + '@eslint-react/jsx-uses-react': 1, + '@eslint-react/jsx-uses-vars': 1, + '@eslint-react/no-access-state-in-setstate': 1, + '@eslint-react/no-array-index-key': 0, + '@eslint-react/no-children-count': 1, + '@eslint-react/no-children-for-each': 1, + '@eslint-react/no-children-map': 1, + '@eslint-react/no-children-only': 1, + '@eslint-react/no-children-prop': 1, + '@eslint-react/no-children-to-array': 1, + '@eslint-react/no-class-component': 1, + '@eslint-react/no-clone-element': 1, + '@eslint-react/no-component-will-mount': 1, + '@eslint-react/no-component-will-receive-props': 1, + '@eslint-react/no-component-will-update': 1, + '@eslint-react/no-context-provider': 1, + '@eslint-react/no-create-ref': 1, + '@eslint-react/no-direct-mutation-state': 1, + '@eslint-react/no-duplicate-key': 1, + '@eslint-react/no-forward-ref': 1, + '@eslint-react/no-implicit-key': 1, + '@eslint-react/no-leaked-conditional-rendering': 1, + '@eslint-react/no-missing-component-display-name': 1, + '@eslint-react/no-missing-context-display-name': 1, + '@eslint-react/no-missing-key': 1, + '@eslint-react/no-misused-capture-owner-stack': 1, + '@eslint-react/no-nested-component-definitions': 1, + '@eslint-react/no-nested-lazy-component-declarations': 1, + '@eslint-react/no-redundant-should-component-update': 1, + '@eslint-react/no-set-state-in-component-did-mount': 1, + '@eslint-react/no-set-state-in-component-did-update': 1, + '@eslint-react/no-set-state-in-component-will-update': 1, + '@eslint-react/no-unnecessary-use-callback': 1, + '@eslint-react/no-unnecessary-use-memo': 1, + '@eslint-react/no-unnecessary-use-prefix': 1, + '@eslint-react/no-unsafe-component-will-mount': 1, + '@eslint-react/no-unsafe-component-will-receive-props': 1, + '@eslint-react/no-unsafe-component-will-update': 1, + '@eslint-react/no-unstable-context-value': 1, + '@eslint-react/no-unstable-default-props': 1, + '@eslint-react/no-unused-class-component-members': 1, + '@eslint-react/no-unused-props': 1, + '@eslint-react/no-unused-state': 1, + '@eslint-react/no-use-context': 1, + '@eslint-react/no-useless-fragment': [1, { allowExpressions: false }], + '@eslint-react/prefer-destructuring-assignment': 1, + '@eslint-react/prefer-namespace-import': 1, + '@eslint-react/purity': 1, + '@eslint-react/refs': 1, + '@eslint-react/rules-of-hooks': 1, + '@eslint-react/set-state-in-effect': 0, + '@eslint-react/set-state-in-render': 1, + '@eslint-react/unsupported-syntax': 1, + '@eslint-react/use-memo': 1, + '@eslint-react/use-state': 1, + '@eslint-react/rsc/function-definition': 1, + '@eslint-react/dom/no-dangerously-set-innerhtml': 1, + '@eslint-react/dom/no-dangerously-set-innerhtml-with-children': 1, + '@eslint-react/dom/no-find-dom-node': 1, + '@eslint-react/dom/no-flush-sync': 0, + '@eslint-react/dom/no-hydrate': 1, + '@eslint-react/dom/no-missing-button-type': 1, + '@eslint-react/dom/no-missing-iframe-sandbox': 1, + '@eslint-react/dom/no-namespace': 1, + '@eslint-react/dom/no-render': 1, + '@eslint-react/dom/no-render-return-value': 1, + '@eslint-react/dom/no-script-url': 1, + '@eslint-react/dom/no-string-style-prop': 1, + '@eslint-react/dom/no-unknown-property': 0, + '@eslint-react/dom/no-unsafe-iframe-sandbox': 1, + '@eslint-react/dom/no-unsafe-target-blank': 1, + '@eslint-react/dom/no-use-form-state': 1, + '@eslint-react/dom/no-void-elements-with-children': 1, + '@eslint-react/dom/prefer-namespace-import': 1, + '@eslint-react/web-api/no-leaked-event-listener': 1, + '@eslint-react/web-api/no-leaked-interval': 1, + '@eslint-react/web-api/no-leaked-resize-observer': 1, + '@eslint-react/web-api/no-leaked-timeout': 1, + '@eslint-react/naming-convention/component-name': 1, + '@eslint-react/naming-convention/context-name': 1, + '@eslint-react/naming-convention/id-name': 1, + '@eslint-react/naming-convention/ref-name': 1, // SonarJS rules // https://github.com/SonarSource/SonarJS/blob/master/packages/jsts/src/rules/README.md#rules @@ -290,7 +413,7 @@ export default defineConfig([ copy( Iterator.from( document - // selecto rules table + // select rules table .querySelector('.markdown-heading:has(> a[href="#rules"]) ~ markdown-accessiblity-table') // select all rows with a rule .querySelectorAll('tr:has(a)') @@ -438,7 +561,7 @@ copy( 'sonarjs/no-identical-functions': 1, 'sonarjs/no-ignored-exceptions': 1, 'sonarjs/no-ignored-return': 1, - 'sonarjs/no-implicit-dependencies': 1, + 'sonarjs/no-implicit-dependencies': 0, 'sonarjs/no-implicit-global': 1, 'sonarjs/no-in-misuse': 1, 'sonarjs/no-incomplete-assertions': 1, @@ -654,19 +777,7 @@ copy( '@typescript-eslint/no-redeclare': 1, '@typescript-eslint/no-redundant-type-constituents': 1, '@typescript-eslint/no-require-imports': 1, - '@typescript-eslint/no-restricted-imports': [ - 1, - { - name: 'react', - importNames: ['default'], - message: 'Use named imports instead.' - }, - { - name: 'react-dom', - importNames: ['default'], - message: 'Use named imports instead.' - } - ], + '@typescript-eslint/no-restricted-imports': 0, '@typescript-eslint/no-restricted-types': 0, '@typescript-eslint/no-shadow': 0, '@typescript-eslint/no-this-alias': 1, @@ -803,6 +914,9 @@ copy( rules: { '@typescript-eslint/no-floating-promises': 1, + '@eslint-react/component-hook-factories': 0, + '@eslint-react/no-create-ref': 0, + // https://github.com/vitest-dev/eslint-plugin-vitest#rules 'vitest/consistent-each-for': 1, 'vitest/consistent-test-filename': 0, diff --git a/package.json b/package.json index da78680b9d..35c5896238 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "typecheck": "tsc --build" }, "devDependencies": { - "@eslint-react/eslint-plugin": "^2.3.12", + "@eslint-react/eslint-plugin": "^3.0.0-beta.42", "@eslint/markdown": "^7.5.1", "@faker-js/faker": "^10.0.0", "@tanstack/react-router": "^1.132.31", diff --git a/src/Columns.tsx b/src/Columns.tsx index 19e1533238..163b3280d7 100644 --- a/src/Columns.tsx +++ b/src/Columns.tsx @@ -4,13 +4,13 @@ import { SelectCellFormatter } from './cellRenderers'; export const SELECT_COLUMN_KEY = 'rdg-select-column'; -function HeaderRenderer(props: RenderHeaderCellProps) { +function HeaderRenderer({ tabIndex }: RenderHeaderCellProps) { const { isIndeterminate, isRowSelected, onRowSelectionChange } = useHeaderRowSelection(); return ( { @@ -20,32 +20,32 @@ function HeaderRenderer(props: RenderHeaderCellProps) { ); } -function SelectFormatter(props: RenderCellProps) { +function SelectFormatter({ row, tabIndex }: RenderCellProps) { const { isRowSelectionDisabled, isRowSelected, onRowSelectionChange } = useRowSelection(); return ( { - onRowSelectionChange({ row: props.row, checked, isShiftClick }); + onRowSelectionChange({ row, checked, isShiftClick }); }} /> ); } -function SelectGroupFormatter(props: RenderGroupCellProps) { +function SelectGroupFormatter({ row, tabIndex }: RenderGroupCellProps) { const { isRowSelected, onRowSelectionChange } = useRowSelection(); return ( { - onRowSelectionChange({ row: props.row, checked, isShiftClick: false }); + onRowSelectionChange({ row, checked, isShiftClick: false }); }} /> ); diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index ebaeb25d86..9d8586c45b 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -315,8 +315,8 @@ export function DataGrid(props: DataGridPr const [columnWidthsInternal, setColumnWidthsInternal] = useState( (): ColumnWidths => columnWidthsRaw ?? new Map() ); - const [isColumnResizing, setColumnResizing] = useState(false); - const [isDragging, setDragging] = useState(false); + const [isColumnResizing, setIsColumnResizing] = useState(false); + const [isDragging, setIsDragging] = useState(false); const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); const [scrollToPosition, setScrollToPosition] = useState(null); const [shouldFocusCell, setShouldFocusCell] = useState(false); @@ -454,7 +454,7 @@ export function DataGrid(props: DataGridPr columnWidths, onColumnWidthsChange, onColumnResize, - setColumnResizing + setIsColumnResizing ); const minColIdx = isTreeGrid ? -1 : 0; @@ -695,7 +695,7 @@ export function DataGrid(props: DataGridPr // This check is needed as double click on the resize handle triggers onPointerMove if (isColumnResizing) { onColumnWidthsChangeRaw?.(columnWidths); - setColumnResizing(false); + setIsColumnResizing(false); } } @@ -705,7 +705,7 @@ export function DataGrid(props: DataGridPr if (event.pointerType === 'mouse' && event.button !== 0) { return; } - setDragging(true); + setIsDragging(true); event.currentTarget.setPointerCapture(event.pointerId); } @@ -728,7 +728,7 @@ export function DataGrid(props: DataGridPr } function handleDragHandleLostPointerCapture() { - setDragging(false); + setIsDragging(false); if (draggedOverRowIdx === undefined) return; const { rowIdx } = selectedPosition; diff --git a/src/DataGridDefaultRenderersContext.ts b/src/DataGridDefaultRenderersContext.ts index 5cf1bcbe10..06aca65cd6 100644 --- a/src/DataGridDefaultRenderersContext.ts +++ b/src/DataGridDefaultRenderersContext.ts @@ -1,10 +1,11 @@ -import { createContext, useContext } from 'react'; +import { createContext, use } from 'react'; import type { Maybe, Renderers } from './types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const DataGridDefaultRenderersContext = createContext>>(undefined); +DataGridDefaultRenderersContext.displayName = 'DataGridDefaultRenderersContext'; export function useDefaultRenderers(): Maybe> { - return useContext(DataGridDefaultRenderersContext); + return use(DataGridDefaultRenderersContext); } diff --git a/src/EditCell.tsx b/src/EditCell.tsx index 1138192035..35b5638007 100644 --- a/src/EditCell.tsx +++ b/src/EditCell.tsx @@ -115,12 +115,12 @@ export default function EditCell({ } } - addEventListener('mousedown', onWindowCaptureMouseDown, { capture: true }); - addEventListener('mousedown', onWindowMouseDown); + window.addEventListener('mousedown', onWindowCaptureMouseDown, { capture: true }); + window.addEventListener('mousedown', onWindowMouseDown); return () => { - removeEventListener('mousedown', onWindowCaptureMouseDown, { capture: true }); - removeEventListener('mousedown', onWindowMouseDown); + window.removeEventListener('mousedown', onWindowCaptureMouseDown, { capture: true }); + window.removeEventListener('mousedown', onWindowMouseDown); cancelTask(); }; }, [commitOnOutsideClick]); diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index 7a9183cfce..5a2388504d 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -21,7 +21,7 @@ export function useColumnWidths( } | null>(null); const [columnsToMeasureOnResize, setColumnsToMeasureOnResize] = useState | null>(null); - const [prevGridWidth, setPreviousGridWidth] = useState(gridWidth); + const [prevGridWidth, setPrevGridWidth] = useState(gridWidth); const columnsCanFlex: boolean = columns.length === viewportColumns.length; const ignorePreviouslyMeasuredColumnsOnGridWidthChange = // Allow columns to flex again when... @@ -57,7 +57,7 @@ export function useColumnWidths( useLayoutEffect(updateMeasuredAndResizedWidths); function updateMeasuredAndResizedWidths() { - setPreviousGridWidth(gridWidth); + setPrevGridWidth(gridWidth); if (columnsToMeasure.length === 0) return; const newColumnWidths = new Map(columnWidths); diff --git a/src/hooks/useRowSelection.ts b/src/hooks/useRowSelection.ts index a70438d811..ea6c019697 100644 --- a/src/hooks/useRowSelection.ts +++ b/src/hooks/useRowSelection.ts @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import { createContext, use } from 'react'; import type { SelectHeaderRowEvent, SelectRowEvent } from '../types'; @@ -8,15 +8,17 @@ export interface RowSelectionContextValue { } export const RowSelectionContext = createContext(undefined); +RowSelectionContext.displayName = 'RowSelectionContext'; export const RowSelectionChangeContext = createContext< // eslint-disable-next-line @typescript-eslint/no-explicit-any ((selectRowEvent: SelectRowEvent) => void) | undefined >(undefined); +RowSelectionChangeContext.displayName = 'RowSelectionChangeContext'; export function useRowSelection() { - const rowSelectionContext = useContext(RowSelectionContext); - const rowSelectionChangeContext = useContext(RowSelectionChangeContext); + const rowSelectionContext = use(RowSelectionContext); + const rowSelectionChangeContext = use(RowSelectionChangeContext); if (rowSelectionContext === undefined || rowSelectionChangeContext === undefined) { throw new Error('useRowSelection must be used within renderCell'); @@ -37,14 +39,16 @@ export interface HeaderRowSelectionContextValue { export const HeaderRowSelectionContext = createContext( undefined ); +HeaderRowSelectionContext.displayName = 'HeaderRowSelectionContext'; export const HeaderRowSelectionChangeContext = createContext< ((selectRowEvent: SelectHeaderRowEvent) => void) | undefined >(undefined); +HeaderRowSelectionChangeContext.displayName = 'HeaderRowSelectionChangeContext'; export function useHeaderRowSelection() { - const headerRowSelectionContext = useContext(HeaderRowSelectionContext); - const headerRowSelectionChangeContext = useContext(HeaderRowSelectionChangeContext); + const headerRowSelectionContext = use(HeaderRowSelectionContext); + const headerRowSelectionChangeContext = use(HeaderRowSelectionChangeContext); if (headerRowSelectionContext === undefined || headerRowSelectionChangeContext === undefined) { throw new Error('useHeaderRowSelection must be used within renderHeaderCell'); diff --git a/website/directionContext.ts b/website/directionContext.ts index 03d01540ad..f7bed75ccb 100644 --- a/website/directionContext.ts +++ b/website/directionContext.ts @@ -1,9 +1,10 @@ -import { createContext, useContext } from 'react'; +import { createContext, use } from 'react'; import type { Direction } from '../src/types'; export const DirectionContext = createContext('ltr'); +DirectionContext.displayName = 'DirectionContext'; export function useDirection(): Direction { - return useContext(DirectionContext); + return use(DirectionContext); } diff --git a/website/routes/ContextMenu.tsx b/website/routes/ContextMenu.tsx index 58655488e1..7753a188a7 100644 --- a/website/routes/ContextMenu.tsx +++ b/website/routes/ContextMenu.tsx @@ -74,10 +74,10 @@ function ContextMenuDemo() { setContextMenuProps(null); } - addEventListener('mousedown', onMouseDown); + window.addEventListener('mousedown', onMouseDown); return () => { - removeEventListener('mousedown', onMouseDown); + window.removeEventListener('mousedown', onMouseDown); }; }, [isContextMenuOpen]); diff --git a/website/routes/HeaderFilters.tsx b/website/routes/HeaderFilters.tsx index f4b26fd95b..c283a079d0 100644 --- a/website/routes/HeaderFilters.tsx +++ b/website/routes/HeaderFilters.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useMemo, useState } from 'react'; +import { createContext, use, useMemo, useState } from 'react'; import { faker } from '@faker-js/faker'; import { css } from 'ecij'; @@ -66,6 +66,7 @@ interface Filter extends Omit { // Context is needed to read filter values otherwise columns are // re-created when filters are changed and filter loses focus const FilterContext = createContext(undefined); +FilterContext.displayName = 'FilterContext'; function inputStopPropagation(event: React.KeyboardEvent) { if (['ArrowLeft', 'ArrowRight'].includes(event.key)) { @@ -316,7 +317,7 @@ function FilterRenderer({ }: RenderHeaderCellProps & { children: (args: { tabIndex: number; filters: Filter }) => React.ReactElement; }) { - const filters = useContext(FilterContext)!; + const filters = use(FilterContext)!; return ( <>
{column.name}
diff --git a/website/routes/NoRows.tsx b/website/routes/NoRows.tsx index ccc9c3c61c..14861c8e42 100644 --- a/website/routes/NoRows.tsx +++ b/website/routes/NoRows.tsx @@ -44,7 +44,7 @@ function rowKeyGetter(row: Row) { function NoRows() { const direction = useDirection(); - const [selectedRows, onSelectedRowsChange] = useState((): ReadonlySet => new Set()); + const [selectedRows, setSelectedRows] = useState((): ReadonlySet => new Set()); return ( }} selectedRows={selectedRows} - onSelectedRowsChange={onSelectedRowsChange} + onSelectedRowsChange={setSelectedRows} rowKeyGetter={rowKeyGetter} className={gridClassname} direction={direction}