Skip to content

Commit b5265c6

Browse files
fix: Next.js hydration throws a class mismatch error (DevExpress#28855)
1 parent 27efc5d commit b5265c6

File tree

4 files changed

+33
-7
lines changed

4 files changed

+33
-7
lines changed

packages/devextreme/js/__internal/core/m_devices.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EventsStrategy } from '@js/core/events_strategy';
44
import $ from '@js/core/renderer';
55
import type { Callback } from '@js/core/utils/callbacks';
66
import Callbacks from '@js/core/utils/callbacks';
7+
import { when } from '@js/core/utils/deferred';
78
import { extend } from '@js/core/utils/extend';
89
import readyCallbacks from '@js/core/utils/ready_callbacks';
910
import resizeCallbacks from '@js/core/utils/resize_callbacks';
@@ -12,6 +13,7 @@ import { sessionStorage as SessionStorage } from '@js/core/utils/storage';
1213
import { isPlainObject } from '@js/core/utils/type';
1314
import { changeCallback, value as viewPort } from '@js/core/utils/view_port';
1415
import { getNavigator, getWindow, hasWindow } from '@js/core/utils/window';
16+
import { uiLayerInitialized } from '@ts/core/utils/m_common';
1517

1618
export interface Device {
1719
android?: boolean;
@@ -387,14 +389,16 @@ class Devices {
387389

388390
const devices = new Devices();
389391

390-
const viewPortElement = viewPort();
391-
if (viewPortElement) {
392-
devices.attachCssClasses(viewPortElement);
393-
}
392+
when(uiLayerInitialized).done(() => {
393+
const viewPortElement = viewPort();
394+
if (viewPortElement) {
395+
devices.attachCssClasses(viewPortElement);
396+
}
394397

395-
changeCallback.add((viewPort, prevViewport) => {
396-
devices.detachCssClasses(prevViewport);
397-
devices.attachCssClasses(viewPort);
398+
changeCallback.add((viewPort, prevViewport) => {
399+
devices.detachCssClasses(prevViewport);
400+
devices.attachCssClasses(viewPort);
401+
});
398402
});
399403

400404
/// #DEBUG

packages/devextreme/js/__internal/core/utils/m_common.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
isDefined, isFunction, isObject, isString, type,
88
} from '@js/core/utils/type';
99

10+
// @ts-expect-error new deferred
11+
export const uiLayerInitialized = new Deferred();
12+
1013
export const ensureDefined = function (value, defaultValue) {
1114
return isDefined(value) ? value : defaultValue;
1215
};

packages/devextreme/js/__internal/core/widget/dom_component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { isDefined, isFunction, isString } from '@js/core/utils/type';
1717
import { hasWindow } from '@js/core/utils/window';
1818
import license, { peekValidationPerformed } from '@ts/core/license/license_validation';
1919
import TemplateManagerModule from '@ts/core/m_template_manager';
20+
import { uiLayerInitialized } from '@ts/core/utils/m_common';
2021

2122
import { Component } from './component';
2223
import type { OptionChanged } from './types';
@@ -98,6 +99,8 @@ class DOMComponent<
9899
if (!validationAlreadyPerformed && peekValidationPerformed()) {
99100
config({ licenseKey: '' });
100101
}
102+
103+
uiLayerInitialized.resolve();
101104
}
102105

103106
_createElement(element: Element): void {

packages/devextreme/testing/tests/DevExpress.core/devices.tests.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import '../../helpers/includeThemesLinks.js';
2+
import 'ui/button';
23

34
import $ from 'jquery';
45
import domAdapter from '__internal/core/m_dom_adapter';
@@ -220,6 +221,20 @@ QUnit.test('attach css classes', function(assert) {
220221
}
221222
});
222223

224+
QUnit.test('attach css classes side effect', function(assert) {
225+
const attachCssClassesSpy = sinon.spy(devices, 'attachCssClasses');
226+
227+
assert.ok(!viewPort.value() || !viewPort.value()[0].className.includes('dx-device-'));
228+
assert.strictEqual(attachCssClassesSpy.callCount, 0);
229+
230+
$('<div>').dxButton({
231+
text: 'Foo',
232+
});
233+
234+
assert.strictEqual(attachCssClassesSpy.callCount, 1);
235+
assert.ok(viewPort.value()[0].className.includes('dx-device-'));
236+
});
237+
223238
QUnit.test('attach css classes (dx-device-mobile)', function(assert) {
224239
const originalCurrentDevice = devices.current();
225240

@@ -293,6 +308,7 @@ QUnit.test('move classes from previous viewport to new viewport', function(asser
293308
devices.real({ platform: 'ios', version: [7, 1] });
294309
devices.attachCssClasses($element);
295310

311+
$('<div>').dxButton({ text: 'Foo' });
296312
const $newElement = $('<div>');
297313

298314
viewPortChanged.fire($newElement, $element);

0 commit comments

Comments
 (0)