Repository: plotly/dash-ag-grid (language: python)

Issue:
Dash AG Grid bug in getRowStyle with infinite scroll
Dash AG-GRID getRowStyle with infinite scroll does not work.

```
import dash_ag_grid as dag
from dash import Dash, Input, Output, html, no_update, callback
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    # "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
    "data.csv"
)

crimson_palette = ['#fff5f0', '#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a'][::-1]
getRowStyle            = {
    "styleConditions": [
        {
            "condition": "params.data['athlete'].includes('Michael')",
            "style": {
                "backgroundColor": crimson_palette[0],
                'color'          : 'whitesmoke'
            },
        },
        {
            "condition": "params.data['athlete'].includes('Ryan')",
            "style": {
                "backgroundColor": crimson_palette[1],
                # 'color': 'whitesmoke'
            },
        },
    ]
}

columnDefs = [
    # this row shows the row index, doesn't use any data from the row
    {
        "headerName": "ID",
        "maxWidth": 100,
        # it is important to have node.id here, so that when the id changes (which happens when the row is loaded)
        # then the cell is refreshed.
        "valueGetter": {"function": "params.node.id"},
    },
    {"field": "athlete", "minWidth": 150},
    {"field": "country", "minWidth": 150},
    {"field": "year"},
    {"field": "sport", "minWidth": 150},
    {"field": "total"},
]

defaultColDef = {
    "flex": 1,
    "minWidth": 150,
    "sortable": False,
    "resizable": True,
}

app.layout = html.Div(
    [
        dag.AgGrid(
            id="infinite-row-no-sort",
            columnDefs=columnDefs,
            defaultColDef=defaultColDef,
            rowModelType="infinite",
            getRowStyle=getRowStyle,
            dashGridOptions={
                "rowSelection": "multiple",
                "rowBuffer": 0,
                # how big each page in our page cache will be, default is 100
                "cacheBlockSize": 100,
                # How many blocks to keep in the store. Default is no limit, so every requested block is kept.
                # how many extra blank rows to display to the user at the end of the dataset, which sets the vertical
                # scroll and then allows the grid to request viewing more rows of data.
                # Default is 1, meaning show 1 row.
                "cacheOverflowSize": 2,
                # how many server side requests to send at a time. if user is scrolling lots, then the requests are
                # throttled down
                "maxConcurrentDatasourceRequests": 1,
                # how many rows to initially show in the grid. having 1 shows a blank row, so it looks like the grid
                # is loading from the users perspective
                "infiniteInitialRowCount": 1000,
                # how many pages to store in cache. default is undefined, which allows an infinite sized cache, pages
                # are never purged. this should be set for large data to stop your browser from getting full of data
                "maxBlocksInCache": 10,
            },
        ),
    ],
)


@callback(
    Output("infinite-row-no-sort", "getRowsResponse"),
    Input("infinite-row-no-sort", "getRowsRequest"),
)
def infinite_scroll(request):
    if request is None:
        return no_update
    partial = df.iloc[request["startRow"]: request["endRow"]]
    return {"rowData": partial.to_dict("records"), "rowCount": len(df.index)}


if __name__ == "__main__":
    app.run(debug=True)
```


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94f2469..44e2841 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,7 +21,9 @@ Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source D
 
 ### Changed
  - [#327](https://github.com/plotly/dash-ag-grid/pull/327) bumping to v`31.3.2` for the grid
-
+ - [#332](https://github.com/plotly/dash-ag-grid/pull/332)
+   - `handleDynamicStyle` now checks for `params.data` before attempting to apply the test
+   - [#321](https://github.com/plotly/dash-ag-grid/issues/321) fixes issue with infinite scroll and dynamic row styling
 
 ## [31.2.0] - 2024-02-25
 
diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js
index 0682fb9..84df8aa 100644
--- a/src/lib/fragments/AgGrid.react.js
+++ b/src/lib/fragments/AgGrid.react.js
@@ -1120,8 +1120,12 @@ export default class DashAgGrid extends Component {
             }));
             return (params) => {
                 for (const {test, style} of tests) {
-                    if (test(params)) {
-                        return style;
+                    if (params) {
+                        if (params.data) {
+                            if (test(params)) {
+                                return style;
+                            }
+                        }
                     }
                 }
                 return _defaultStyle;


Test patch:
diff --git a/tests/test_infinite_scroll.py b/tests/test_infinite_scroll.py
index cbcce94..ca3ef0b 100644
--- a/tests/test_infinite_scroll.py
+++ b/tests/test_infinite_scroll.py
@@ -155,3 +155,96 @@ def infinite_scroll(request):
     grid.wait_for_cell_text(1, 0, "3600")
     grid.get_header_cell(1).click()
     grid.wait_for_cell_text(0, 0, "7263")
+
+
+def test_is002_infinite_scroll_styling(dash_duo):
+    app = Dash(__name__)
+
+    df = pd.read_csv(
+        "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
+        # "data.csv"
+    )
+
+    crimson_palette = ['#fff5f0', '#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a'][::-1]
+    getRowStyle = {
+        "styleConditions": [
+            {
+                "condition": "params.data['athlete'].includes('Michael')",
+                "style": {
+                    "backgroundColor": crimson_palette[0],
+                    'color': 'whitesmoke'
+                },
+            },
+            {
+                "condition": "params.data['athlete'].includes('Ryan')",
+                "style": {
+                    "backgroundColor": crimson_palette[1],
+                    # 'color': 'whitesmoke'
+                },
+            },
+        ]
+    }
+
+    columnDefs = [
+        # this row shows the row index, doesn't use any data from the row
+        {
+            "headerName": "ID",
+            "maxWidth": 100,
+            # it is important to have node.id here, so that when the id changes (which happens when the row is loaded)
+            # then the cell is refreshed.
+            "valueGetter": {"function": "params.node.id"},
+        },
+        {"field": "athlete", "minWidth": 150},
+        {"field": "country", "minWidth": 150},
+        {"field": "year"},
+        {"field": "sport", "minWidth": 150},
+        {"field": "total"},
+    ]
+
+    defaultColDef = {
+        "flex": 1,
+        "minWidth": 150,
+        "sortable": False,
+        "resizable": True,
+    }
+
+    app.layout = html.Div(
+        [
+            dag.AgGrid(
+                id="infinite-row-no-sort",
+                columnDefs=columnDefs,
+                defaultColDef=defaultColDef,
+                rowModelType="infinite",
+                getRowStyle=getRowStyle,
+            ),
+            html.Button('scroll', id='scroll')
+        ],
+    )
+
+    @app.callback(
+        Output("infinite-row-no-sort", "getRowsResponse"),
+        Input("infinite-row-no-sort", "getRowsRequest"),
+    )
+    def infinite_scroll(request):
+        if request is None:
+            return no_update
+        partial = df.iloc[request["startRow"]: request["endRow"]]
+        return {"rowData": partial.to_dict("records"), "rowCount": len(df.index)}
+
+    @app.callback(
+        Output("infinite-row-no-sort", "scrollTo"),
+        Input('scroll', 'n_clicks'),
+        prevent_initial_call=True
+    )
+    def scroll(n):
+        return {'rowIndex': n*1000+n}
+
+    dash_duo.start_server(app)
+
+    grid = utils.Grid(dash_duo, "infinite-row-no-sort")
+    grid.wait_for_cell_text(0, 0, "0")
+    time.sleep(5)
+    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