Skip to content

PrimeReact Datatable cellMemo causes stale closures in event handlers #8450

@bloodykheeng

Description

@bloodykheeng

Bug Report: cellMemo causes stale closures in event handlers

Description

When cellMemo is enabled (default true), DataTable cells are memoized and don't re-render when row object references remain the same. This causes event handlers in the body template to capture stale state values (closure issue), leading to incorrect behavior when the component state changes.

so what i discorved is that if i click on row one and i cosole log global state i see only one item in array
if i click on row two i see 2 items in global state
if i click on row 3 i see 3 items in global state

yet i expected to see 3 items always every time i click cause that my gloabal state but seems cellMemo was implemeted badly instead of memorising its causing stale closures in that each row stores a capture of the global state that existed when it was created

Environment

  • PrimeReact version: "primereact": "^10.9.7",
  • React version: "react": "19.2.3",
  • Browser: chrome

Steps to Reproduce

ParentForm.tsx:

'use client'
import React from "react";
import { useForm } from "react-hook-form";
import ChildTable from "./ChildTable";

const ParentForm = () => {
    const { setValue, watch } = useForm({
        defaultValues: { items: [] }
    });

    const items = watch("items") || [];

    return (
        <div style={{ padding: "20px" }}>
            <h2>Parent Form</h2>
            <ChildTable items={items} setValue={setValue} />
        </div>
    );
};

export default ParentForm;

ChildTable.tsx:

'use client'
import React, { useState, useEffect, useMemo } from "react";
import { DataTable } from "primereact/datatable";
import { Column } from "primereact/column";
import { Button } from "primereact/button";

interface Item {
    id: number;
    name: string;
    featured: boolean;
}

const ChildTable = ({ items, setValue }: any) => {
    const [localItems, setLocalItems] = useState<Item[]>(items);

    const memoizedItems = useMemo(() => items, [items]);

    useEffect(() => {
        setLocalItems(items);
    }, [memoizedItems]);

    const addItem = () => {
        const newItem = {
            id: localItems.length + 1,
            name: `Item ${localItems.length + 1}`,
            featured: false
        };
        const updatedItems = [...localItems, newItem];
        setLocalItems(updatedItems);
        setValue("items", updatedItems);
    };

    const toggleFeatured = (index: number) => {
        console.log(`🚀 Button ${index} clicked - localItems:`, localItems.length, localItems);
    };

    return (
        <div>
            <Button label="Add Item" onClick={addItem} style={{ marginBottom: "10px" }} />

            <DataTable value={localItems}>
                <Column field="name" header="Name" />
                <Column
                    header="Actions"
                    body={(rowData, { rowIndex }) => (
                        <Button
                            label="Click Me"
                            onClick={() => toggleFeatured(rowIndex)}
                        />
                    )}
                />
            </DataTable>
        </div>
    );
};

export default ChildTable;

Reproduction Steps:

  1. Click "Add Item" button three times to add 3 items
  2. Click the "Click Me" button on each row (Item 1, Item 2, Item 3)

Actual Behavior

Console output shows stale state:

🚀 Button 0 clicked - localItems: 1 [{id: 1, name: "Item 1", featured: false}]
🚀 Button 1 clicked - localItems: 2 [{…}, {…}]
🚀 Button 2 clicked - localItems: 3 [{…}, {…}, {…}]

Each button captures the localItems state from when that specific row was first rendered:

  • Button 0 was created when array had 1 item → captures localItems.length = 1
  • Button 1 was created when array had 2 items → captures localItems.length = 2
  • Button 2 was created when array had 3 items → captures localItems.length = 3

Expected Behavior

All buttons should see the current state:

🚀 Button 0 clicked - localItems: 3 [{…}, {…}, {…}]
🚀 Button 1 clicked - localItems: 3 [{…}, {…}, {…}]
🚀 Button 2 clicked - localItems: 3 [{…}, {…}, {…}]

Root Cause

When cellMemo={true} (default), DataTable memoizes cells based on row object references. When new items are added:

  1. Existing row objects maintain the same reference (shallow copy via spread operator)
  2. DataTable's memoization prevents re-rendering of existing row cells
  3. Event handlers in those cells retain closures over stale state values
  4. Only newly added rows get fresh closures with updated state

Workaround/Solution

Setting cellMemo={false} forces all cells to re-render on every update:

<DataTable value={localItems} cellMemo={false}>

This works but may have performance implications with large datasets.

Discussion

While I understand cellMemo is designed for performance optimization, it creates a problematic interaction with React's closure behavior that:

  1. Is not immediately obvious to developers
  2. Violates the principle of least surprise
  3. Can cause hard-to-debug issues in real applications

Me i ecxpeceted that same comeponet to work as this one but i was supprised

'use client'
import React, { useState, useEffect, useMemo } from "react";

interface Item {
    id: number;
    name: string;
    featured: boolean;
}

const ChildTable = ({ items, setValue }: any) => {
    const [localItems, setLocalItems] = useState<Item[]>(items);

    const memoizedItems = useMemo(() => items, [items]);

    useEffect(() => {
        setLocalItems(items);
    }, [memoizedItems]);

    const addItem = () => {
        const newItem = {
            id: localItems.length + 1,
            name: `Item ${localItems.length + 1}`,
            featured: false
        };
        const updatedItems = [...localItems, newItem];
        setLocalItems(updatedItems);
        setValue("items", updatedItems);
    };

    const toggleFeatured = (index: number) => {
        console.log(`🚀 Button ${index} clicked - localItems length:`, localItems.length, localItems);
    };

    return (
        <div>
            <button onClick={addItem} style={{ marginBottom: "10px", padding: "10px" }}>
                Add Item
            </button>

            <table border={1} style={{ width: "100%", marginTop: "10px" }}>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    {localItems.map((item, index) => (
                        <tr key={item.id}>
                            <td>{item.name}</td>
                            <td>
                                <button onClick={() => toggleFeatured(index)}>
                                    Click Me
                                </button>
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );
};

export default ChildTable;

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: PerformanceIssue is performance or optimization related

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions