Skip to content

Commit 3e834de

Browse files
authored
fix: Dehydration of class components (#1535)
Fixes #1534
1 parent bd08e1f commit 3e834de

3 files changed

Lines changed: 56 additions & 6 deletions

File tree

packages/dashboard/src/DashboardLayout.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import PanelEvent from './PanelEvent';
2828
import { GLPropTypes, useListener } from './layout';
2929
import { getDashboardData, updateDashboardData } from './redux';
3030
import {
31+
isWrappedComponent,
3132
PanelComponentType,
3233
PanelDehydrateFunction,
3334
PanelHydrateFunction,
@@ -118,24 +119,49 @@ export function DashboardLayout({
118119
componentDehydrate
119120
);
120121

121-
function wrappedComponent(props: PanelProps): JSX.Element {
122-
const CType = componentType;
122+
function wrappedComponent(
123+
props: PanelProps,
124+
ref: React.Ref<unknown>
125+
): JSX.Element {
126+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
127+
const CType = componentType as any;
123128
const PanelWrapperType = panelWrapper;
124129

130+
/*
131+
Checking for class components will let us silence the React warning
132+
about assigning refs to function components not using forwardRef.
133+
The ref is used to detect changes to class component state so we
134+
can track changes to panelState. We should opt for more explicit
135+
state changes in the future and in functional components.
136+
*/
137+
const isClassComponent =
138+
(isWrappedComponent(CType) &&
139+
CType.WrappedComponent.prototype != null &&
140+
CType.WrappedComponent.prototype.isReactComponent != null) ||
141+
(CType.prototype != null && CType.prototype.isReactComponent != null);
142+
125143
// Props supplied by GoldenLayout
126144
const { glContainer, glEventHub } = props;
127145
return (
128146
<PanelErrorBoundary glContainer={glContainer} glEventHub={glEventHub}>
129147
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
130148
<PanelWrapperType {...props}>
131-
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
132-
<CType {...props} />
149+
{isClassComponent ? (
150+
// eslint-disable-next-line react/jsx-props-no-spreading
151+
<CType {...props} ref={ref} />
152+
) : (
153+
// eslint-disable-next-line react/jsx-props-no-spreading
154+
<CType {...props} />
155+
)}
133156
</PanelWrapperType>
134157
</PanelErrorBoundary>
135158
);
136159
}
137160

138-
const cleanup = layout.registerComponent(name, wrappedComponent);
161+
const cleanup = layout.registerComponent(
162+
name,
163+
React.forwardRef(wrappedComponent)
164+
);
139165
hydrateMap.set(name, componentHydrate);
140166
dehydrateMap.set(name, componentDehydrate);
141167
return cleanup;

packages/golden-layout/src/utils/ReactComponentHandler.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,17 @@ export default class ReactComponentHandler {
164164
var defaultProps = {
165165
glEventHub: this._container.layoutManager.eventHub,
166166
glContainer: this._container,
167+
/**
168+
* This ref assignment makes use of callback refs which is a legacy ref style in React.
169+
* It uses the callback to inject GoldenLayout _onUpdate into the React componentWillUpdate lifecycle.
170+
* This allows GoldenLayout to track the internal state of class components.
171+
* We then emit this state change and somewhere furter up, serialize it.
172+
* Specifically we look for state.panelState changes.
173+
* We should not do this going forward as it's quite unclear where/why your component's state might be saved.
174+
* This ref cannot be removed unless we refactor all existing panels to not rely on the magic of panelState.
175+
* DashboardUtils#dehydrate is where the panelState gets read/saved.
176+
*/
177+
ref: this._gotReactComponent.bind(this),
167178
};
168179
var props = $.extend(defaultProps, this._container._config.props);
169180
return React.createElement(this._reactClass, props);

tests/notebook.spec.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
22
import shortid from 'shortid';
33
import { pasteInMonaco } from './utils';
44

5-
test('test creating a file, saving it, closing it, re-opening it, running it, then deleting it', async ({
5+
test('test creating a file, saving it, reloading the page, closing it, re-opening it, running it, then deleting it', async ({
66
page,
77
}) => {
88
await page.goto('');
@@ -33,6 +33,19 @@ test('test creating a file, saving it, closing it, re-opening it, running it, th
3333
// Click button:has-text("Save")
3434
await page.locator('button:has-text("Save")').click();
3535

36+
// Pause so the save can actually finish
37+
// We eagerly show the save status, so can't use that to detect save finish
38+
await new Promise(resolve => {
39+
setTimeout(resolve, 2000);
40+
});
41+
42+
// Reload page to check state of panel is saved
43+
await page.reload();
44+
45+
await expect(
46+
page.locator('.editor-container').locator('textarea')
47+
).not.toBeEmpty();
48+
3649
// Click close on the notebook file .lm_close_tab
3750
await page.locator('.lm_close_tab').click();
3851

0 commit comments

Comments
 (0)