Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable max-classes-per-file */
import { cleanup, render } from '@testing-library/react';
import { cleanup, render, fireEvent } from '@testing-library/react';
import * as React from 'react';
import { memo } from 'react';
import { act } from 'react-dom/test-utils';
Expand Down Expand Up @@ -188,6 +188,80 @@ describe('option update', () => {
expect(Widget.option.mock.calls[1][1]).toEqual(ref.current?.instance().element());
});

it('keeps component ref defined when controlled selection triggers optionChanged', () => {
const ref = React.createRef<TestComponentRef>();
const clickInstances: Array<ReturnType<TestComponentRef['instance']> | undefined> = [];
const optionChangedInstances: Array<ReturnType<TestComponentRef['instance']> | undefined> = [];

const SelectionScenario = () => {
const [selectedRowKeys, setSelectedRowKeys] = React.useState<number[]>([]);

const handleClick = React.useCallback(() => {
clickInstances.push(ref.current?.instance());
setSelectedRowKeys((prev) => [...prev, prev.length + 1]);
}, []);

const handleOptionChanged = React.useCallback((e: { fullName?: string }) => {
if (e.fullName === 'selectedRowKeys') {
optionChangedInstances.push(ref.current?.instance());
}
}, []);

return (
<>
<button type="button" onClick={handleClick}>Test</button>
<TestComponent
ref={ref}
selectedRowKeys={selectedRowKeys}
selection={{ mode: 'multiple' }}
onOptionChanged={handleOptionChanged}
independentEvents={['onOptionChanged']}
/>
</>
);
};

const { getByText } = render(<SelectionScenario />);

let currentOnOptionChanged = WidgetClass.mock.calls[0][1].onOptionChanged;

expect(typeof currentOnOptionChanged).toBe('function');

Widget.option.mockImplementation((name: string, value: unknown) => {
if (name === 'integrationOptions.useDeferUpdateForTemplates') {
return false;
}

if (name === 'onOptionChanged') {
currentOnOptionChanged = value as typeof currentOnOptionChanged;
return undefined;
}

if (name === 'selectedRowKeys' && typeof currentOnOptionChanged === 'function') {
currentOnOptionChanged({
component: Widget,
element: undefined,
fullName: name,
model: undefined,
name,
previousValue: undefined,
value,
});
}

return undefined;
});

act(() => {
fireEvent.click(getByText('Test'));
});

expect(clickInstances).toHaveLength(1);
expect(clickInstances[0]).toBeTruthy();
expect(optionChangedInstances).toHaveLength(1);
expect(optionChangedInstances[0]).toBeTruthy();
});

it('updates nested collection item', () => {
const TestContainer = (props: any) => {
const { value } = props;
Expand Down
22 changes: 17 additions & 5 deletions packages/devextreme-react/src/core/__tests__/test-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useCallback,
useContext,
useRef,
useLayoutEffect,
forwardRef,
ReactElement,
} from 'react';
Expand Down Expand Up @@ -65,20 +66,26 @@ const TestComponent = memo(forwardRef<TestComponentRef, any>(function TestCompon
Widget.resetOption.mockReset();
}, []);

const propsRef = useRef(props);

useLayoutEffect(() => {
propsRef.current = props;
}, [props]);

useImperativeHandle(ref, () => {
return {
instance() {
return {
element() {
return getElement();
}
}
};
},
getProps() {
return props;
return propsRef.current;
},
};
}, [componentRef.current, getElement, props]);
}, []);

return (
<Component<P & IHtmlOptions>
Expand All @@ -104,12 +111,17 @@ const TestPortalComponent = memo(forwardRef<TestComponentRef, any>(function Test

const TestRestoreTreeComponent = forwardRef((_, ref: React.ForwardedRef<{ restoreTree?: () => void }>) => {
const restoreParentLink = useContext(RestoreTreeContext);
const restoreParentLinkRef = useRef<() => void>(() => {});

useLayoutEffect(() => {
restoreParentLinkRef.current = restoreParentLink ?? (() => {});
}, [restoreParentLink]);

useImperativeHandle(ref, () => {
return {
restoreTree: restoreParentLink
restoreTree: () => restoreParentLinkRef.current(),
};
}, [restoreParentLink]);
}, []);

return <div>Context Component</div>;
});
Expand Down
71 changes: 24 additions & 47 deletions packages/devextreme-react/src/core/component-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
const [, setForceUpdateToken] = useState(Symbol('initial force update token'));
const removalLocker = useContext(RemovalLockerContext);
const restoreParentLink = useContext(RestoreTreeContext);
const restoreParentLinkRef = useRef(restoreParentLink);
const instance = useRef<any>();
const element = useRef<HTMLDivElement>();
const portalContainer = useRef<HTMLElement | null>();
Expand Down Expand Up @@ -127,15 +128,10 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
childElementsDetached.current = false;
}

