Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGrid] Refactor: simplify useGridApiEventHandler #16479

Merged
merged 6 commits into from
Feb 6, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ Below are described the steps you need to make to migrate from v7 to v8.
- Return early if `apiRef` is `null`
- Throw an error if `apiRef` is `null`

- `createUseGridApiEventHandler` is not exported anymore.

### Localization

- If `estimatedRowCount` is used, the text provided to the [Table Pagination](/material-ui/api/table-pagination/) component from the Material UI library is updated and requires additional translations. Check the example at the end of [Index-based pagination section](/x/react-data-grid/pagination/#index-based-pagination).
Expand Down
1 change: 1 addition & 0 deletions packages/x-data-grid/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './envConstants';
export * from './localeTextConstants';
export * from './gridClasses';
export * from './signature';
9 changes: 9 additions & 0 deletions packages/x-data-grid/src/constants/signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Signal to the underlying logic what version of the public component API
* of the Data Grid is exposed.
*/
export enum GridSignature {
DataGrid = 'DataGrid',
DataGridPro = 'DataGridPro',
DataGridPremium = 'DataGridPremium',
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RefObject } from '@mui/x-internals/types';
import { EventManager } from '@mui/x-internals/EventManager';
import { Store } from '../../utils/Store';
import { useGridApiMethod } from '../utils/useGridApiMethod';
import { GridSignature } from '../utils/useGridApiEventHandler';
import { GridSignature } from '../../constants/signature';
import { DataGridProcessedProps } from '../../models/props/DataGridProps';
import type { GridCoreApi } from '../../models';
import type { GridApiCommon, GridPrivateApiCommon } from '../../models/api/gridApiCommon';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
gridVisibleColumnDefinitionsSelector,
gridColumnPositionsSelector,
} from './gridColumnsSelector';
import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { GridSignature } from '../../../constants/signature';
import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import {
GridPipeProcessor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import { GridSignature } from '../../utils';
import { GridSignature } from '../../../constants/signature';

const MAX_PAGE_SIZE = 100;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
GridRowMultiSelectionApi,
} from '../../../models/api/gridRowSelectionApi';
import { GridGroupNode, GridRowId } from '../../../models/gridRows';
import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { GridSignature } from '../../../constants/signature';
import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { useGridApiMethod } from '../../utils/useGridApiMethod';
import { useGridLogger } from '../../utils/useGridLogger';
import { useGridSelector } from '../../utils/useGridSelector';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RefObject } from '@mui/x-internals/types';
import { GridSignature } from '../../utils/useGridApiEventHandler';
import { GridSignature } from '../../../constants/signature';
import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils';
import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector';
import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector';
Expand Down
3 changes: 2 additions & 1 deletion packages/x-data-grid/src/hooks/features/rows/useGridRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
gridRowGroupsToFetchSelector,
} from './gridRowsSelector';
import { useTimeout } from '../../utils/useTimeout';
import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { GridSignature } from '../../../constants/signature';
import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { GridStateInitializer } from '../../utils/useGridInitializeState';
import { getVisibleRows } from '../../utils/useGridVisibleRows';
import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector';
Expand Down
6 changes: 5 additions & 1 deletion packages/x-data-grid/src/hooks/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export * from './useGridApiEventHandler';
export {
useGridApiEventHandler,
useGridApiOptionHandler,
unstable_resetCleanupTracking,
} from './useGridApiEventHandler';
export * from './useGridApiMethod';
export * from './useGridLogger';
export { useGridSelector } from './useGridSelector';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expect } from 'chai';
import { createRenderer, reactMajor } from '@mui/internal-test-utils';
import { sleep } from 'test/utils/helperFn';
import { isJSDOM, testSkipIf } from 'test/utils/skipIf';
import { createUseGridApiEventHandler } from './useGridApiEventHandler';
import { useGridApiEventHandler, internal_registryContainer } from './useGridApiEventHandler';
import { FinalizationRegistryBasedCleanupTracking } from '../../utils/cleanupTracking/FinalizationRegistryBasedCleanupTracking';
import { TimerBasedCleanupTracking } from '../../utils/cleanupTracking/TimerBasedCleanupTracking';

