While working with PrimeReact’s DataTable, I ran into a bug that nearly broke my brain.
Buttons inside table rows were logging different state values, even though the state was global and clearly up to date.
At first glance, it looked like:
- a React Hook Form issue
- or a state sync problem
- or even a React bug
It turned out to be none of those.
This post walks through:
- the exact problem
- why it was so confusing
- how I isolated the root cause
- and how a one-line fix resolved it without sacrificing performance ________________________________________________________
🧩 The Problem: “Why does each row see a different state?”
I had a DataTable where each row had a button.
Clicking the buttons produced this output:
Button 0 clicked → items.length = 1
Button 1 clicked → items.length = 2
Button 2 clicked → items.length = 3
But this made no sense.
At the time of clicking:
the global state clearly had 3 items
every button should have logged 3
Instead, each button behaved as if it remembered an older version of state.
🔬 Reproducing the Issue
Steps to reproduce:
- Add 3 rows to the table
- Click the button in row 1 → logs 1
- Click row 2 → logs 2
- Click row 3 → logs 3
Each row was somehow “stuck” in the past.
🤔 Why This Was So Confusing
Here’s what made this bug especially sneaky:
- The UI looked correct
- State updates were working
- No errors, no warnings
- Disabling memoization (cellMemo={false}) “fixed” it
That last part was the clue.
đź§ The Hidden Culprit: Memoization + Closures
PrimeReact’s DataTable uses cell memoization (cellMemo=true by default) to improve performance.
That means:
- Cells do not re-render
- unless specific memo keys change
What went wrong?
- Existing rows kept the same object reference
- Memoized cells were not re-rendered
- Event handlers (
onClick) kept the closure from the render they were created in
So each button captured a snapshot of state at creation time.
This is classic stale closure behavior — but caused by memoization, not state logic.
đź§Ş Minimal Reproduction (Pure React)
I recreated the exact same bug with React.memo:
const Row = React.memo(
({ item, index, snapshotLength }) => {
const onClick = () => {
console.log(index, snapshotLength);
};
return <button onClick={onClick}>Click</button>;
},
(prev, next) => prev.item === next.item // ❌ ignores snapshotLength
);
Because snapshotLength was ignored:
the row never re-rendered
the click handler kept stale data
This was the exact same failure mode as DataTable’s cellMemo.
🛠️ The Actual Fix (Not the Hack)
The easy workaround was:
<DataTable cellMemo={false} />
But that:
- disables performance optimizations
- hurts large tables
The real fix
The memoization key needed to change when row structure changed.
In PrimeReact, the fix was simple:
Before
cellMemoProps = { index }
After
cellMemoProps = { rowIndex }
Why this works:
- rowIndex changes when rows are added/removed/reordered
- Memoized cells now re-render when they should
- Fresh closures, same performance benefits
âś… Result
After the fix:
Button 0 → items.length = 3
Button 1 → items.length = 3
Button 2 → items.length = 3
- No stale closures.
- No performance regression.
- No API changes. ________________________________________________________
🤯 Bonus Insight: Why useFieldArray “Just Worked”
One interesting discovery:
When using React Hook Form’s useFieldArray, this bug never appeared.
Why?
Because useFieldArray:
creates new object references on updates
naturally invalidates memoization
So rows re-render automatically.
It wasn’t magic — just object identity working in your favor.
đź§ Key Takeaways
- Memoization bugs are identity bugs, not state bugs
- If a prop affects behavior, it must participate in memo invalidation
- Stale closures can exist even when state is correct
- Disabling memoization is a workaround, not a solution
- Performance optimizations demand extra correctness discipline
🚀 Final Thoughts
This was one of those bugs that:
- looks impossible
- survives logging
- disappears when you “simplify” things
But once you think in render snapshots, everything clicks.
Huge thanks to the PrimeReact maintainers for quick feedback and collaboration — and to the original reporter for the excellent reproduction.
Top comments (0)