From 9acc13fc123bd4bbd97be547da5624d2c93e40c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Jan 2026 11:14:37 +0100 Subject: [PATCH 01/22] VizPanel: render before activation --- packages/scenes/src/components/VizPanel/VizPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/scenes/src/components/VizPanel/VizPanel.tsx b/packages/scenes/src/components/VizPanel/VizPanel.tsx index 601aae04c..e15056fbb 100644 --- a/packages/scenes/src/components/VizPanel/VizPanel.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanel.tsx @@ -125,6 +125,7 @@ export class VizPanel extends Scene private _prevData?: PanelData; private _dataWithFieldConfig?: PanelData; private _structureRev = 0; + protected _renderBeforeActivation = true; public constructor(state: Partial>) { super({ From d912c9e6037d5eaa445f37be9df200b8d5184ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Jan 2026 11:48:20 +0100 Subject: [PATCH 02/22] Update --- packages/scenes/src/core/sceneGraph/cloneSceneObject.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scenes/src/core/sceneGraph/cloneSceneObject.test.ts b/packages/scenes/src/core/sceneGraph/cloneSceneObject.test.ts index 578bb28ba..d3c23e94e 100644 --- a/packages/scenes/src/core/sceneGraph/cloneSceneObject.test.ts +++ b/packages/scenes/src/core/sceneGraph/cloneSceneObject.test.ts @@ -97,7 +97,7 @@ describe('cloneSceneObject', () => { }); // not sure how slow ci systems are so just comparing against plain clone of just the object*3 - expect(sceneCloneTime).toBeLessThan(plainCloneTime * 3); + expect(sceneCloneTime).toBeLessThan(plainCloneTime * 4); }); }); From e9ba57f12ad136c4754ac1a020b9080af4016d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Feb 2026 17:07:13 +0100 Subject: [PATCH 03/22] update --- .../components/VizPanel/VizPanelRenderer.tsx | 142 +++++++++--------- 1 file changed, 74 insertions(+), 68 deletions(-) diff --git a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx index ac2028082..13b6feb4b 100644 --- a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx @@ -38,7 +38,7 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { collapsed, _renderCounter = 0, } = model.useState(); - const [ref, { width, height }] = useMeasure(); + let [ref, { width, height }] = useMeasure(); const appEvents = useMemo(() => getAppEvents(), []); const setPanelAttention = useCallback(() => { @@ -230,76 +230,82 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { const context = model.getPanelContext(); const panelId = model.getLegacyPanelId(); + if (width === 0) { + width = 100; + } + + if (height === 0) { + height = 100; + } + return (
} className={absoluteWrapper} data-viz-panel-key={model.state.key}> - {width > 0 && height > 0 && ( - 0 ? titleItemsElement : undefined} - dragClass={dragClass} - actions={actionsElement} - dragClassCancel={dragClassCancel} - padding={plugin.noPadding ? 'none' : 'md'} - menu={panelMenu} - onCancelQuery={model.onCancelQuery} - onFocus={setPanelAttention} - onMouseEnter={setPanelAttention} - onMouseMove={debouncedMouseMove} - // @ts-expect-error remove this on next grafana/ui update - subHeaderContent={subHeaderElement.length ? subHeaderElement : undefined} - onDragStart={(e: React.PointerEvent) => { - dragHooks.onDragStart?.(e, model); - }} - showMenuAlways={showMenuAlways} - {...(collapsible - ? { - collapsible: Boolean(collapsible), - collapsed, - onToggleCollapse: model.onToggleCollapse, - } - : { hoverHeader, hoverHeaderOffset })} - > - {(innerWidth, innerHeight) => ( - <> - - - - {isReadyToRender && ( - - )} - - - - - )} - - )} + 0 ? titleItemsElement : undefined} + dragClass={dragClass} + actions={actionsElement} + dragClassCancel={dragClassCancel} + padding={plugin.noPadding ? 'none' : 'md'} + menu={panelMenu} + onCancelQuery={model.onCancelQuery} + onFocus={setPanelAttention} + onMouseEnter={setPanelAttention} + onMouseMove={debouncedMouseMove} + // @ts-expect-error remove this on next grafana/ui update + subHeaderContent={subHeaderElement.length ? subHeaderElement : undefined} + onDragStart={(e: React.PointerEvent) => { + dragHooks.onDragStart?.(e, model); + }} + showMenuAlways={showMenuAlways} + {...(collapsible + ? { + collapsible: Boolean(collapsible), + collapsed, + onToggleCollapse: model.onToggleCollapse, + } + : { hoverHeader, hoverHeaderOffset })} + > + {(innerWidth, innerHeight) => ( + <> + + + + {isReadyToRender && ( + + )} + + + + + )} +
); From 3bad4d368f0e93e570e6acb8b3f829890eaac907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2026 10:48:01 +0100 Subject: [PATCH 04/22] progress --- packages/scenes-app/src/demos/index.ts | 7 ++ .../components/VizPanel/VizPanelRenderer.tsx | 96 +++++++++++++------ packages/scenes/src/core/SceneObjectBase.tsx | 2 +- 3 files changed, 77 insertions(+), 28 deletions(-) diff --git a/packages/scenes-app/src/demos/index.ts b/packages/scenes-app/src/demos/index.ts index 2170c9734..bb733298d 100644 --- a/packages/scenes-app/src/demos/index.ts +++ b/packages/scenes-app/src/demos/index.ts @@ -43,6 +43,7 @@ import { getSceneGraphEventsDemo } from './sceneGraphEvents'; import { getSeriesLimitTest } from './seriesLimit'; import { getScopesDemo } from './scopesDemo'; import { getVariableWithObjectValuesDemo } from './variableWithObjectValuesDemo'; +import { getFlickeringDemo } from './flickeringDemo'; export interface DemoDescriptor { title: string; @@ -319,5 +320,11 @@ export function getDemos(): DemoDescriptor[] { getPage: getVariableWithObjectValuesDemo, getSourceCodeModule: () => import('!!raw-loader!../demos/variableWithObjectValuesDemo.tsx'), }, + { + title: 'Flickering demo', + description: 'Demo showing flickering panels', + getPage: getFlickeringDemo, + getSourceCodeModule: () => import('!!raw-loader!../demos/flickeringDemo.tsx'), + }, ].sort((a, b) => a.title.localeCompare(b.title)); } diff --git a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx index 13b6feb4b..92496f4ba 100644 --- a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx @@ -1,9 +1,18 @@ import { Trans } from '@grafana/i18n'; -import React, { RefCallback, useCallback, useEffect, useLayoutEffect, useMemo } from 'react'; +import React, { memo, RefCallback, useCallback, useEffect, useLayoutEffect, useMemo } from 'react'; import { useMeasure } from 'react-use'; // @ts-ignore -import { AlertState, GrafanaTheme2, PanelData, PluginContextProvider, SetPanelAttentionEvent } from '@grafana/data'; +import { + AlertState, + GrafanaTheme2, + PanelData, + PanelPlugin, + PanelProps, + PluginContextProvider, + PluginType, + SetPanelAttentionEvent, +} from '@grafana/data'; import { getAppEvents } from '@grafana/runtime'; import { PanelChrome, ErrorBoundaryAlert, PanelContextProvider, Tooltip, useStyles2, Icon } from '@grafana/ui'; @@ -78,7 +87,7 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { } }); - const plugin = model.getPlugin(); + const plugin = model.getPlugin() ?? getLoadingPlugin(); const { dragClass, dragClassCancel } = getDragClasses(model); const dragHooks = getDragHooks(model); @@ -103,16 +112,6 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { const titleInterpolated = model.interpolate(title, undefined, 'text'); const alertStateStyles = useStyles2(getAlertStateStyles); - if (!plugin) { - return ( -
- - Loading plugin panel... - -
- ); - } - if (!plugin.panel) { return (
@@ -230,14 +229,6 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { const context = model.getPanelContext(); const panelId = model.getLegacyPanelId(); - if (width === 0) { - width = 100; - } - - if (height === 0) { - height = 100; - } - return (
} className={absoluteWrapper} data-viz-panel-key={model.state.key}> @@ -247,8 +238,8 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { loadingState={data.state} statusMessage={getChromeStatusMessage(data, _pluginLoadError)} statusMessageOnClick={model.onStatusMessageClick} - width={width} - height={height} + width={width === 0 ? undefined : width} + height={height === 0 ? undefined : height} selectionId={model.state.key} displayMode={displayMode} titleItems={titleItemsElement.length > 0 ? titleItemsElement : undefined} @@ -275,8 +266,12 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { } : { hoverHeader, hoverHeaderOffset })} > - {(innerWidth, innerHeight) => ( - <> + {(innerWidth, innerHeight) => { + if (innerWidth === 0 || innerHeight === 0) { + return null; + } + + return ( @@ -303,8 +298,8 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { - - )} + ); + }}
@@ -406,3 +401,50 @@ const getAlertStateStyles = (theme: GrafanaTheme2) => { }), }; }; + +let loadingPluginInstance: PanelPlugin | null = null; + +export function getLoadingPlugin(): PanelPlugin { + if (loadingPluginInstance) { + return loadingPluginInstance; + } + + const LoadingPluginComp = memo(() => { + return ( +
+ + Loading plugin panel... + +
+ ); + }); + + LoadingPluginComp.displayName = 'LoadingPlugin'; + + loadingPluginInstance = new PanelPlugin(LoadingPluginComp); + + loadingPluginInstance.meta = { + id: 'loading-plugin', + name: 'Loading Plugin', + sort: 100, + type: PluginType.panel, + module: '', + baseUrl: '', + info: { + author: { + name: '', + }, + description: '', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '', + version: '', + }, + }; + + return loadingPluginInstance; +} diff --git a/packages/scenes/src/core/SceneObjectBase.tsx b/packages/scenes/src/core/SceneObjectBase.tsx index 6e4c5cc69..6b46ea911 100644 --- a/packages/scenes/src/core/SceneObjectBase.tsx +++ b/packages/scenes/src/core/SceneObjectBase.tsx @@ -35,7 +35,7 @@ export abstract class SceneObjectBase Date: Thu, 19 Feb 2026 14:26:07 +0100 Subject: [PATCH 05/22] progress --- packages/scenes-app/src/components/Routes/Routes.tsx | 10 +++++++++- packages/scenes/src/components/VizPanel/VizPanel.tsx | 1 - packages/scenes/src/components/layout/LazyLoader.tsx | 12 +++++++----- .../layout/grid/SceneGridLayoutRenderer.tsx | 1 + packages/scenes/src/core/SceneObjectBase.tsx | 4 +++- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/scenes-app/src/components/Routes/Routes.tsx b/packages/scenes-app/src/components/Routes/Routes.tsx index fec7205d3..b9daebafb 100644 --- a/packages/scenes-app/src/components/Routes/Routes.tsx +++ b/packages/scenes-app/src/components/Routes/Routes.tsx @@ -1,12 +1,20 @@ import * as React from 'react'; -import { Route, Routes } from 'react-router-dom'; +import { Route, Routes, useLocation } from 'react-router-dom'; import { ROUTES } from '../../constants'; import { DemoListPage } from '../../pages/DemoListPage'; import GrafanaMonitoringApp from '../../monitoring-app/GrafanaMonitoringApp'; import { ReactDemoPage } from '../../react-demo/Home'; import { HomePage } from '../../home-demo/HomeApp'; +import { SceneObjectBase } from '@grafana/scenes'; export function AppRoutes() { + const location = useLocation(); + const params = new URLSearchParams(location.search); + + if (params.get('renderBeforeActivation') === 'true') { + SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true; + } + return ( diff --git a/packages/scenes/src/components/VizPanel/VizPanel.tsx b/packages/scenes/src/components/VizPanel/VizPanel.tsx index e15056fbb..601aae04c 100644 --- a/packages/scenes/src/components/VizPanel/VizPanel.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanel.tsx @@ -125,7 +125,6 @@ export class VizPanel extends Scene private _prevData?: PanelData; private _dataWithFieldConfig?: PanelData; private _structureRev = 0; - protected _renderBeforeActivation = true; public constructor(state: Partial>) { super({ diff --git a/packages/scenes/src/components/layout/LazyLoader.tsx b/packages/scenes/src/components/layout/LazyLoader.tsx index da251f6fa..c916cb07f 100644 --- a/packages/scenes/src/components/layout/LazyLoader.tsx +++ b/packages/scenes/src/components/layout/LazyLoader.tsx @@ -17,6 +17,12 @@ export interface Props extends Omit, 'onChange' key: string; onLoad?: () => void; onChange?: (isInView: boolean) => void; + /** + * If true will render children on first render/mount even if it out of view + * But will LazyLoaderInViewContext will be false on first render + * This can reduce flickering / initial empty div on first render + */ + onlySetIsInView?: boolean; } export interface LazyLoaderType extends ForwardRefExoticComponent { @@ -67,11 +73,7 @@ export const LazyLoader: LazyLoaderType = React.forwardRef
); } diff --git a/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx b/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx index 582034867..a4862e754 100644 --- a/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx +++ b/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx @@ -119,6 +119,7 @@ const GridItemWrapper = React.forwardRef(( className={cx(className, props.className)} style={style} ref={ref} + onlySetIsInView={true} > {innerContent} {children} diff --git a/packages/scenes/src/core/SceneObjectBase.tsx b/packages/scenes/src/core/SceneObjectBase.tsx index 6b46ea911..a5c02da9f 100644 --- a/packages/scenes/src/core/SceneObjectBase.tsx +++ b/packages/scenes/src/core/SceneObjectBase.tsx @@ -25,6 +25,8 @@ import { SceneObjectRef } from './SceneObjectRef'; export abstract class SceneObjectBase implements SceneObject { + public static RENDER_BEFORE_ACTIVATION_DEFAULT = false; + private _isActive = false; private _state: TState; private _activationHandlers: SceneActivationHandler[] = []; @@ -35,7 +37,7 @@ export abstract class SceneObjectBase Date: Fri, 20 Feb 2026 06:40:31 +0100 Subject: [PATCH 06/22] Update --- .../src/core/SceneComponentWrapper.test.tsx | 51 ++++++++++++++++--- packages/scenes/src/core/SceneObjectBase.tsx | 2 +- .../variables/sets/SceneVariableSet.test.tsx | 12 ++--- .../src/variables/sets/SceneVariableSet.ts | 5 ++ 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/scenes/src/core/SceneComponentWrapper.test.tsx b/packages/scenes/src/core/SceneComponentWrapper.test.tsx index 15f029719..7dd18c60e 100644 --- a/packages/scenes/src/core/SceneComponentWrapper.test.tsx +++ b/packages/scenes/src/core/SceneComponentWrapper.test.tsx @@ -12,16 +12,17 @@ export interface TestSceneState extends SceneObjectState { export class TestScene extends SceneObjectBase { public renderCount = 0; - - public setRenderBeforeActivation(value: boolean) { - this._renderBeforeActivation = value; - } + public wasRenderedWhileInactive?: boolean; public static Component = ({ model }: SceneComponentProps) => { const { name } = model.useState(); model.renderCount += 1; + if (!model.isActive) { + model.wasRenderedWhileInactive = true; + } + return (
name: {name}
@@ -31,7 +32,11 @@ export class TestScene extends SceneObjectBase { }; } -describe('SceneComponentWrapper', () => { +describe('SceneComponentWrapper no render before activiation', () => { + beforeAll(() => { + SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = false; + }); + it('Should render should activate object', () => { const scene = new TestScene({ name: 'nested' }); render(); @@ -58,13 +63,43 @@ describe('SceneComponentWrapper', () => { expect(scene.renderCount).toBe(1); expect(screen.getByText('isActive: true')).toBeInTheDocument(); }); +}); - it('should render component before activation whgen renderBeforeActivation is true', () => { - const scene = new TestScene({ name: 'nested' }); - scene.setRenderBeforeActivation(true); +describe('SceneComponentWrapper render before activiation', () => { + beforeAll(() => { + SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true; + }); + it('Should render should activate object', () => { + const scene = new TestScene({ name: 'nested' }); render(); expect(scene.renderCount).toBe(2); + expect(scene.isActive).toBe(true); + }); + + it('Unmount should deactivate', () => { + const scene = new TestScene({ name: 'nested' }); + const { unmount } = render(); + + expect(scene.isActive).toBe(true); + + unmount(); + + expect(scene.isActive).toBe(false); + }); + + it('should render component before activation', () => { + const scene = new TestScene({ name: 'nested' }); + const screen = render(); + + expect(scene.wasRenderedWhileInactive).toBe(true); + expect(screen.getByText('isActive: true')).toBeInTheDocument(); + }); + + it('should render component before activation whgen renderBeforeActivation is true', () => { + const scene = new TestScene({ name: 'nested' }); + render(); + expect(scene.renderCount).toBe(2); }); }); diff --git a/packages/scenes/src/core/SceneObjectBase.tsx b/packages/scenes/src/core/SceneObjectBase.tsx index a5c02da9f..f1a1ee246 100644 --- a/packages/scenes/src/core/SceneObjectBase.tsx +++ b/packages/scenes/src/core/SceneObjectBase.tsx @@ -25,7 +25,7 @@ import { SceneObjectRef } from './SceneObjectRef'; export abstract class SceneObjectBase implements SceneObject { - public static RENDER_BEFORE_ACTIVATION_DEFAULT = false; + public static RENDER_BEFORE_ACTIVATION_DEFAULT = true; private _isActive = false; private _state: TState; diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx b/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx index 9c3b26ed0..132263c94 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx +++ b/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx @@ -196,16 +196,16 @@ describe('SceneVariableList', () => { }); expect(screen.getByText('AA - AAA')).toBeInTheDocument(); - expect((helloText as any).renderCount).toBe(1); - expect((sceneObjectWithVariable as any).renderCount).toBe(2); + expect((helloText as any).renderCount).toBe(2); + expect((sceneObjectWithVariable as any).renderCount).toBe(3); act(() => { B.changeValueTo('B'); }); expect(screen.getByText('AA - B')).toBeInTheDocument(); - expect((helloText as any).renderCount).toBe(1); - expect((sceneObjectWithVariable as any).renderCount).toBe(3); + expect((helloText as any).renderCount).toBe(2); + expect((sceneObjectWithVariable as any).renderCount).toBe(4); }); }); }); @@ -647,7 +647,7 @@ describe('SceneVariableList', () => { expect(innerSet.isVariableLoadingOrWaitingToUpdate(scopedA)).toBe(false); }); - it('Should ignore isActivate state', async () => { + it('Should check isActivate state', async () => { const A = new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }); const set = new SceneVariableSet({ variables: [A] }); const deactivate = set.activate(); @@ -658,7 +658,7 @@ describe('SceneVariableList', () => { deactivate(); - expect(set.isVariableLoadingOrWaitingToUpdate(A)).toBe(false); + expect(set.isVariableLoadingOrWaitingToUpdate(A)).toBe(true); }); }); diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.ts b/packages/scenes/src/variables/sets/SceneVariableSet.ts index df74ed0c8..b9e9207a3 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.ts +++ b/packages/scenes/src/variables/sets/SceneVariableSet.ts @@ -352,6 +352,11 @@ export class SceneVariableSet extends SceneObjectBase imp * For example if C depends on variable B which depends on variable A and A is loading this returns true for variable C and B. */ public isVariableLoadingOrWaitingToUpdate(variable: SceneVariable) { + // If we are not active yet we have not initialized variables so we should treat them as loading + if (!this.isActive) { + return true; + } + if (variable.state.loading) { return true; } From 7bf26fec7a09114d02be2d928f22f6066495f650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2026 08:29:13 +0100 Subject: [PATCH 07/22] Better demo --- .../src/components/RefreshPicker.tsx | 3 +- .../src/components/SceneFlexLayout.tsx | 3 +- .../scenes-react/src/components/VizPanel.tsx | 3 +- .../src/contexts/SceneContextObject.tsx | 30 +++++++++++++++++++ .../src/hooks/useDataTransformer.ts | 3 +- .../scenes-react/src/hooks/useSceneObject.ts | 3 +- packages/scenes/src/core/SceneObjectBase.tsx | 2 +- 7 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/scenes-react/src/components/RefreshPicker.tsx b/packages/scenes-react/src/components/RefreshPicker.tsx index fad3f2274..7054a9fbc 100644 --- a/packages/scenes-react/src/components/RefreshPicker.tsx +++ b/packages/scenes-react/src/components/RefreshPicker.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useId } from 'react'; import { useSceneContext } from '../hooks/hooks'; import { SceneRefreshPicker, SceneRefreshPickerState } from '@grafana/scenes'; import { usePrevious } from 'react-use'; +import { useAddToScene } from '../contexts/SceneContextObject'; export interface Props { refresh?: string; @@ -22,7 +23,7 @@ export function RefreshPicker(props: Props) { }); } - useEffect(() => scene.addToScene(picker), [picker, scene]); + useAddToScene(picker, scene); // Update options useEffect(() => { diff --git a/packages/scenes-react/src/components/SceneFlexLayout.tsx b/packages/scenes-react/src/components/SceneFlexLayout.tsx index f78a5b3c6..e9364f4a3 100644 --- a/packages/scenes-react/src/components/SceneFlexLayout.tsx +++ b/packages/scenes-react/src/components/SceneFlexLayout.tsx @@ -9,6 +9,7 @@ import { import { useSceneContext } from '../hooks/hooks'; import { SceneFlexItem, type SceneFlexItemProps } from './SceneFlexItem'; import { SceneFlexLayoutContext } from './SceneFlexLayoutContext'; +import { useAddToScene } from '../contexts/SceneContextObject'; export interface SceneFlexLayoutProps extends SceneFlexItemPlacement { children: React.ReactNode; @@ -43,7 +44,7 @@ export function SceneFlexLayout(props: SceneFlexLayoutProps) { }); } - useEffect(() => scene.addToScene(layout), [layout, scene]); + useAddToScene(layout, scene); // Keep layout placement props in sync (but do not touch children here). useEffect(() => { diff --git a/packages/scenes-react/src/components/VizPanel.tsx b/packages/scenes-react/src/components/VizPanel.tsx index 21abf5462..23d455d89 100644 --- a/packages/scenes-react/src/components/VizPanel.tsx +++ b/packages/scenes-react/src/components/VizPanel.tsx @@ -13,6 +13,7 @@ import { getPanelOptionsWithDefaults } from '@grafana/data'; import { PanelContext } from '@grafana/ui'; import { writeSceneLog } from '../utils'; import { useSceneContext } from '../hooks/hooks'; +import { useAddToScene } from '../contexts/SceneContextObject'; export interface VizPanelProps { title: string; @@ -81,7 +82,7 @@ export function VizPanel(props: VizPanelProps) { }); } - useEffect(() => scene.addToScene(panel), [panel, scene]); + useAddToScene(panel, scene); // Update options useEffect(() => { diff --git a/packages/scenes-react/src/contexts/SceneContextObject.tsx b/packages/scenes-react/src/contexts/SceneContextObject.tsx index efad0a575..addb614a5 100644 --- a/packages/scenes-react/src/contexts/SceneContextObject.tsx +++ b/packages/scenes-react/src/contexts/SceneContextObject.tsx @@ -7,6 +7,7 @@ import { NewSceneObjectAddedEvent, } from '@grafana/scenes'; import { writeSceneLog } from '../utils'; +import { useEffect } from 'react'; export interface SceneContextObjectState extends SceneObjectState { childContexts?: SceneContextObject[]; @@ -87,3 +88,32 @@ export class SceneContextObject extends SceneObjectBase writeSceneLog('SceneContext', `Remvoing child context: ${ctx.constructor.name} key: ${ctx.state.key}`); } } + +export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) { + // Old behavior + if (!SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT) { + ctx.addToScene(obj); + return; + } + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + const deactivate = obj.activate(); + + return () => { + writeSceneLog('SceneContext', `Removing from scene: ${obj.constructor.name} key: ${obj.state.key}`); + ctx.setState({ children: ctx.state.children.filter((x) => x !== obj) }); + + deactivate(); + }; + }, [ctx, obj]); + + if (ctx.state.children.includes(obj)) { + return; + } + + ctx.publishEvent(new NewSceneObjectAddedEvent(obj), true); + ctx.setState({ children: [...ctx.state.children, obj] }); + + writeSceneLog('SceneContext', `Adding to scene: ${obj.constructor.name} key: ${obj.state.key}`); +} diff --git a/packages/scenes-react/src/hooks/useDataTransformer.ts b/packages/scenes-react/src/hooks/useDataTransformer.ts index 6c1ecadb0..3d367f87e 100644 --- a/packages/scenes-react/src/hooks/useDataTransformer.ts +++ b/packages/scenes-react/src/hooks/useDataTransformer.ts @@ -8,6 +8,7 @@ import { useSceneContext } from './hooks'; import { useEffect, useId } from 'react'; import { isEqual } from 'lodash'; import { DataTransformerConfig } from '@grafana/schema'; +import { useAddToScene } from '../contexts/SceneContextObject'; export interface UseDataTransformerOptions { transformations: Array; @@ -28,7 +29,7 @@ export function useDataTransformer(options: UseDataTransformerOptions) { }); } - useEffect(() => scene.addToScene(dataTransformer), [dataTransformer, scene]); + useAddToScene(dataTransformer, scene); useEffect(() => { if (!isEqual(dataTransformer.state.transformations, options.transformations)) { diff --git a/packages/scenes-react/src/hooks/useSceneObject.ts b/packages/scenes-react/src/hooks/useSceneObject.ts index f44fe0c6a..304ad598f 100644 --- a/packages/scenes-react/src/hooks/useSceneObject.ts +++ b/packages/scenes-react/src/hooks/useSceneObject.ts @@ -2,6 +2,7 @@ import { useEffect, useId } from 'react'; import { SceneObject, sceneGraph } from '@grafana/scenes'; import { useSceneContext } from './hooks'; import { CacheKey, SceneObjectConstructor, getSceneObjectCache } from '../caching/SceneObjectCache'; +import { useAddToScene } from '../contexts/SceneContextObject'; export interface UseSceneObjectProps { factory: (key: string) => T; @@ -44,7 +45,7 @@ export function useSceneObject(options: UseSceneObjectPro } } - useEffect(() => scene.addToScene(obj), [obj, scene]); + useAddToScene(obj, scene); return obj; } diff --git a/packages/scenes/src/core/SceneObjectBase.tsx b/packages/scenes/src/core/SceneObjectBase.tsx index f1a1ee246..a5c02da9f 100644 --- a/packages/scenes/src/core/SceneObjectBase.tsx +++ b/packages/scenes/src/core/SceneObjectBase.tsx @@ -25,7 +25,7 @@ import { SceneObjectRef } from './SceneObjectRef'; export abstract class SceneObjectBase implements SceneObject { - public static RENDER_BEFORE_ACTIVATION_DEFAULT = true; + public static RENDER_BEFORE_ACTIVATION_DEFAULT = false; private _isActive = false; private _state: TState; From 48f1eff975e3b6806ef06aeb54543a4a7f5de7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2026 10:13:57 +0100 Subject: [PATCH 08/22] Fixes --- .../src/contexts/SceneContextObject.tsx | 2 + .../variables/sets/SceneVariableSet.test.tsx | 63 ++++++++++++++++--- .../src/variables/sets/SceneVariableSet.ts | 5 +- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/scenes-react/src/contexts/SceneContextObject.tsx b/packages/scenes-react/src/contexts/SceneContextObject.tsx index addb614a5..13bc7da99 100644 --- a/packages/scenes-react/src/contexts/SceneContextObject.tsx +++ b/packages/scenes-react/src/contexts/SceneContextObject.tsx @@ -112,6 +112,8 @@ export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) { return; } + // This is technically a state change during render. Ee have to add it to the state tree right away in order to render the object on the first pass + // Should be ok as nothing subscribes to SceneContextObject state changes and the NewSceneObjectAddedEvent is syncing url state to obj state ctx.publishEvent(new NewSceneObjectAddedEvent(obj), true); ctx.setState({ children: [...ctx.state.children, obj] }); diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx b/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx index 132263c94..4e4312ef2 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx +++ b/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx @@ -17,6 +17,7 @@ import { LocalValueVariable } from '../variants/LocalValueVariable'; import { TestObjectWithVariableDependency, TestScene } from '../TestScene'; import { activateFullSceneTree } from '../../utils/test/activateFullSceneTree'; import { SceneVariable, SceneVariableState, VariableValue } from '../types'; +import { ObjectVariable } from '../variants/ObjectVariable'; interface SceneTextItemState extends SceneObjectState { text: string; @@ -174,6 +175,45 @@ describe('SceneVariableList', () => { }); describe('When update process completed and variables have changed values', () => { + it('Should trigger re-renders of dependent scene objects', async () => { + const A = new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }); + const B = new TestVariable({ name: 'B', query: 'A.$A.*', value: '', text: '', options: [] }); + + const helloText = new SceneTextItem({ text: 'Hello' }); + const sceneObjectWithVariable = new SceneTextItem({ text: '$A - $B', key: '' }); + + const scene = new SceneFlexLayout({ + $variables: new SceneVariableSet({ variables: [B, A] }), + children: [new SceneFlexItem({ body: helloText }), new SceneFlexItem({ body: sceneObjectWithVariable })], + }); + + render(); + + expect(screen.getByText('Hello')).toBeInTheDocument(); + + act(() => { + A.signalUpdateCompleted(); + B.signalUpdateCompleted(); + }); + + expect(screen.getByText('AA - AAA')).toBeInTheDocument(); + expect((helloText as any).renderCount).toBe(1); + expect((sceneObjectWithVariable as any).renderCount).toBe(2); + + act(() => { + B.changeValueTo('B'); + }); + + expect(screen.getByText('AA - B')).toBeInTheDocument(); + expect((helloText as any).renderCount).toBe(1); + expect((sceneObjectWithVariable as any).renderCount).toBe(3); + }); + }); + + describe('When update process completed and variables have changed values RENDER_BEFORE_ACTIVATION = true', () => { + beforeAll(() => (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true)); + afterAll(() => (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = false)); + it('Should trigger re-renders of dependent scene objects', async () => { const A = new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }); const B = new TestVariable({ name: 'B', query: 'A.$A.*', value: '', text: '', options: [] }); @@ -647,18 +687,23 @@ describe('SceneVariableList', () => { expect(innerSet.isVariableLoadingOrWaitingToUpdate(scopedA)).toBe(false); }); - it('Should check isActivate state', async () => { - const A = new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }); - const set = new SceneVariableSet({ variables: [A] }); - const deactivate = set.activate(); + describe('When RENDER_BEFORE_ACTIVATION = true', () => { + beforeAll(() => (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true)); + afterAll(() => (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = false)); - // Should start variables with no dependencies - expect(A.state.loading).toBe(true); - A.signalUpdateCompleted(); + it('Should return true if variable needs update / validation', async () => { + const A = new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }); + const set = new SceneVariableSet({ variables: [A] }); - deactivate(); + expect(set.isVariableLoadingOrWaitingToUpdate(A)).toBe(true); + }); - expect(set.isVariableLoadingOrWaitingToUpdate(A)).toBe(true); + it('Should return false if variable does not need update / validation', async () => { + const A = new ObjectVariable({ name: 'A', value: { test: 'value' }, type: 'custom' }); + const set = new SceneVariableSet({ variables: [A] }); + + expect(set.isVariableLoadingOrWaitingToUpdate(A)).toBe(false); + }); }); }); diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.ts b/packages/scenes/src/variables/sets/SceneVariableSet.ts index b9e9207a3..93dfb5ea1 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.ts +++ b/packages/scenes/src/variables/sets/SceneVariableSet.ts @@ -352,8 +352,9 @@ export class SceneVariableSet extends SceneObjectBase imp * For example if C depends on variable B which depends on variable A and A is loading this returns true for variable C and B. */ public isVariableLoadingOrWaitingToUpdate(variable: SceneVariable) { - // If we are not active yet we have not initialized variables so we should treat them as loading - if (!this.isActive) { + // When SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true panel / query runners can activate before parents (and variable sets) + // So in order to block query execution before set has activated we check if the variable needs update/validation and if so return true here + if (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT && !this.isActive && this._variableNeedsUpdate(variable)) { return true; } From 2ad85918770084ffb620f5d30be57ca041068e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2026 10:45:02 +0100 Subject: [PATCH 09/22] fix --- .../scenes-react/src/contexts/SceneContextObject.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/scenes-react/src/contexts/SceneContextObject.tsx b/packages/scenes-react/src/contexts/SceneContextObject.tsx index 13bc7da99..912a292b0 100644 --- a/packages/scenes-react/src/contexts/SceneContextObject.tsx +++ b/packages/scenes-react/src/contexts/SceneContextObject.tsx @@ -92,7 +92,8 @@ export class SceneContextObject extends SceneObjectBase export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) { // Old behavior if (!SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT) { - ctx.addToScene(obj); + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => ctx.addToScene(obj)); return; } @@ -108,8 +109,11 @@ export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) { }; }, [ctx, obj]); - if (ctx.state.children.includes(obj)) { - return; + // Check if scene contains object instance or object with same key + for (let i = 0; i < ctx.state.children.length; i++) { + if (ctx.state.children[i] === obj || ctx.state.children[i].state.key === obj.state.key) { + return; + } } // This is technically a state change during render. Ee have to add it to the state tree right away in order to render the object on the first pass From 957bca914542c0af9617c3f205713e1375906cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2026 10:46:17 +0100 Subject: [PATCH 10/22] Fix --- packages/scenes-react/src/contexts/SceneContextObject.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/scenes-react/src/contexts/SceneContextObject.tsx b/packages/scenes-react/src/contexts/SceneContextObject.tsx index 912a292b0..147e84eb6 100644 --- a/packages/scenes-react/src/contexts/SceneContextObject.tsx +++ b/packages/scenes-react/src/contexts/SceneContextObject.tsx @@ -109,11 +109,9 @@ export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) { }; }, [ctx, obj]); - // Check if scene contains object instance or object with same key - for (let i = 0; i < ctx.state.children.length; i++) { - if (ctx.state.children[i] === obj || ctx.state.children[i].state.key === obj.state.key) { - return; - } + // Check if scene contains object instance + if (ctx.state.children.includes(obj)) { + return; } // This is technically a state change during render. Ee have to add it to the state tree right away in order to render the object on the first pass From 1a1ff31d67fc7b052f4e6c617644020f7d394687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2026 12:35:13 +0100 Subject: [PATCH 11/22] fix --- packages/scenes-react/src/components/VizPanel.test.tsx | 2 +- packages/scenes-react/src/contexts/SceneContextObject.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scenes-react/src/components/VizPanel.test.tsx b/packages/scenes-react/src/components/VizPanel.test.tsx index 30b308866..a29ad87fc 100644 --- a/packages/scenes-react/src/components/VizPanel.test.tsx +++ b/packages/scenes-react/src/components/VizPanel.test.tsx @@ -59,7 +59,7 @@ describe('VizPanel', () => { it('Should render with titleItems', () => { const scene = new SceneContextObject(); const viz = VizConfigBuilders.timeseries().build(); - const titleItems =
Title Item
; + const titleItems =
Title Item
; const { rerender, unmount } = render( diff --git a/packages/scenes-react/src/contexts/SceneContextObject.tsx b/packages/scenes-react/src/contexts/SceneContextObject.tsx index 147e84eb6..dcc1f1392 100644 --- a/packages/scenes-react/src/contexts/SceneContextObject.tsx +++ b/packages/scenes-react/src/contexts/SceneContextObject.tsx @@ -93,7 +93,7 @@ export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) { // Old behavior if (!SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT) { // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => ctx.addToScene(obj)); + useEffect(() => ctx.addToScene(obj), [ctx, obj]); return; } From ebfb3a68a861cb22943983eb5fd697fce92f994b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2026 12:43:36 +0100 Subject: [PATCH 12/22] fix --- .../scenes-app/src/demos/flickeringDemo.tsx | 141 ++++++++++++++++++ .../scenes-react/src/hooks/useSceneObject.ts | 2 +- .../src/components/layout/LazyLoader.tsx | 1 - .../src/locales/en-US/grafana-scenes.json | 3 - 4 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 packages/scenes-app/src/demos/flickeringDemo.tsx diff --git a/packages/scenes-app/src/demos/flickeringDemo.tsx b/packages/scenes-app/src/demos/flickeringDemo.tsx new file mode 100644 index 000000000..a53fd9f11 --- /dev/null +++ b/packages/scenes-app/src/demos/flickeringDemo.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { + EmbeddedScene, + PanelBuilders, + SceneAppPage, + SceneAppPageState, + SceneCSSGridLayout, + SceneFlexItem, + SceneFlexLayout, + SceneObjectBase, + SceneRefreshPicker, + SceneTimePicker, + SceneTimeRange, + SceneVariableSet, + VariableValueSelectors, +} from '@grafana/scenes'; +import { getQueryRunnerWithRandomWalkQuery } from './utils'; +import { Button, InlineSwitch } from '@grafana/ui'; +import { useLocation } from 'react-router-dom'; +import { locationUtil } from '@grafana/data'; + +export function getFlickeringDemo(defaults: SceneAppPageState) { + const layout = new SceneCSSGridLayout({ + autoRows: 'auto', + children: [], + isLazy: true, + }); + + layout.setState({ + children: [ + PanelBuilders.timeseries() + .setTitle('Panel with explore button') + .setData(getQueryRunnerWithRandomWalkQuery()) + .setHeaderActions() + .build(), + PanelBuilders.timeseries().setTitle('Panel below').setData(getQueryRunnerWithRandomWalkQuery()).build(), + ], + }); + + return new SceneAppPage({ + ...defaults, + $timeRange: new SceneTimeRange(), + controls: [new RenderBeforeActivationSwitch({}), new SceneTimePicker({}), new SceneRefreshPicker({})], + tabs: [ + new SceneAppPage({ + title: 'Overview', + url: `${defaults.url}/overview`, + routePath: 'overview', + getScene: () => { + return new EmbeddedScene({ + controls: [new VariableValueSelectors({})], + $variables: new SceneVariableSet({ + variables: [], + }), + body: layout, + }); + }, + }), + new SceneAppPage({ + title: 'Details', + url: `${defaults.url}/details`, + routePath: 'details', + getScene: () => { + return new EmbeddedScene({ + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexItem({ + body: PanelBuilders.timeseries() + .setTitle('Panel with explore button') + .setData(getQueryRunnerWithRandomWalkQuery()) + .build(), + }), + ], + }), + }); + }, + }), + ], + }); +} + +interface VizPanelExploreButtonProps { + layout: SceneCSSGridLayout; +} + +let counter = 0; + +function getNewPanel(layout: SceneCSSGridLayout) { + counter++; + + if (counter % 2 === 0) { + return PanelBuilders.timeseries() + .setTitle(`Another panel ${counter}`) + .setData(getQueryRunnerWithRandomWalkQuery()) + .setHeaderActions() + .build(); + } + + return PanelBuilders.gauge() + .setTitle(`Another panel ${counter}`) + .setData(getQueryRunnerWithRandomWalkQuery()) + .setHeaderActions() + .build(); +} + +function SwitchPanelButton({ layout }: VizPanelExploreButtonProps) { + const onClick = () => { + layout.setState({ children: [getNewPanel(layout), ...layout.state.children.slice(1)] }); + }; + + return ( + + ); +} + +export class RenderBeforeActivationSwitch extends SceneObjectBase { + public static Component = RenderBeforeActivationSwitchRenderer; +} + +function RenderBeforeActivationSwitchRenderer() { + const location = useLocation(); + + const onToggle = (evt: React.ChangeEvent) => { + const url = locationUtil.getUrlForPartial(location, { + renderBeforeActivation: evt.currentTarget.checked ? 'true' : null, + }); + window.location.href = locationUtil.assureBaseUrl(url); + }; + + return ( + + ); +} diff --git a/packages/scenes-react/src/hooks/useSceneObject.ts b/packages/scenes-react/src/hooks/useSceneObject.ts index 304ad598f..fc8e36aa5 100644 --- a/packages/scenes-react/src/hooks/useSceneObject.ts +++ b/packages/scenes-react/src/hooks/useSceneObject.ts @@ -1,4 +1,4 @@ -import { useEffect, useId } from 'react'; +import { useId } from 'react'; import { SceneObject, sceneGraph } from '@grafana/scenes'; import { useSceneContext } from './hooks'; import { CacheKey, SceneObjectConstructor, getSceneObjectCache } from '../caching/SceneObjectCache'; diff --git a/packages/scenes/src/components/layout/LazyLoader.tsx b/packages/scenes/src/components/layout/LazyLoader.tsx index c916cb07f..ce7f9728c 100644 --- a/packages/scenes/src/components/layout/LazyLoader.tsx +++ b/packages/scenes/src/components/layout/LazyLoader.tsx @@ -4,7 +4,6 @@ import { useEffectOnce } from 'react-use'; import { uniqueId } from 'lodash'; import { css } from '@emotion/css'; import { useStyles2 } from '@grafana/ui'; -import { t } from '@grafana/i18n'; export function useUniqueId(): string { const idRefLazy = useRef(undefined); diff --git a/packages/scenes/src/locales/en-US/grafana-scenes.json b/packages/scenes/src/locales/en-US/grafana-scenes.json index 4adef081c..e5474ac60 100644 --- a/packages/scenes/src/locales/en-US/grafana-scenes.json +++ b/packages/scenes/src/locales/en-US/grafana-scenes.json @@ -23,9 +23,6 @@ "subTitle": "The url did not match any page", "title": "Not found" }, - "lazy-loader": { - "placeholder": "\u00a0" - }, "nested-scene-renderer": { "collapse-button-label": "Collapse scene", "expand-button-label": "Expand scene", From a1c38a12aee98823a0934e9d4235b5caa7029491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Feb 2026 07:14:27 +0100 Subject: [PATCH 13/22] Update --- .../variables/sets/SceneVariableSet.test.tsx | 35 +++++++++++++++++++ .../src/variables/sets/SceneVariableSet.ts | 13 ++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx b/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx index 4e4312ef2..3072bb512 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx +++ b/packages/scenes/src/variables/sets/SceneVariableSet.test.tsx @@ -931,6 +931,41 @@ describe('SceneVariableList', () => { expect(nestedScene.state.didSomethingCount).toBe(2); expect(nestedScene.state.variableValueChanged).toBe(1); }); + + describe('When RENDER_BEFORE_ACTIVATION = true', () => { + beforeAll(() => (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true)); + afterAll(() => (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = false)); + + it('When local value overrides parent variable changes on top level should propagate', () => { + const topLevelVar = new TestVariable({ + name: 'test', + options: [], + value: 'B', + optionsToReturn: [{ label: 'B', value: 'B' }], + delayMs: 0, + }); + + const nestedScene = new TestObjectWithVariableDependency({ + title: '$test', + $variables: new SceneVariableSet({ + variables: [new LocalValueVariable({ name: 'test', value: 'nestedValue' })], + }), + }); + + const scene = new TestScene({ + $variables: new SceneVariableSet({ variables: [topLevelVar] }), + nested: nestedScene, + }); + + nestedScene.activate(); + nestedScene.doSomethingThatRequiresVariables(); + + scene.activate(); + + expect(nestedScene.state.didSomethingCount).toBe(1); + expect(nestedScene.state.variableValueChanged).toBe(1); + }); + }); }); describe('When changing a dependency while variable is loading', () => { diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.ts b/packages/scenes/src/variables/sets/SceneVariableSet.ts index 93dfb5ea1..d7a083aed 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.ts +++ b/packages/scenes/src/variables/sets/SceneVariableSet.ts @@ -270,6 +270,12 @@ export class SceneVariableSet extends SceneObjectBase imp * This is the main mechanism lower level variable set's react to changes on higher levels. */ private _handleParentVariableUpdatesCompleted(variable: SceneVariable, hasChanged: boolean) { + console.log( + 'Parent variable update completed, checking for dependent variables to update', + variable.state.name, + hasChanged + ); + // First loop through changed variables and add any of our variables that depend on the higher level variable to the update queue if (hasChanged) { this._addDependentVariablesToUpdateQueue(variable); @@ -279,6 +285,11 @@ export class SceneVariableSet extends SceneObjectBase imp if (this._variablesToUpdate.size > 0 && this._updating.size === 0) { this._updateNextBatch(); } + + // // For variables like local value variables we need to propagate changes down the tree + // if (variable.isAncestorLoading) { + // this._notifyDependentSceneObjects(variable); + // } } private _addDependentVariablesToUpdateQueue(variableThatChanged: SceneVariable) { @@ -352,7 +363,7 @@ export class SceneVariableSet extends SceneObjectBase imp * For example if C depends on variable B which depends on variable A and A is loading this returns true for variable C and B. */ public isVariableLoadingOrWaitingToUpdate(variable: SceneVariable) { - // When SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true panel / query runners can activate before parents (and variable sets) + // When SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true then panel and query runners can activate before parents (and variable sets) // So in order to block query execution before set has activated we check if the variable needs update/validation and if so return true here if (SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT && !this.isActive && this._variableNeedsUpdate(variable)) { return true; From ab1dbef6fcdca7380ecbef9aa3ec0c240fcb93f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Feb 2026 07:36:42 +0100 Subject: [PATCH 14/22] update --- .../scenes/src/variables/sets/SceneVariableSet.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.ts b/packages/scenes/src/variables/sets/SceneVariableSet.ts index d7a083aed..7b438774d 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.ts +++ b/packages/scenes/src/variables/sets/SceneVariableSet.ts @@ -270,12 +270,6 @@ export class SceneVariableSet extends SceneObjectBase imp * This is the main mechanism lower level variable set's react to changes on higher levels. */ private _handleParentVariableUpdatesCompleted(variable: SceneVariable, hasChanged: boolean) { - console.log( - 'Parent variable update completed, checking for dependent variables to update', - variable.state.name, - hasChanged - ); - // First loop through changed variables and add any of our variables that depend on the higher level variable to the update queue if (hasChanged) { this._addDependentVariablesToUpdateQueue(variable); @@ -285,11 +279,6 @@ export class SceneVariableSet extends SceneObjectBase imp if (this._variablesToUpdate.size > 0 && this._updating.size === 0) { this._updateNextBatch(); } - - // // For variables like local value variables we need to propagate changes down the tree - // if (variable.isAncestorLoading) { - // this._notifyDependentSceneObjects(variable); - // } } private _addDependentVariablesToUpdateQueue(variableThatChanged: SceneVariable) { From 6fe1f09868d99adae126cb83eed20de4232b0cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Feb 2026 13:30:45 +0100 Subject: [PATCH 15/22] Fixed scopes issue --- packages/scenes-app/src/demos/scopesDemo.tsx | 12 ++++++++---- .../src/variables/adhoc/AdHocFiltersVariable.tsx | 8 +++++--- .../scenes/src/variables/sets/SceneVariableSet.ts | 14 ++++++++++---- .../src/variables/variants/ScopesVariable.tsx | 6 ++++++ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/scenes-app/src/demos/scopesDemo.tsx b/packages/scenes-app/src/demos/scopesDemo.tsx index 524112fe6..bf1b81050 100644 --- a/packages/scenes-app/src/demos/scopesDemo.tsx +++ b/packages/scenes-app/src/demos/scopesDemo.tsx @@ -5,24 +5,28 @@ import { SceneAppPageState, SceneFlexItem, SceneFlexLayout, + SceneObjectBase, SceneVariableSet, ScopesVariable, + VariableValueSelectors, VizPanel, } from '@grafana/scenes'; import { EmbeddedSceneWithContext } from '@grafana/scenes-react'; import { getEmbeddedSceneDefaults, getPromQueryInstant } from './utils'; +SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true; + export function getScopesDemo(defaults: SceneAppPageState) { return new SceneAppPage({ ...defaults, - $variables: new SceneVariableSet({ - variables: [new ScopesVariable({ enable: true }), new AdHocFiltersVariable({ layout: 'combobox' })], - }), getScene: () => { return new EmbeddedSceneWithContext({ ...getEmbeddedSceneDefaults(), - + $variables: new SceneVariableSet({ + variables: [new ScopesVariable({ enable: true }), new AdHocFiltersVariable({ layout: 'combobox' })], + }), key: 'Prometheus query that uses scopes', + controls: [new VariableValueSelectors({})], body: new SceneFlexLayout({ direction: 'column', children: [ diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index da394fc73..45aa14aac 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -335,9 +335,11 @@ export class AdHocFiltersVariable const scopes = sceneGraph.getScopes(this); if (!scopes || !scopes.length) { - this.setState({ - originFilters: this.state.originFilters?.filter((filter) => filter.origin !== 'scope'), - }); + if (this.state.originFilters?.length) { + this.setState({ + originFilters: this.state.originFilters?.filter((filter) => filter.origin !== 'scope'), + }); + } return; } diff --git a/packages/scenes/src/variables/sets/SceneVariableSet.ts b/packages/scenes/src/variables/sets/SceneVariableSet.ts index 7b438774d..fdd2eda54 100644 --- a/packages/scenes/src/variables/sets/SceneVariableSet.ts +++ b/packages/scenes/src/variables/sets/SceneVariableSet.ts @@ -294,9 +294,6 @@ export class SceneVariableSet extends SceneObjectBase imp if (otherVariable.validateAndUpdate) { this._variablesToUpdate.add(otherVariable); } - - // Because _traverseSceneAndNotify skips itself (and this sets variables) we call this here to notify the variable of the change - otherVariable.variableDependency.variableUpdateCompleted(variableThatChanged, true); } } } @@ -317,8 +314,17 @@ export class SceneVariableSet extends SceneObjectBase imp * Recursivly walk the full scene object graph and notify all objects with dependencies that include any of changed variables */ private _traverseSceneAndNotify(sceneObject: SceneObject, variable: SceneVariable, hasChanged: boolean) { - // No need to notify variables under this SceneVariableSet + // Special handling for this SceneVariableSet if (this === sceneObject) { + for (const childVariable of this.state.variables) { + if (childVariable === variable) { + continue; + } + + if (childVariable.variableDependency) { + childVariable.variableDependency.variableUpdateCompleted(variable, hasChanged); + } + } return; } diff --git a/packages/scenes/src/variables/variants/ScopesVariable.tsx b/packages/scenes/src/variables/variants/ScopesVariable.tsx index 5653f3da5..234c30214 100644 --- a/packages/scenes/src/variables/variants/ScopesVariable.tsx +++ b/packages/scenes/src/variables/variants/ScopesVariable.tsx @@ -4,6 +4,7 @@ import { SceneVariable, SceneVariableState, SceneVariableValueChangedEvent, + ValidateAndUpdateResult, VariableValue, } from '../types'; import { Scope } from '@grafana/data'; @@ -15,6 +16,7 @@ import { SCOPES_VARIABLE_NAME } from '../constants'; import { isEqual } from 'lodash'; import { getQueryController } from '../../core/sceneGraph/getQueryController'; import { SCOPES_CHANGED_INTERACTION } from '../../performance/interactionConstants'; +import { Observable, of } from 'rxjs'; export interface ScopesVariableState extends SceneVariableState { /** @@ -47,6 +49,10 @@ export class ScopesVariable extends SceneObjectBase impleme }); } + public validateAndUpdate(): Observable { + return of({}); + } + /** * Temporary simple implementation to stringify the scopes. */ From 90fe5713345ad291448070cb13f96d8379fc0c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 26 Feb 2026 14:22:58 +0100 Subject: [PATCH 16/22] Fixes --- .../src/variables/adhoc/AdHocFiltersVariable.tsx | 10 ++++------ .../src/variables/variants/ScopesVariable.tsx | 13 +++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index 45aa14aac..c4545b631 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -335,16 +335,14 @@ export class AdHocFiltersVariable const scopes = sceneGraph.getScopes(this); if (!scopes || !scopes.length) { - if (this.state.originFilters?.length) { - this.setState({ - originFilters: this.state.originFilters?.filter((filter) => filter.origin !== 'scope'), - }); - } + this.setState({ + originFilters: this.state.originFilters?.filter((filter) => filter.origin !== 'scope'), + }); + return; } const scopeFilters = getAdHocFiltersFromScopes(scopes); - if (!scopeFilters.length) { return; } diff --git a/packages/scenes/src/variables/variants/ScopesVariable.tsx b/packages/scenes/src/variables/variants/ScopesVariable.tsx index 234c30214..75c28ce9a 100644 --- a/packages/scenes/src/variables/variants/ScopesVariable.tsx +++ b/packages/scenes/src/variables/variants/ScopesVariable.tsx @@ -16,7 +16,7 @@ import { SCOPES_VARIABLE_NAME } from '../constants'; import { isEqual } from 'lodash'; import { getQueryController } from '../../core/sceneGraph/getQueryController'; import { SCOPES_CHANGED_INTERACTION } from '../../performance/interactionConstants'; -import { Observable, of } from 'rxjs'; +import { Observable, Subject, of } from 'rxjs'; export interface ScopesVariableState extends SceneVariableState { /** @@ -36,6 +36,7 @@ export class ScopesVariable extends SceneObjectBase impleme // Special options that enables variables to be hidden but still render to access react contexts public UNSAFE_renderAsHidden = true; public static Component = ScopesVariableRenderer; + private _validateAndUpdateObs?: Subject; public constructor(state: Partial) { super({ @@ -50,7 +51,13 @@ export class ScopesVariable extends SceneObjectBase impleme } public validateAndUpdate(): Observable { - return of({}); + if (!this.state.loading) { + this._validateAndUpdateObs = undefined; + return of({}); + } + + this._validateAndUpdateObs = new Subject(); + return this._validateAndUpdateObs; } /** @@ -111,6 +118,8 @@ export class ScopesVariable extends SceneObjectBase impleme queryController?.startProfile(SCOPES_CHANGED_INTERACTION); this.setState({ scopes: state.value, loading }); this.publishEvent(new SceneVariableValueChangedEvent(this), true); + // Signals to SceneVariableSet that we have a value + this._validateAndUpdateObs?.next({}); } else { this.setState({ loading }); } From 9893e7da636958a24ec67553bb0b0690d5b80a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Feb 2026 13:42:33 +0100 Subject: [PATCH 17/22] Fixes --- packages/scenes/src/querying/SceneQueryRunner.test.ts | 3 +++ packages/scenes/src/querying/SceneQueryRunner.ts | 4 +++- .../src/querying/__snapshots__/SceneQueryRunner.test.ts.snap | 4 ++-- .../src/querying/layers/annotations/AnnotationsDataLayer.tsx | 4 +++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/scenes/src/querying/SceneQueryRunner.test.ts b/packages/scenes/src/querying/SceneQueryRunner.test.ts index 958033630..737b592d8 100644 --- a/packages/scenes/src/querying/SceneQueryRunner.test.ts +++ b/packages/scenes/src/querying/SceneQueryRunner.test.ts @@ -184,6 +184,9 @@ describe.each(['11.1.2', '11.1.1'])('SceneQueryRunner', (v) => { expect(sentRequest).toBeDefined(); const { scopedVars, ...request } = sentRequest!; + // Remove requestId from snapshot as it depends on test execution order + request.requestId = 'test'; + expect(Object.keys(scopedVars)).toMatchInlineSnapshot(` [ "__sceneObject", diff --git a/packages/scenes/src/querying/SceneQueryRunner.ts b/packages/scenes/src/querying/SceneQueryRunner.ts index 412e98412..34d70f52a 100644 --- a/packages/scenes/src/querying/SceneQueryRunner.ts +++ b/packages/scenes/src/querying/SceneQueryRunner.ts @@ -390,7 +390,9 @@ export class SceneQueryRunner extends SceneObjectBase implemen this._timeSubRange = timeRange; this._timeSub = timeRange.subscribeToState(() => { - this.runWithTimeRange(timeRange); + // setTimeout to let SceneVariableSet also respond to time range change + // So that variables that depend on time range have time to switch to loading state + setTimeout(() => this.runWithTimeRange(timeRange), 0); }); } diff --git a/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap b/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap index 20215f320..5e64a3ebb 100644 --- a/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap +++ b/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap @@ -21,7 +21,7 @@ exports[`SceneQueryRunner when running query should build DataQueryRequest objec "from": "now-6h", "to": "now", }, - "requestId": "SQR100", + "requestId": "test", "scopes": undefined, "startTime": 1689063488000, "targets": [ @@ -57,7 +57,7 @@ exports[`SceneQueryRunner when running query should build DataQueryRequest objec "from": "now-6h", "to": "now", }, - "requestId": "SQR187", + "requestId": "test", "scopes": undefined, "startTime": 1689063488000, "targets": [ diff --git a/packages/scenes/src/querying/layers/annotations/AnnotationsDataLayer.tsx b/packages/scenes/src/querying/layers/annotations/AnnotationsDataLayer.tsx index d9ba06593..7c411b2c2 100644 --- a/packages/scenes/src/querying/layers/annotations/AnnotationsDataLayer.tsx +++ b/packages/scenes/src/querying/layers/annotations/AnnotationsDataLayer.tsx @@ -64,7 +64,9 @@ export class AnnotationsDataLayer }); this._timeRangeSub = timeRange.subscribeToState(() => { - this.runWithTimeRange(timeRange); + // setTimeout to let SceneVariableSet also respond to time range change + // So that variables that depend on time range have time to switch to loading state + setTimeout(() => this.runWithTimeRange(timeRange), 0); }); } From dfdd7670cd9a5e78f37ff0a4e8a732adadbef5cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Mar 2026 12:30:36 +0100 Subject: [PATCH 18/22] Update packages/scenes-react/src/contexts/SceneContextObject.tsx Co-authored-by: Oscar Kilhed --- packages/scenes-react/src/contexts/SceneContextObject.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scenes-react/src/contexts/SceneContextObject.tsx b/packages/scenes-react/src/contexts/SceneContextObject.tsx index dcc1f1392..07c247c22 100644 --- a/packages/scenes-react/src/contexts/SceneContextObject.tsx +++ b/packages/scenes-react/src/contexts/SceneContextObject.tsx @@ -114,7 +114,7 @@ export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) { return; } - // This is technically a state change during render. Ee have to add it to the state tree right away in order to render the object on the first pass + // This is technically a state change during render. We have to add it to the state tree right away in order to render the object on the first pass // Should be ok as nothing subscribes to SceneContextObject state changes and the NewSceneObjectAddedEvent is syncing url state to obj state ctx.publishEvent(new NewSceneObjectAddedEvent(obj), true); ctx.setState({ children: [...ctx.state.children, obj] }); From b82b8af41123d1df3477849639815f3f45a6cd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Mar 2026 12:30:43 +0100 Subject: [PATCH 19/22] Update packages/scenes/src/core/SceneComponentWrapper.test.tsx Co-authored-by: Oscar Kilhed --- packages/scenes/src/core/SceneComponentWrapper.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scenes/src/core/SceneComponentWrapper.test.tsx b/packages/scenes/src/core/SceneComponentWrapper.test.tsx index 7dd18c60e..6c1c63f67 100644 --- a/packages/scenes/src/core/SceneComponentWrapper.test.tsx +++ b/packages/scenes/src/core/SceneComponentWrapper.test.tsx @@ -32,7 +32,7 @@ export class TestScene extends SceneObjectBase { }; } -describe('SceneComponentWrapper no render before activiation', () => { +describe('SceneComponentWrapper no render before activation', () => { beforeAll(() => { SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = false; }); From 2d6c26e499608cb1276e3c7f960a4512ab2e46cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Mar 2026 12:30:50 +0100 Subject: [PATCH 20/22] Update packages/scenes/src/core/SceneComponentWrapper.test.tsx Co-authored-by: Oscar Kilhed --- packages/scenes/src/core/SceneComponentWrapper.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scenes/src/core/SceneComponentWrapper.test.tsx b/packages/scenes/src/core/SceneComponentWrapper.test.tsx index 6c1c63f67..b8077c488 100644 --- a/packages/scenes/src/core/SceneComponentWrapper.test.tsx +++ b/packages/scenes/src/core/SceneComponentWrapper.test.tsx @@ -65,7 +65,7 @@ describe('SceneComponentWrapper no render before activation', () => { }); }); -describe('SceneComponentWrapper render before activiation', () => { +describe('SceneComponentWrapper render before activation', () => { beforeAll(() => { SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true; }); From a3a95996b9bbf077681ed1eac31d32eb82b93ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Mar 2026 13:10:55 +0100 Subject: [PATCH 21/22] Added lazy mode --- .../layout/CSSGrid/SceneCSSGridLayout.tsx | 2 +- .../src/components/layout/LazyLoader.tsx | 22 ++++++++++++++----- .../layout/grid/SceneGridLayoutRenderer.tsx | 2 +- packages/scenes/src/index.ts | 2 +- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/scenes/src/components/layout/CSSGrid/SceneCSSGridLayout.tsx b/packages/scenes/src/components/layout/CSSGrid/SceneCSSGridLayout.tsx index 30d9f1841..7b16396c2 100644 --- a/packages/scenes/src/components/layout/CSSGrid/SceneCSSGridLayout.tsx +++ b/packages/scenes/src/components/layout/CSSGrid/SceneCSSGridLayout.tsx @@ -79,7 +79,7 @@ function SceneCSSGridLayoutRenderer({ model }: SceneCSSGridItemRenderProps + ); diff --git a/packages/scenes/src/components/layout/LazyLoader.tsx b/packages/scenes/src/components/layout/LazyLoader.tsx index ce7f9728c..4e4d1699e 100644 --- a/packages/scenes/src/components/layout/LazyLoader.tsx +++ b/packages/scenes/src/components/layout/LazyLoader.tsx @@ -17,13 +17,18 @@ export interface Props extends Omit, 'onChange' onLoad?: () => void; onChange?: (isInView: boolean) => void; /** - * If true will render children on first render/mount even if it out of view - * But will LazyLoaderInViewContext will be false on first render - * This can reduce flickering / initial empty div on first render + * mount (default) = mounts/renders children when they are first in view. + * query = children is always rendered. Only sets the LazyLoaderInViewContext which is used to block query execution for panels out of view */ - onlySetIsInView?: boolean; + mode?: LazyLoaderMode; } +/** + * mount (default) = mounts children when they are first in view. + * query = children is always rendered. Only sets the LazyLoaderInViewContext which is used to block query execution for panels out of view + */ +export type LazyLoaderMode = 'mount' | 'query'; + export interface LazyLoaderType extends ForwardRefExoticComponent { addCallback: (id: string, c: (e: IntersectionObserverEntry) => void) => void; callbacks: Record void>; @@ -31,7 +36,7 @@ export interface LazyLoaderType extends ForwardRefExoticComponent { } export const LazyLoader: LazyLoaderType = React.forwardRef( - ({ children, onLoad, onChange, className, ...rest }, ref) => { + ({ children, onLoad, onChange, className, mode = 'mount', ...rest }, ref) => { const id = useUniqueId(); const { hideEmpty } = useStyles2(getStyles); const [loaded, setLoaded] = useState(false); @@ -72,7 +77,12 @@ export const LazyLoader: LazyLoaderType = React.forwardRef
); } diff --git a/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx b/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx index a4862e754..e06b650c5 100644 --- a/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx +++ b/packages/scenes/src/components/layout/grid/SceneGridLayoutRenderer.tsx @@ -119,7 +119,7 @@ const GridItemWrapper = React.forwardRef(( className={cx(className, props.className)} style={style} ref={ref} - onlySetIsInView={true} + mode="query" > {innerContent} {children} diff --git a/packages/scenes/src/index.ts b/packages/scenes/src/index.ts index e789f4b32..a244ef118 100644 --- a/packages/scenes/src/index.ts +++ b/packages/scenes/src/index.ts @@ -123,7 +123,7 @@ export { SceneGridItem } from './components/layout/grid/SceneGridItem'; export { SceneGridRow } from './components/layout/grid/SceneGridRow'; export { type SceneGridItemStateLike, type SceneGridItemLike } from './components/layout/grid/types'; export { SplitLayout } from './components/layout/split/SplitLayout'; -export { LazyLoader } from './components/layout/LazyLoader'; +export { LazyLoader, type LazyLoaderMode } from './components/layout/LazyLoader'; export { type SceneAppPageLike, type SceneRouteMatch, From 3f942cd782324b152ebaaeb7824f720bf204c649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 1 Apr 2026 07:24:19 +0200 Subject: [PATCH 22/22] Update --- packages/scenes/src/locales/en-US/grafana-scenes.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/scenes/src/locales/en-US/grafana-scenes.json b/packages/scenes/src/locales/en-US/grafana-scenes.json index 2fc1b1b64..d2fea6381 100644 --- a/packages/scenes/src/locales/en-US/grafana-scenes.json +++ b/packages/scenes/src/locales/en-US/grafana-scenes.json @@ -30,9 +30,6 @@ "group-by-key": "Group by {{keyLabel}}", "remove-group-by-key": "Remove group by {{keyLabel}}" }, - "lazy-loader": { - "placeholder": "\u00a0" - }, "nested-scene-renderer": { "collapse-button-label": "Collapse scene", "expand-button-label": "Expand scene",