diff --git a/CHANGELOG.md b/CHANGELOG.md index 0252802f..a4bb9f02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source D - Added test for `OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS` to test that the value is an object before parsing, - this allows for reused keys to not comply with being an object +### Fixed +- [#454](https://github.com/plotly/dash-ag-grid/pull/454) fixes issue where a rowCount of 0 would cause the grid not to display new data ## [35.2.0] - 2026-04-03 ### Added diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 6f7dad3a..dd5ed434 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -274,6 +274,9 @@ export function DashAgGrid(props) { const [openGroups, setOpenGroups] = useState({}); const [columnState_push, setColumnState_push] = useState(true); const [rowTransactionState, setRowTransactionState] = useState(null); + const resettingCount = useRef(false); + const prevRowCountRef = useRef(null); + const resetTimeoutRef = useRef(null); const components = useMemo( () => ({ @@ -841,9 +844,11 @@ export function DashAgGrid(props) { return { getRows(params) { getRowsParams.current = params; + if (resettingCount.current) { + return; + } customSetProps({getRowsRequest: params}); }, - destroy() { getRowsParams.current = null; }, @@ -1408,13 +1413,50 @@ export function DashAgGrid(props) { } }, [props.id]); - // Handle infinite scrolling datasource + // handle getRowsResponse useEffect(() => { - if (isDatasourceLoadedForInfiniteScrolling()) { + if (isDatasourceLoadedForInfiniteScrolling() && getRowsParams.current) { + const params = getRowsParams.current; + const {rowData, rowCount} = props.getRowsResponse; - getRowsParams.current.successCallback(rowData, rowCount); + + // If we were previously at 0 rows, tell ag‑Grid the new count first, + // then defer the successCallback so ag‑Grid has processed setRowCount. + // This avoids an edge case where ag‑Grid ignores the successCallback because it thinks the + // request is already fulfilled, since the row count is >0, but then doesn't render any rows + // because it hasn't processed the new row count yet. + // We do not use purge, reset on the cache or datasource refresh here, + // since those would trigger a new getRows request, which we do not want since we already have the new data + // and just need to get ag‑Grid to process the new row count and render it. + if ( + prevRowCountRef.current !== null && + prevRowCountRef.current === 0 + ) { + resettingCount.current = true; + params.api.setRowCount(rowCount, false); + + resetTimeoutRef.current = setTimeout(() => { + resettingCount.current = false; + const p = getRowsParams.current; + if (p) { + p.successCallback(rowData, rowCount); + } + }, 0); + } else { + params.successCallback(rowData, rowCount); + } + + prevRowCountRef.current = rowCount; customSetProps({getRowsResponse: null}); } + + return () => { + if (resetTimeoutRef.current) { + clearTimeout(resetTimeoutRef.current); + resetTimeoutRef.current = null; + } + resettingCount.current = false; + }; }, [props.getRowsResponse]); // Handle master detail response diff --git a/tests/test_infinite_scroll.py b/tests/test_infinite_scroll.py index d34f4e7d..6294c104 100644 --- a/tests/test_infinite_scroll.py +++ b/tests/test_infinite_scroll.py @@ -3,12 +3,12 @@ """ import dash_ag_grid as dag -import dash -from dash import Input, Output, html, dcc, Dash, no_update -from . import utils -import pandas as pd +from dash import Input, Output, html, dcc, Dash, no_update, ctx from dash.testing.wait import until +from . import utils import time +import numpy as np +import pandas as pd def test_is001_infinite_scroll(dash_duo): @@ -247,4 +247,87 @@ def scroll(n): for x in range(8): dash_duo.find_element("#scroll").click() time.sleep(3) # pausing to emulate separation because user inputs - assert list(filter(lambda i: i.get("level") != "WARNING", dash_duo.get_logs())) == [] \ No newline at end of file + assert list(filter(lambda i: i.get("level") != "WARNING", dash_duo.get_logs())) == [] + +def test_is003_infinite_scroll_clear(dash_duo): + app = Dash(__name__) + data = pd.DataFrame( + { + "id": [str(x) for x in range(1000)], + "value": np.random.rand(1000), + } + ) + + clear_button = html.Button("Clear", id="clear") + reset_button = html.Button("Reset", id='reset') + + grid = dag.AgGrid( + columnDefs=[ + {"field": "id"}, + {"field": "value"}, + ], + getRowId="params.data.id", + rowModelType="infinite", + id="grid" + ) + + test_data = [] + + + @app.callback( + Output(grid, "getRowsResponse"), + Input(grid, "getRowsRequest"), + Input(clear_button, "n_clicks"), + Input(reset_button, "n_clicks"), + prevent_initial_call=True, + ) + def update_rfq_grid_rows( + request, + n_clicks_clear, + n_clicks_reset, + ): + if ctx.triggered_id == "clear": + response = { + "rowData": [], "rowCount": 0, + } + return response + + if request is None: + partial_df = data.head(100) + else: + partial_df = data.iloc[request["startRow"] : request["endRow"]] + + response = { + "rowData": partial_df.to_dict("records"), + "rowCount": len(data.index), + } + test_data.append('response') + return response + + app.layout = html.Div( + children=[ + clear_button, + reset_button, + grid, + ] + ) + + dash_duo.start_server(app) + + grid_dom = utils.Grid(dash_duo, 'grid') + grid_dom.wait_for_cell_text(0, 0, "0") + + for x in range(2, 5): + dash_duo.find_element("#clear").click() + until( + lambda: len( + dash_duo.find_elements( + "#grid .ag-center-cols-container > *" + ) + ) + == 0, + timeout=3, + ) + dash_duo.find_element("#reset").click() + grid_dom.wait_for_cell_text(0, 0, "0") + assert x == len(test_data) # make sure the callback was called the expected number of times \ No newline at end of file