Expand All @@ -18,9 +18,8 @@ describe('useGridApiEventHandler', () => {
testSkipIf(
!isJSDOM || typeof FinalizationRegistry === 'undefined' || typeof global.gc === 'undefined',
)('should unsubscribe event listeners registered by uncommitted components', async () => {
const useGridApiEventHandler = createUseGridApiEventHandler({
registry: new FinalizationRegistryBasedCleanupTracking(),
});
internal_registryContainer.current = new FinalizationRegistryBasedCleanupTracking();

const unsubscribe = spy();
const apiRef = {
current: { subscribeEvent: spy(() => unsubscribe) },
Expand Down Expand Up @@ -53,9 +52,8 @@ describe('useGridApiEventHandler', () => {

describe('Timer-based implementation', () => {
it('should unsubscribe event listeners registered by uncommitted components', async () => {
const useGridApiEventHandler = createUseGridApiEventHandler({
registry: new TimerBasedCleanupTracking(50),
});
internal_registryContainer.current = new TimerBasedCleanupTracking(50);

const unsubscribe = spy();
const apiRef = {
current: { subscribeEvent: spy(() => unsubscribe) },
Expand Down
177 changes: 83 additions & 94 deletions packages/x-data-grid/src/hooks/utils/useGridApiEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,71 @@ import * as React from 'react';
import { RefObject } from '@mui/x-internals/types';
import { EventListenerOptions } from '@mui/x-internals/EventManager';
import { GridEventListener, GridEvents } from '../../models/events';
import { UnregisterToken, CleanupTracking } from '../../utils/cleanupTracking/CleanupTracking';
import { UnregisterToken } from '../../utils/cleanupTracking/CleanupTracking';
import { TimerBasedCleanupTracking } from '../../utils/cleanupTracking/TimerBasedCleanupTracking';
import { FinalizationRegistryBasedCleanupTracking } from '../../utils/cleanupTracking/FinalizationRegistryBasedCleanupTracking';
import type { GridApiCommon } from '../../models';

/**
* Signal to the underlying logic what version of the public component API
* of the Data Grid is exposed.
*/
enum GridSignature {
DataGrid = 'DataGrid',
DataGridPro = 'DataGridPro',
DataGridPremium = 'DataGridPremium',
}
// Based on https://github.com/Bnaya/use-dispose-uncommitted/blob/main/src/finalization-registry-based-impl.ts
// Check https://github.com/facebook/react/issues/15317 to get more information

interface RegistryContainer {
registry: CleanupTracking | null;
// We use class to make it easier to detect in heap snapshots by name
class ObjectToBeRetainedByReact {
static create() {
return new ObjectToBeRetainedByReact();
}
}

// We use class to make it easier to detect in heap snapshots by name
class ObjectToBeRetainedByReact {}
const registryContainer = {
current: createRegistry(),
};

// Based on https://github.com/Bnaya/use-dispose-uncommitted/blob/main/src/finalization-registry-based-impl.ts
// Check https://github.com/facebook/react/issues/15317 to get more information
export function createUseGridApiEventHandler(registryContainer: RegistryContainer) {
let cleanupTokensCounter = 0;

return function useGridApiEventHandler<Api extends GridApiCommon, E extends GridEvents>(
apiRef: RefObject<Api>,
eventName: E,
handler?: GridEventListener<E>,
options?: EventListenerOptions,
) {
if (registryContainer.registry === null) {
registryContainer.registry =
typeof FinalizationRegistry !== 'undefined'
? new FinalizationRegistryBasedCleanupTracking()
: new TimerBasedCleanupTracking();
}
let cleanupTokensCounter = 0;

const [objectRetainedByReact] = React.useState(new ObjectToBeRetainedByReact());
const subscription = React.useRef<(() => void) | null>(null);
const handlerRef = React.useRef<GridEventListener<E> | undefined>(null);
handlerRef.current = handler;
const cleanupTokenRef = React.useRef<UnregisterToken | null>(null);
export function useGridApiEventHandler<Api extends GridApiCommon, E extends GridEvents>(
apiRef: RefObject<Api>,
eventName: E,
handler?: GridEventListener<E>,
options?: EventListenerOptions,
) {
const objectRetainedByReact = React.useState(ObjectToBeRetainedByReact.create)[0];
const subscription = React.useRef<(() => void) | null>(null);
const handlerRef = React.useRef<GridEventListener<E> | undefined>(null);
handlerRef.current = handler;
const cleanupTokenRef = React.useRef<UnregisterToken | null>(null);

if (!subscription.current && handlerRef.current) {
const enhancedHandler: GridEventListener<E> = (params, event, details) => {
if (!event.defaultMuiPrevented) {
handlerRef.current?.(params, event, details);
}
};

subscription.current = apiRef.current.subscribeEvent(eventName, enhancedHandler, options);

cleanupTokensCounter += 1;
cleanupTokenRef.current = { cleanupToken: cleanupTokensCounter };

registryContainer.current.register(
objectRetainedByReact, // The callback below will be called once this reference stops being retained
() => {
subscription.current?.();
subscription.current = null;
cleanupTokenRef.current = null;
},
cleanupTokenRef.current,
);
} else if (!handlerRef.current && subscription.current) {
subscription.current();
subscription.current = null;

if (cleanupTokenRef.current) {
registryContainer.current.unregister(cleanupTokenRef.current);
cleanupTokenRef.current = null;
}
}

React.useEffect(() => {
if (!subscription.current && handlerRef.current) {
const enhancedHandler: GridEventListener<E> = (params, event, details) => {
if (!event.defaultMuiPrevented) {
Expand All @@ -56,74 +75,44 @@ export function createUseGridApiEventHandler(registryContainer: RegistryContaine
};

subscription.current = apiRef.current.subscribeEvent(eventName, enhancedHandler, options);

cleanupTokensCounter += 1;
cleanupTokenRef.current = { cleanupToken: cleanupTokensCounter };

registryContainer.registry.register(
objectRetainedByReact, // The callback below will be called once this reference stops being retained
() => {
subscription.current?.();
subscription.current = null;
cleanupTokenRef.current = null;
},
cleanupTokenRef.current,
);
} else if (!handlerRef.current && subscription.current) {
subscription.current();
subscription.current = null;

if (cleanupTokenRef.current) {
registryContainer.registry.unregister(cleanupTokenRef.current);
cleanupTokenRef.current = null;
}
}

React.useEffect(() => {
if (!subscription.current && handlerRef.current) {
const enhancedHandler: GridEventListener<E> = (params, event, details) => {
if (!event.defaultMuiPrevented) {
handlerRef.current?.(params, event, details);
}
};

subscription.current = apiRef.current.subscribeEvent(eventName, enhancedHandler, options);
}

if (cleanupTokenRef.current && registryContainer.registry) {
// If the effect was called, it means that this render was committed
// so we can trust the cleanup function to remove the listener.
registryContainer.registry.unregister(cleanupTokenRef.current);
cleanupTokenRef.current = null;
}
if (cleanupTokenRef.current && registryContainer.current) {
// If the effect was called, it means that this render was committed
// so we can trust the cleanup function to remove the listener.
registryContainer.current.unregister(cleanupTokenRef.current);
cleanupTokenRef.current = null;
}

return () => {
subscription.current?.();
subscription.current = null;
};
}, [apiRef, eventName, options]);
};
return () => {
subscription.current?.();
subscription.current = null;
};
}, [apiRef, eventName, options]);
}

const registryContainer: RegistryContainer = { registry: null };

// TODO: move to @mui/x-data-grid/internals
// eslint-disable-next-line @typescript-eslint/naming-convention
export const unstable_resetCleanupTracking = () => {
registryContainer.registry?.reset();
registryContainer.registry = null;
};

export const useGridApiEventHandler = createUseGridApiEventHandler(registryContainer);

const optionsSubscriberOptions: EventListenerOptions = { isFirst: true };
const OPTIONS_IS_FIRST: EventListenerOptions = { isFirst: true };

export function useGridApiOptionHandler<Api extends GridApiCommon, E extends GridEvents>(
apiRef: RefObject<Api>,
eventName: E,
handler?: GridEventListener<E>,
) {
useGridApiEventHandler(apiRef, eventName, handler, optionsSubscriberOptions);
useGridApiEventHandler(apiRef, eventName, handler, OPTIONS_IS_FIRST);
}

// TODO: move to @mui/x-data-grid/internals
// eslint-disable-next-line @typescript-eslint/naming-convention
export function unstable_resetCleanupTracking() {
registryContainer.current?.reset();
registryContainer.current = createRegistry();
}

export { GridSignature };
// eslint-disable-next-line @typescript-eslint/naming-convention
export const internal_registryContainer = registryContainer;

function createRegistry() {
return typeof FinalizationRegistry !== 'undefined'
? new FinalizationRegistryBasedCleanupTracking()
: new TimerBasedCleanupTracking();
}
1 change: 1 addition & 0 deletions packages/x-data-grid/src/internals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type { GridPinnedRowsProps } from '../components/GridPinnedRows';
export { GridHeaders } from '../components/GridHeaders';
export { GridBaseColumnHeaders } from '../components/columnHeaders/GridBaseColumnHeaders';
export { DATA_GRID_DEFAULT_SLOTS_COMPONENTS } from '../constants/defaultGridSlotsComponents';
export * from '../constants/signature';

export { getGridFilter } from '../components/panel/filterPanel/GridFilterPanel';
export { getValueOptions } from '../components/panel/filterPanel/filterPanelUtils';
Expand Down
2 changes: 1 addition & 1 deletion packages/x-data-grid/src/internals/utils/propValidation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { warnOnce } from '@mui/x-internals/warning';
import { isNumber } from '../../utils/utils';
import { DataGridProcessedProps } from '../../models/props/DataGridProps';
import { GridSignature } from '../../hooks/utils/useGridApiEventHandler';
import { GridSignature } from '../../constants/signature';

export type PropValidator<TProps> = (props: TProps) => string | undefined;

Expand Down
5 changes: 2 additions & 3 deletions scripts/x-data-grid-premium.exports.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
{ "name": "ColumnsStylesInterface", "kind": "Interface" },
{ "name": "COMFORTABLE_DENSITY_FACTOR", "kind": "Variable" },
{ "name": "COMPACT_DENSITY_FACTOR", "kind": "Variable" },
{ "name": "createUseGridApiEventHandler", "kind": "Function" },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a breaking change
migration guide needs an entry for the removal

{ "name": "CursorCoordinates", "kind": "Interface" },
{ "name": "DATA_GRID_PREMIUM_PROPS_DEFAULT_VALUES", "kind": "Variable" },
{ "name": "DataGrid", "kind": "Function" },
Expand Down Expand Up @@ -677,10 +676,10 @@
{ "name": "unstable_gridDefaultPromptResolver", "kind": "Function" },
{ "name": "Unstable_GridToolbarPromptControl", "kind": "Function" },
{ "name": "unstable_PromptResponse", "kind": "TypeAlias" },
{ "name": "unstable_resetCleanupTracking", "kind": "Variable" },
{ "name": "unstable_resetCleanupTracking", "kind": "Function" },
{ "name": "useFirstRender", "kind": "Variable" },
{ "name": "useGridApiContext", "kind": "Variable" },
{ "name": "useGridApiEventHandler", "kind": "Variable" },
{ "name": "useGridApiEventHandler", "kind": "Function" },
{ "name": "useGridApiMethod", "kind": "Function" },
{ "name": "useGridApiOptionHandler", "kind": "Function" },
{ "name": "useGridApiRef", "kind": "Variable" },
Expand Down
5 changes: 2 additions & 3 deletions scripts/x-data-grid-pro.exports.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
{ "name": "ColumnsPanelPropsOverrides", "kind": "Interface" },
{ "name": "COMFORTABLE_DENSITY_FACTOR", "kind": "Variable" },
{ "name": "COMPACT_DENSITY_FACTOR", "kind": "Variable" },
{ "name": "createUseGridApiEventHandler", "kind": "Function" },
{ "name": "CursorCoordinates", "kind": "Interface" },
{ "name": "DATA_GRID_PRO_PROPS_DEFAULT_VALUES", "kind": "Variable" },
{ "name": "DataGrid", "kind": "Function" },
Expand Down Expand Up @@ -621,10 +620,10 @@
{ "name": "selectedIdsLookupSelector", "kind": "Variable" },
{ "name": "SkeletonCellPropsOverrides", "kind": "Interface" },
{ "name": "ToolbarPropsOverrides", "kind": "Interface" },
{ "name": "unstable_resetCleanupTracking", "kind": "Variable" },
{ "name": "unstable_resetCleanupTracking", "kind": "Function" },
{ "name": "useFirstRender", "kind": "Variable" },
{ "name": "useGridApiContext", "kind": "Variable" },
{ "name": "useGridApiEventHandler", "kind": "Variable" },
{ "name": "useGridApiEventHandler", "kind": "Function" },
{ "name": "useGridApiMethod", "kind": "Function" },
{ "name": "useGridApiOptionHandler", "kind": "Function" },
{ "name": "useGridApiRef", "kind": "Variable" },
Expand Down
Loading