Skip to content
6 changes: 6 additions & 0 deletions app/client/src/actions/windowActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";

export const updateWindowDimensions = (height: number, width: number) => ({
type: ReduxActionTypes.UPDATE_WINDOW_DIMENSIONS,
payload: { height, width },
});
5 changes: 5 additions & 0 deletions app/client/src/ce/constants/ReduxActionConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,10 @@ const AppThemeActionsTypes = {
RESET_APP_THEME_SUCCESS: "RESET_APP_THEME_SUCCESS",
};

const WindowActionsTypes = {
UPDATE_WINDOW_DIMENSIONS: "UPDATE_WINDOW_DIMENSIONS",
};

const AppThemeActionErrorTypes = {
FETCH_APP_THEMES_ERROR: "FETCH_APP_THEMES_ERROR",
SET_DEFAULT_SELECTED_THEME_ERROR: "SET_DEFAULT_SELECTED_THEME_ERROR",
Expand Down Expand Up @@ -1304,6 +1308,7 @@ export const ReduxActionTypes = {
...AIActionTypes,
...ApplicationActionTypes,
...AppThemeActionsTypes,
...WindowActionsTypes,
...AppViewActionTypes,
...AppSettingsActionTypes,
...BatchUpdateActionTypes,
Expand Down
2 changes: 2 additions & 0 deletions app/client/src/ce/reducers/uiReducers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import crudInfoModalReducer from "reducers/uiReducers/crudInfoModalReducer";
import { widgetReflowReducer } from "reducers/uiReducers/reflowReducer";
import jsObjectNameReducer from "reducers/uiReducers/jsObjectNameReducer";
import appThemingReducer from "reducers/uiReducers/appThemingReducer";
import windowReducer from "reducers/uiReducers/windowReducer";
import mainCanvasReducer from "ee/reducers/uiReducers/mainCanvasReducer";
import focusHistoryReducer from "reducers/uiReducers/focusHistoryReducer";
import { editorContextReducer } from "ee/reducers/uiReducers/editorContextReducer";
Expand Down Expand Up @@ -85,6 +86,7 @@ export const uiReducerObject = {
crudInfoModal: crudInfoModalReducer,
widgetReflow: widgetReflowReducer,
appTheming: appThemingReducer,
windowDimensions: windowReducer,
mainCanvas: mainCanvasReducer,
appSettingsPane: appSettingsPaneReducer,
focusHistory: focusHistoryReducer,
Expand Down
2 changes: 2 additions & 0 deletions app/client/src/ce/sagas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { watchActionSagas } from "sagas/ActionSagas";
import apiPaneSagas from "sagas/ApiPaneSagas";
import applicationSagas from "ee/sagas/ApplicationSagas";
import appThemingSaga from "sagas/AppThemingSaga";
import windowSaga from "sagas/WindowSaga";
import AutoHeightSagas from "sagas/autoHeightSagas";
import autoLayoutUpdateSagas from "sagas/AutoLayoutUpdateSagas";
import batchSagas from "sagas/BatchSagas";
Expand Down Expand Up @@ -95,6 +96,7 @@ export const sagas = [
gitSyncSagas,
SuperUserSagas,
appThemingSaga,
windowSaga,
NavigationSagas,
editorContextSagas,
AutoHeightSagas,
Expand Down
21 changes: 21 additions & 0 deletions app/client/src/pages/AppIDE/AppIDE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type { Page } from "entities/Page";
import { IDE_HEADER_HEIGHT } from "@appsmith/ads";
import { GitApplicationContextProvider } from "git-artifact-helpers/application/components";
import { AppIDEModals } from "ee/pages/AppIDE/components/AppIDEModals";
import { updateWindowDimensions } from "actions/windowActions";

interface EditorProps {
currentApplicationId?: string;
Expand All @@ -60,12 +61,14 @@ interface EditorProps {
isMultiPane: boolean;
widgetConfigBuildSuccess: () => void;
pages: Page[];
updateWindowDimensions: (height: number, width: number) => void;
}

type Props = EditorProps & RouteComponentProps<BuilderRouteParams>;

class Editor extends Component<Props> {
prevPageId: string | null = null;
private handleResize: (() => void) | null = null;

componentDidMount() {
const { basePageId } = this.props.match.params || {};
Expand All @@ -75,6 +78,17 @@ class Editor extends Component<Props> {
editorInitializer().then(() => {
this.props.widgetConfigBuildSuccess();
});

// Set up window resize listener for window dimensions
this.handleResize = () => {
this.props.updateWindowDimensions(window.innerHeight, window.innerWidth);
};

// Set initial dimensions immediately
this.props.updateWindowDimensions(window.innerHeight, window.innerWidth);

// Add resize listener
window.addEventListener("resize", this.handleResize);
}

shouldComponentUpdate(nextProps: Props) {
Expand Down Expand Up @@ -159,6 +173,11 @@ class Editor extends Component<Props> {
componentWillUnmount() {
this.props.resetEditorRequest();
urlBuilder.setCurrentBasePageId(null);

// Clean up window resize listener
if (this.handleResize) {
window.removeEventListener("resize", this.handleResize);
}
}

public render() {
Expand Down Expand Up @@ -218,6 +237,8 @@ const mapDispatchToProps = (dispatch: any) => {
setupPage: (pageId: string) => dispatch(setupPageAction({ id: pageId })),
updateCurrentPage: (pageId: string) => dispatch(updateCurrentPage(pageId)),
widgetConfigBuildSuccess: () => dispatch(widgetInitialisationSuccess()),
updateWindowDimensions: (height: number, width: number) =>
dispatch(updateWindowDimensions(height, width)),
};
};

Expand Down
22 changes: 21 additions & 1 deletion app/client/src/pages/AppViewer/AppPage/AppPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useEffect, useMemo, useRef } from "react";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import type { CanvasWidgetStructure } from "WidgetProvider/types";
import { useSelector } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import { getAppMode } from "ee/selectors/applicationSelectors";
import { APP_MODE } from "entities/App";
import { renderAppsmithCanvas } from "layoutSystems/CanvasFactory";
import type { WidgetProps } from "widgets/BaseWidget";
import { useAppViewerSidebarProperties } from "utils/hooks/useAppViewerSidebarProperties";
import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors";
import { updateWindowDimensions } from "actions/windowActions";

import { PageView, PageViewWrapper } from "./AppPage.styled";
import { useCanvasWidthAutoResize } from "../../hooks/useCanvasWidthAutoResize";
Expand All @@ -24,6 +25,7 @@ export function AppPage(props: AppPageProps) {
const { appName, basePageId, canvasWidth, pageName, widgetsStructure } =
props;

const dispatch = useDispatch();
const appMode = useSelector(getAppMode);
const isPublished = appMode === APP_MODE.PUBLISHED;
const isAnvilLayout = useSelector(getIsAnvilLayout);
Expand All @@ -46,6 +48,24 @@ export function AppPage(props: AppPageProps) {
});
}, [appName, basePageId, pageName]);

// Set up window resize listener for window dimensions
useEffect(() => {
const handleResize = () => {
dispatch(updateWindowDimensions(window.innerHeight, window.innerWidth));
};

// Set initial dimensions immediately
dispatch(updateWindowDimensions(window.innerHeight, window.innerWidth));

// Add resize listener
window.addEventListener("resize", handleResize);

// Cleanup
return () => {
window.removeEventListener("resize", handleResize);
};
}, [dispatch]);

return (
<PageViewWrapper
hasPinnedSidebar={hasSidebarPinned}
Expand Down
25 changes: 25 additions & 0 deletions app/client/src/reducers/uiReducers/windowReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createImmerReducer } from "utils/ReducerUtils";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import type { ReduxAction } from "actions/ReduxActionTypes";

export interface WindowDimensionsState {
height: number;
width: number;
}

const initialState: WindowDimensionsState = {
height: typeof window !== "undefined" ? window.innerHeight : 0,
width: typeof window !== "undefined" ? window.innerWidth : 0,
};

const windowReducer = createImmerReducer(initialState, {
[ReduxActionTypes.UPDATE_WINDOW_DIMENSIONS]: (
state: WindowDimensionsState,
action: ReduxAction<{ height: number; width: number }>,
) => {
state.height = action.payload.height;
state.width = action.payload.width;
},
});

export default windowReducer;
17 changes: 17 additions & 0 deletions app/client/src/sagas/WindowSaga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import { takeLatest, select, call } from "redux-saga/effects";
import { evaluateTreeSaga } from "./EvaluationsSaga";
import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";

export function* handleWindowDimensionsUpdate() {
const unEvalAndConfigTree = yield select(getUnevaluatedDataTree);

yield call(evaluateTreeSaga, unEvalAndConfigTree);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add debouncing to prevent excessive tree re-evaluations.

Window resize events fire rapidly during user interaction. Re-evaluating the entire data tree on each event can severely impact performance, especially in applications with complex state. Even with takeLatest, evaluations may start before resizing completes.

Consider adding a debounce pattern. Replace takeLatest in the saga with a debounced approach:

+import { debounce } from "redux-saga/effects";
+
 export default function* windowSaga() {
-  yield takeLatest(
+  yield debounce(
+    250, // Wait 250ms after last resize event
     ReduxActionTypes.UPDATE_WINDOW_DIMENSIONS,
     handleWindowDimensionsUpdate,
   );
 }

This ensures the tree is re-evaluated only after the user has finished resizing, rather than continuously during the resize operation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/client/src/sagas/WindowSaga.ts around lines 6 to 10, the saga currently
calls evaluateTreeSaga directly on every resize which causes excessive
re-evaluations; replace this with a debounced approach so the tree is evaluated
only after resize noise settles. Implement a debounce by creating a watcher that
collects resize actions and delays execution (e.g., 200–300ms) before calling
evaluateTreeSaga, cancelling/resetting the delay on new resize events (use
redux-saga's debounce helper from redux-saga/effects or implement with a loop
using take, fork, delay and cancel). Ensure the debounced handler still selects
getUnevaluatedDataTree before calling evaluateTreeSaga and that cancellation
logic prevents multiple concurrent evaluations.


export default function* windowSaga() {
yield takeLatest(
ReduxActionTypes.UPDATE_WINDOW_DIMENSIONS,
handleWindowDimensionsUpdate,
);
}
4 changes: 4 additions & 0 deletions app/client/src/selectors/dataTreeSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import "url-search-params-polyfill";
import type { DefaultRootState } from "react-redux";
import { getSelectedAppThemeProperties } from "./appThemingSelectors";
import { getWindowDimensions } from "./windowSelectors";
import type { LoadingEntitiesState } from "reducers/evaluationReducers/loadingEntitiesReducer";
import _, { get } from "lodash";
import type { EvaluationError } from "utils/DynamicBindingUtils";
Expand Down Expand Up @@ -149,6 +150,7 @@ export const getUnevaluatedDataTree = createSelector(
getMetaWidgetsFromUnevaluatedDataTree,
getAppData,
getSelectedAppThemeProperties,
getWindowDimensions,
getCurrentAppWorkspace,
getCurrentApplication,
getCurrentPageName,
Expand All @@ -160,6 +162,7 @@ export const getUnevaluatedDataTree = createSelector(
metaWidgets,
appData,
theme,
windowDimensions,
currentWorkspace,
currentApplication,
getCurrentPageName,
Expand All @@ -182,6 +185,7 @@ export const getUnevaluatedDataTree = createSelector(
// taking precedence in case the key is the same
store: appData.store,
theme,
ui: windowDimensions,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the structure of appsmith.ui to match the intended API.

The assignment ui: windowDimensions directly exposes the window dimensions object (with height and width properties) as appsmith.ui. Ensure this matches the desired public API structure and doesn't conflict with any existing ui property.


🏁 Script executed:

#!/bin/bash
# Search for existing references to appsmith.ui to verify structure expectations
rg -nP 'appsmith\.ui\b' --type=ts --type=tsx -C3

Length of output: 27



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for existing references to appsmith.ui with corrected type flags
rg -nP 'appsmith\.ui\b' --type=typescript -C3

Length of output: 34


🏁 Script executed:

#!/bin/bash
# Also search for ui property assignments and windowDimensions usage in dataTree context
rg -nP '(ui\s*:|windowDimensions)' app/client/src/selectors/dataTreeSelectors.ts -C5

Length of output: 691


🏁 Script executed:

#!/bin/bash
# Search for dataTree object construction to understand the structure
rg -nP 'dataTree\s*=\s*\{' --type=typescript -C10

Length of output: 34



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the AppsmithEntity type definition
rg -nP 'type AppsmithEntity|interface AppsmithEntity' -C5

Length of output: 2717


🏁 Script executed:

#!/bin/bash
# Search for ui property references in the same file
rg -n 'ui:' app/client/src/selectors/dataTreeSelectors.ts

Length of output: 31


🏁 Script executed:

#!/bin/bash
# Find windowDimensions type/definition
rg -nP 'windowDimensions\s*[=:|]' -C3 | head -50

Length of output: 613


🏁 Script executed:

#!/bin/bash
# Search for appsmith.ui references without type flags
rg -n 'appsmith\.ui' -C2

Length of output: 3987



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the complete AppsmithEntity interface definition
rg -n 'export interface AppsmithEntity' -A20 app/client/src/ce/entities/DataTree/types.ts

Length of output: 451


🏁 Script executed:

#!/bin/bash
# Also check if ui is defined in AppDataState (which AppsmithEntity extends from)
rg -n 'interface AppDataState|type AppDataState' -A15

Length of output: 1319



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the complete selector function and type casting context around line 193
rg -n 'as AppsmithEntity' app/client/src/selectors/dataTreeSelectors.ts -B10 -A2

Length of output: 656


🏁 Script executed:

#!/bin/bash
# Search for any type extensions or augmentations of AppsmithEntity that add ui property
rg -n 'AppsmithEntity' app/client/src/ce/entities/DataTree/types.ts -A30

Length of output: 906


Add ui property to the AppsmithEntity interface definition.

The assignment of ui: windowDimensions at line 188 is being bypassed by the as AppsmithEntity cast at line 193, but the ui property is not declared in the interface. Production code in ExternalWidget and CustomWidget actively consumes appsmith.ui with a {width, height} structure, confirming this is intentional.

Update app/client/src/ce/entities/DataTree/types.ts to declare the property:

export interface AppsmithEntity extends Omit<AppDataState, "store"> {
  ENTITY_TYPE: typeof ENTITY_TYPE.APPSMITH;
  store: Record<string, unknown>;
  theme: AppTheme["properties"];
  ui: { width: number; height: number };
  currentPageName: string;
  workspaceName: string;
  appName: string;
  currentEnvironmentName: string;
}
🤖 Prompt for AI Agents
In app/client/src/ce/entities/DataTree/types.ts (update the AppsmithEntity
interface), add the missing ui property declaration so AppsmithEntity includes
ui: { width: number; height: number }; keep the rest of the interface as-is
(Omit<AppDataState, "store">, ENTITY_TYPE, store, theme, currentPageName,
workspaceName, appName, currentEnvironmentName) to match runtime usage where
code sets appsmith.ui = windowDimensions; ensure any necessary imports/types
referenced (e.g., AppTheme) remain unchanged.

currentPageName: getCurrentPageName,
workspaceName: currentWorkspace.name,
appName: currentApplication?.name,
Expand Down
5 changes: 5 additions & 0 deletions app/client/src/selectors/windowSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { DefaultRootState } from "react-redux";

export const getWindowDimensions = (state: DefaultRootState) => {
return state.ui.windowDimensions;
};
Loading