if (restoreParentLink && element.current && !element.current.isConnected) {
restoreParentLink();
if (restoreParentLinkRef.current && element.current && !element.current.isConnected) {
restoreParentLinkRef.current();
}
}, [
childNodes.current,
element.current,
childElementsDetached.current,
restoreParentLink,
]);
}, []);

// eslint-disable-next-line @stylistic/max-len
const updateCssClasses = useCallback((prevProps: (P & ComponentBaseProps) | undefined, newProps: P & ComponentBaseProps) => {
Expand All @@ -157,7 +153,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
element.current?.classList.add(...classNames);
}
}
}, [element.current]);
}, []);

const setInlineStyles = useCallback((styles) => {
if (element.current) {
Expand All @@ -173,7 +169,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
},
);
}
}, [element.current]);
}, []);

const setTemplateManagerHooks = useCallback(({
createDXTemplates: createDXTemplatesFn,
Expand All @@ -183,11 +179,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
createDXTemplates.current = createDXTemplatesFn;
clearInstantiationModels.current = clearInstantiationModelsFn;
updateTemplates.current = updateTemplatesFn;
}, [
createDXTemplates.current,
clearInstantiationModels.current,
updateTemplates.current,
]);
}, []);

const getElementProps = useCallback(() => {
const elementProps: Record<string, any> = {
Expand All @@ -204,7 +196,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
}
});
return elementProps;
}, [element.current, props]);
}, [props]);

const scheduleTemplatesUpdate = useCallback(() => {
if (guardsUpdateScheduled.current) {
Expand All @@ -223,11 +215,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
});

unscheduleGuards();
}, [
guardsUpdateScheduled.current,
useDeferUpdateForTemplates.current,
updateTemplates.current,
]);
}, []);

const createWidget = useCallback((el?: Element) => {
beforeCreateWidget();
Expand Down Expand Up @@ -270,14 +258,8 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
}, [
beforeCreateWidget,
afterCreateWidget,
element.current,
optionsManager.current,
createDXTemplates.current,
clearInstantiationModels.current,
WidgetClass,
useRequestAnimationFrameFlag,
useDeferUpdateForTemplates.current,
instance.current,
subscribableOptions,
independentEvents,
widgetConfig,
Expand All @@ -288,7 +270,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
instance.current.focus();
shouldRestoreFocus.current = false;
}
}, [shouldRestoreFocus.current, instance.current]);
}, []);

const onComponentUpdated = useCallback(() => {
if (!optionsManager.current?.isInstanceSet) {
Expand All @@ -305,9 +287,6 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(

prevPropsRef.current = props;
}, [
optionsManager.current,
prevPropsRef.current,
createDXTemplates.current,
scheduleTemplatesUpdate,
updateCssClasses,
props,
Expand All @@ -330,9 +309,6 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(

prevPropsRef.current = props;
}, [
childNodes.current,
element.current,
childElementsDetached.current,
updateCssClasses,
setInlineStyles,
props,
Expand Down Expand Up @@ -362,15 +338,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
optionsManager.current.dispose();

removalLocker?.unlock();
}, [
removalLocker,
instance.current,
childNodes.current,
element.current,
optionsManager.current,
childElementsDetached.current,
shouldRestoreFocus.current,
]);
}, [removalLocker]);

useLayoutEffect(() => {
onComponentMounted();
Expand All @@ -380,10 +348,20 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
};
}, []);

useLayoutEffect(() => {
restoreParentLinkRef.current = restoreParentLink;
}, [restoreParentLink]);

useLayoutEffect(() => {
onComponentUpdated();
});

const createWidgetRef = useRef(createWidget);

useLayoutEffect(() => {
createWidgetRef.current = createWidget;
}, [createWidget]);

useImperativeHandle(ref, () => (
{
getInstance() {
Expand All @@ -393,10 +371,10 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
return element.current;
},
createWidget(el) {
createWidget(el);
createWidgetRef.current?.(el);
},
}
), [instance.current, element.current, createWidget]);
), []);

const _renderChildren = useCallback(() => {
if (renderChildren) {
Expand All @@ -410,7 +388,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
const renderPortal = useCallback(() => portalContainer.current && createPortal(
_renderChildren(),
portalContainer.current,
), [portalContainer.current, _renderChildren]);
), [_renderChildren]);

const renderContent = useCallback(() => {
const { children } = props;
Expand All @@ -429,7 +407,6 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
}, [
props,
isPortalComponent,
portalContainer.current,
_renderChildren,
]);

Expand Down
Loading
Loading