🧠 The React Bug That Only Appears When Your Code Is Too Fast
Source: Dev.to
🧩 The Problem: “Why does each row see a different state?”
I had a PrimeReact DataTable where each row contained a button. Clicking the buttons produced output like:
Button 0 clicked → items.length = 1
Even though the global state at click time clearly had 3 items, each button behaved as if it remembered an older version of the state.
🔬 Reproducing the Issue
- Add 3 rows to the table.
- Click the button in row 1 → logs 1.
- Click the button in row 2 → logs 2.
- Click the button in row 3 → logs 3.
Each row appeared “stuck” in the past.
🤔 Why This Was So Confusing
- The UI looked correct.
- State updates were working.
- No errors or warnings appeared.
- Disabling memoization (
cellMemo={false}) “fixed” it, which turned out to be the clue.
🧠 The Hidden Culprit: Memoization + Closures
PrimeReact’s DataTable enables cell memoization (cellMemo=true by default) to improve performance. This means cells only re‑render when specific memo keys change.
What went wrong?
- Existing rows kept the same object reference.
- Memoized cells were not re‑rendered.
- Event handlers (
onClick) retained the closure from the render they were created in.
Thus each button captured a snapshot of state at creation time—a classic stale‑closure problem caused by memoization, not by the state logic itself.
🧪 Minimal Reproduction (Pure React)
import React from "react";
const Row = React.memo(
({ item, index, snapshotLength }) => {
const onClick = () => {
console.log(index, snapshotLength);
};
return Click;
},
(prev, next) => prev.item === next.item // ❌ ignores snapshotLength
);
Because snapshotLength was ignored in the memo comparison, the row never re‑rendered, and the click handler kept stale data—mirroring the DataTable bug.
🛠️ The Actual Fix (Not the Hack)
Quick Workaround
Disabling memoization removes the performance benefits for large tables.
Proper Fix
In PrimeReact, adjust the memoization key so it changes when the row structure changes.
Before
cellMemoProps = { index };
After
cellMemoProps = { rowIndex };
rowIndex changes when rows are added, removed, or reordered, causing memoized cells to re‑render when needed. This restores fresh closures without sacrificing performance.
✅ Result
After applying 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”
When using React Hook Form’s useFieldArray, the bug never appeared because useFieldArray creates new object references on updates, naturally invalidating memoization. Rows re‑render automatically—no magic, just object identity working in your favor.
🧠 Key Takeaways
- Memoization bugs are identity bugs, not state bugs.
- Any prop that influences behavior must be part of the memo‑invalidating key.
- 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 bug:
- Looked impossible.
- Survived logging.
- Disappeared when you “simplified” things.
Thinking in terms of render snapshots makes the issue click into place. Huge thanks to the PrimeReact maintainers for quick feedback and collaboration, and to the original reporter for the excellent reproduction.