DEV Community

Rohan Satkar | Coderxrohan
Rohan Satkar | Coderxrohan

Posted on

đź§  The React Bug That Only Appears When Your Code Is Too Fast

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:

  1. Add 3 rows to the table
  2. Click the button in row 1 → logs 1
  3. Click row 2 → logs 2
  4. 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
);
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

After

cellMemoProps = { rowIndex }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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)