Skip to content

fix(table): use updateData with targeted reformat for row updates#3982

Merged
Befkadu1 merged 2 commits intomainfrom
fix/table-replace-data-on-row-updates
Apr 9, 2026
Merged

fix(table): use updateData with targeted reformat for row updates#3982
Befkadu1 merged 2 commits intomainfrom
fix/table-replace-data-on-row-updates

Conversation

@Befkadu1
Copy link
Copy Markdown
Contributor

@Befkadu1 Befkadu1 commented Mar 27, 2026

Summary

  • Use updateData + row.reformat() on changed rows instead of replaceData when the table detects row content changes (same IDs, same order, but different field values)
  • Patch missing fields as undefined before updating so Tabulator clears stale values from its internal data model
  • Reserve replaceData only for structural changes (different IDs or reordered rows)

Why this change?

Tabulator's updateData() performs a partial merge: it matches rows by their index field and patches only the supplied properties. This causes two problems:

  1. Deleted properties are not removed — Internally, row.updateData only iterates over fields present in the new data (for (let attrname in updatedData)). If a field was dropped from the API response (e.g. due to ACL filtering), updateData preserves the old value because it never visits that cell.

  2. Custom components may not re-renderupdateData guards cell re-renders with cell.getValue() !== value (strict comparison). Custom component cells rendered via the ElementPool may not reliably re-render through this path alone.

How we solve both problems without replaceData

  1. fillMissingFields — Before calling updateData, we compare old and new row data and explicitly set any missing fields to undefined. This ensures updateData visits those cells and clears the stale values from Tabulator's internal data model.

  2. row.reformat() — After updateData resolves (data model is now correct), we call row.reformat() only on rows where content actually changed. This forces Tabulator to re-run all formatters for that row, including custom component formatters that read from the now-updated row data.

Example scenario

A table displays helpdesk tickets with a conversation.earliestunread column. When a user marks a conversation as read, the parent component fetches updated data and passes it to the table. The row IDs and order haven't changed — only the field value has. With the old code, updateData alone either missed the removed field or didn't re-render the custom component icon. With this fix, fillMissingFields patches the dropped field, updateData clears it in the data model, and reformat() forces the icon component to re-render.

Performance: why replaceData doesn't scale

The previous approach used replaceData for any row content change. replaceData tears down all rows and rebuilds them — every cell, every formatter, every custom component. Consider a table with 500 rows and 100 columns where a user edits a single cell:

Approach Cells processed Custom component re-renders (10 cols) Estimated cost
replaceData (all 500 rows) 50,000 5,000 Seconds of blocking work
updateData + reformat (2 changed rows) 200 20 <50ms

That's 250x less work for the common case of a few rows changing.

Even with pagination (100 rows/page), replaceData still processes 10,000 cells per update. For tables without pagination (e.g. embedded tables, dashboards), the cost is unbounded.

Cost breakdown of replaceData at scale

Each cell rebuild involves:

  • Formatter invocation
  • For custom component columns: pool.get() + Object.assign(element, props) + ResizeObserver setup
  • DOM element creation/replacement

At 500 rows × 100 columns, that's 50,000 formatter calls, up to 15,000 ResizeObserver setups (for 30 custom component columns), and a full DOM reflow. This easily exceeds a frame budget and causes visible jank.

When replaceData is still used

replaceData + pool.releaseAll() is reserved for structural changes where surgical updates aren't possible:

  • Row IDs changed (different dataset)
  • Row order changed (reordered)

In these cases, Tabulator can't match old rows to new rows by ID, so a full rebuild is the only option.

Linked issue

Closes Lundalogik/crm-client#801

Test plan

  • Verify that relational fields (e.g. conversation.earliestunread) update correctly after changes made through related cards
  • Verify that custom cell components (formatters) display the correct updated values
  • Verify that fields dropped from the API response are cleared (not stale) — covered by fillMissingFields unit test
  • Verify that adding/removing/reordering rows still uses replaceData (covered by existing tests)
  • Smoke-test with a paginated table to confirm acceptable render performance

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • More reliable incremental table updates: async row updates now trigger reformatting after apply, selection is preserved, and fields missing from new rows are retained as explicit undefined to avoid data loss.
  • Refactor

    • Streamlined table update flow for more consistent, predictable handling of row changes and reduced replacement churn.
  • Tests

    • Tests updated/added to cover async update behavior, row reformatting, and missing-field handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The table component's @Watch('data') now decides replacement based on id/order checks; for in-place updates it fills missing fields (fillMissingFields) and calls tabulator.updateData(patched) asynchronously. After the promise resolves it runs reformatChangedRows(...) and setSelection(). shouldReplaceData was removed.

Changes

Cohort / File(s) Summary
Table component
src/components/table/table.tsx
Rewrote @Watch('data') update flow: replacement is decided via id/order checks; in-place updates compute patched = fillMissingFields(newData, oldData) (missing keys filled with undefined) and call tabulator.updateData(patched) asynchronously, then run reformatChangedRows(newData, oldData) and setSelection() after resolution. Removed shouldReplaceData; added fillMissingFields and reformatChangedRows.
Tests
src/components/table/table.spec.ts
Updated mocks to make tabulator.updateData async and added tabulator.getRow(...).reformat. Converted row-change tests to async, asserting updateData receives patched rows (missing fields as undefined), getRow(...).reformat() is called for changed rows, replaceData/updateOrAddData not called, and pool.releaseAll not invoked for incremental updates.

Sequence Diagram(s)

sequenceDiagram
    participant Component as Component
    participant Pool as Pool
    participant Tabulator as Tabulator

    Component->>Component: receive newData, oldData
    alt ids/order differ -> replace
        Component->>Pool: releaseAll()
        Component->>Tabulator: replaceData(newData)
    else same ids/order -> incremental
        Component->>Component: patched = fillMissingFields(newData, oldData)
        Component->>Tabulator: updateData(patched)
        Note right of Tabulator: updateData returns Promise
        Tabulator-->>Component: Promise resolves
        Component->>Tabulator: reformatChangedRows(newData, oldData) -> getRow(...).reformat()
        Component->>Component: setSelection()
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

bug, usability

Suggested reviewers

  • jgroth
  • FredrikWallstrom
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: replacing blanket replaceData() calls with a targeted updateData() + reformat() approach for row updates, which directly aligns with the changeset's core objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/table-replace-data-on-row-updates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-3982/

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/table/table.tsx (1)

333-346: ⚠️ Potential issue | 🟠 Major

Await replaceData() before applying selection state, and merge duplicate branches.

setSelection() currently runs immediately after replaceData(newData). Although the code is wrapped in setTimeout, this only defers execution—it doesn't await the Promise. If replaceData completes asynchronously (which it does), selection will be applied before the data replacement and render finish, risking stale row references.

Additionally, the shouldReplace and hasRowUpdates branches are identical and should be merged.

Proposed fix
-            if (shouldReplace) {
-                this.pool.releaseAll();
-                this.tabulator.replaceData(newData);
-                this.setSelection();
-
-                return;
-            }
-
-            if (hasRowUpdates) {
-                this.pool.releaseAll();
-                this.tabulator.replaceData(newData);
-                this.setSelection();
-
-                return;
-            }
+            if (shouldReplace || hasRowUpdates) {
+                this.pool.releaseAll();
+                void this.tabulator
+                    .replaceData(newData)
+                    .then(() => this.setSelection())
+                    .catch(() => undefined);
+
+                return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 333 - 346, The two identical
branches using shouldReplace and hasRowUpdates should be merged and made to
await the asynchronous replace operation before calling setSelection: change the
conditional to if (shouldReplace || hasRowUpdates) then call
this.pool.releaseAll(); await this.tabulator.replaceData(newData); and only
after that call this.setSelection(); ensure the enclosing method is async (or
return the Promise from replaceData().then(...)) and remove the current
setTimeout-based defer so selection is applied after replaceData completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/components/table/table.tsx`:
- Around line 333-346: The two identical branches using shouldReplace and
hasRowUpdates should be merged and made to await the asynchronous replace
operation before calling setSelection: change the conditional to if
(shouldReplace || hasRowUpdates) then call this.pool.releaseAll(); await
this.tabulator.replaceData(newData); and only after that call
this.setSelection(); ensure the enclosing method is async (or return the Promise
from replaceData().then(...)) and remove the current setTimeout-based defer so
selection is applied after replaceData completes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f8fc1b2c-41d6-4709-88e3-2464dbb15e70

📥 Commits

Reviewing files that changed from the base of the PR and between 2e359ba and b7acf65.

📒 Files selected for processing (1)
  • src/components/table/table.tsx

@Befkadu1 Befkadu1 force-pushed the fix/table-replace-data-on-row-updates branch from b7acf65 to 58ae11f Compare March 27, 2026 12:04
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/table.tsx`:
- Around line 341-344: The code races selection updates because
tabulator.replaceData(...) returns a Promise but setSelection() is called
immediately; update both branches (the shouldReplace branch and the
hasRowUpdates branch around replaceData, pool.releaseAll and setSelection) to
await the replaceData promise (or use .then()) before calling
this.setSelection(), ensuring you call this.pool.releaseAll() at the same
correct point and reapply selection only after this.tabulator.replaceData(...)
has resolved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8bc6b8ed-45d6-464e-9afa-d1921c973d8e

📥 Commits

Reviewing files that changed from the base of the PR and between b7acf65 and 58ae11f.

📒 Files selected for processing (1)
  • src/components/table/table.tsx

@Befkadu1 Befkadu1 force-pushed the fix/table-replace-data-on-row-updates branch from f8b1de1 to c2a3b82 Compare March 27, 2026 12:33
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/table.spec.ts`:
- Around line 32-34: Add an assertion that the mocked pool.releaseAll() is
invoked when replaceData() runs: in the test where (component as any).pool = {
releaseAll: vi.fn() } is set and replaceData() is called, assert that (component
as any).pool.releaseAll was called (e.g., using toHaveBeenCalled()) to verify
the pool cleanup behavior; locate the test that calls component.replaceData and
add the expect((component as any).pool.releaseAll).toHaveBeenCalled() assertion
after the call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba0771be-83f3-4135-a801-ce6f7786d1d5

📥 Commits

Reviewing files that changed from the base of the PR and between 58ae11f and c2a3b82.

📒 Files selected for processing (2)
  • src/components/table/table.spec.ts
  • src/components/table/table.tsx

Comment on lines +32 to +34
(component as any).pool = {
releaseAll: vi.fn(),
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider asserting pool.releaseAll() is called.

The mock is set up but no test verifies that pool.releaseAll() is invoked before replaceData(). Adding an assertion would ensure the pool cleanup behavior is tested.

Example assertion to add in relevant tests
expect((component as any).pool.releaseAll).toHaveBeenCalled();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.spec.ts` around lines 32 - 34, Add an assertion
that the mocked pool.releaseAll() is invoked when replaceData() runs: in the
test where (component as any).pool = { releaseAll: vi.fn() } is set and
replaceData() is called, assert that (component as any).pool.releaseAll was
called (e.g., using toHaveBeenCalled()) to verify the pool cleanup behavior;
locate the test that calls component.replaceData and add the expect((component
as any).pool.releaseAll).toHaveBeenCalled() assertion after the call.

const tabulator = (component as any).tabulator;
expect(tabulator.replaceData).not.toHaveBeenCalled();
expect(tabulator.updateData).toHaveBeenCalledWith(newData);
expect(tabulator.replaceData).toHaveBeenCalledWith(newData);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since releasing the pool is a key part of the fix (to avoid stale custom component references), consider adding an assertion here:

expect((component as any).pool.releaseAll).toHaveBeenCalled();

This would verify the pool cleanup behavior that the PR description highlights.

@Befkadu1 Befkadu1 changed the title fix(table): use replaceData instead of updateData for row updates fix(table): use updateData with targeted reformat for row updates Apr 8, 2026
@Befkadu1 Befkadu1 force-pushed the fix/table-replace-data-on-row-updates branch from c5c4e31 to b27462c Compare April 8, 2026 14:05
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/components/table/table.tsx (1)

334-347: ⚠️ Potential issue | 🟠 Major

Don't let post-update work outlive the Tabulator instance that started it.

Line 337 still reapplies selection before replaceData() has finished, and Lines 344-347 reuse this.tabulator without checking whether disconnectedCallback() or init() has already swapped it out. That can restore selection too early or make reformatChangedRows() hit a dead/new table.

For Tabulator 6.x, do `replaceData()` and `updateData()` return Promises that resolve after the table data has been updated, and should follow-up work run from that promise chain?
Suggested fix
         setTimeout(() => {
             if (!this.tabulator || !this.initialized) {
                 return;
             }

+            const tabulator = this.tabulator;
+
             if (shouldReplace) {
                 this.pool.releaseAll();
-                this.tabulator.replaceData(newData);
-                this.setSelection();
+                tabulator.replaceData(newData).then(() => {
+                    if (this.destroyed || this.tabulator !== tabulator) {
+                        return;
+                    }
+
+                    this.setSelection();
+                });

                 return;
             }

             if (!areRowsEqual(newData, oldData)) {
                 const patched = this.fillMissingFields(newData, oldData);
-                this.tabulator.updateData(patched).then(() => {
+                tabulator.updateData(patched).then(() => {
+                    if (this.destroyed || this.tabulator !== tabulator) {
+                        return;
+                    }
+
                     this.reformatChangedRows(newData, oldData);
                     this.setSelection();
                 });

                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 334 - 347, The post-update work
currently runs without awaiting Tabulator's async completion and can act on a
different instance if disconnectedCallback()/init() swapped it; capture the
current table instance into a local variable (e.g., const tab = this.tabulator)
before calling replaceData() or updateData(), await/then the returned Promise
from tab.replaceData(...) and tab.updateData(...), and inside the then handler
first verify tab === this.tabulator (return early if not) before calling
this.setSelection() or this.reformatChangedRows(...); do the same
instance-identity guard for the patched = this.fillMissingFields(...) +
tab.updateData(patched).then(...) path and keep pool.releaseAll() where
appropriate. Ensure you reference replaceData, updateData, reformatChangedRows,
setSelection, fillMissingFields, pool.releaseAll, disconnectedCallback and init
when making the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/table/table.tsx`:
- Around line 334-347: The post-update work currently runs without awaiting
Tabulator's async completion and can act on a different instance if
disconnectedCallback()/init() swapped it; capture the current table instance
into a local variable (e.g., const tab = this.tabulator) before calling
replaceData() or updateData(), await/then the returned Promise from
tab.replaceData(...) and tab.updateData(...), and inside the then handler first
verify tab === this.tabulator (return early if not) before calling
this.setSelection() or this.reformatChangedRows(...); do the same
instance-identity guard for the patched = this.fillMissingFields(...) +
tab.updateData(patched).then(...) path and keep pool.releaseAll() where
appropriate. Ensure you reference replaceData, updateData, reformatChangedRows,
setSelection, fillMissingFields, pool.releaseAll, disconnectedCallback and init
when making the changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 227f37df-fff9-4af0-aef7-f23dd5d07454

📥 Commits

Reviewing files that changed from the base of the PR and between c2a3b82 and b27462c.

📒 Files selected for processing (2)
  • src/components/table/table.spec.ts
  • src/components/table/table.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/table/table.tsx (1)

334-340: ⚠️ Potential issue | 🟠 Major

Await replaceData() before reapplying selection.

replaceData() returns a Promise. Calling setSelection() immediately can race the row rebuild. This was addressed for the updateData path but the replaceData branch still calls them synchronously.

Suggested fix
         if (shouldReplace) {
             this.pool.releaseAll();
-            this.tabulator.replaceData(newData);
-            this.setSelection();
-
-            return;
+            this.tabulator.replaceData(newData).then(() => {
+                if (!this.tabulator) {
+                    return;
+                }
+
+                this.setSelection();
+            });
+            return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 334 - 340, The replaceData
branch currently calls this.tabulator.replaceData(newData) synchronously and
then this.setSelection(), which can race because replaceData() returns a
Promise; change the flow to await this.tabulator.replaceData(newData) (making
the enclosing method async if it isn't already), call this.pool.releaseAll() as
before, then call this.setSelection() only after the awaited replaceData
completes so selection is applied after rows are rebuilt (mirror the updateData
path behavior).
♻️ Duplicate comments (1)
src/components/table/table.spec.ts (1)

77-89: 🧹 Nitpick | 🔵 Trivial

Consider asserting pool.releaseAll() is called for structural changes.

This test verifies replaceData is called when IDs change, but doesn't assert that pool.releaseAll() is invoked before it. Adding this assertion would ensure the pool cleanup behavior is tested.

Suggested assertion
     const tabulator = (component as any).tabulator;
+    expect((component as any).pool.releaseAll).toHaveBeenCalled();
     expect(tabulator.replaceData).toHaveBeenCalledWith(newData);
     expect(tabulator.updateData).not.toHaveBeenCalled();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.spec.ts` around lines 77 - 89, The test should
also assert that the pool is cleared when structural changes occur: in the
'replaces data when the dataset changes' spec, mock or spy on the component's
pool.releaseAll and assert it was called (and called before invoking
tabulator.replaceData) when calling (component as any).updateData(newData,
oldData); keep references to the updateData method, the pool.releaseAll spy, and
tabulator.replaceData to ensure the test verifies pool cleanup happens prior to
replaceData.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/table.tsx`:
- Around line 454-466: The function reformatChangedRows currently declares
parameters on multiple lines and uses a traditional for loop; change its
signature to a single-line parameter list (reformatChangedRows(newData:
RowData[], oldData: RowData[])) and replace the indexed loop with a for-of loop
that iterates over newData (e.g., for (const newRow of newData) { ... }) while
finding the corresponding old row (by index or id) and comparing with isEqual;
keep using this.tabulator.getRow(newRow.id) and call row.reformat() when
different.

---

Outside diff comments:
In `@src/components/table/table.tsx`:
- Around line 334-340: The replaceData branch currently calls
this.tabulator.replaceData(newData) synchronously and then this.setSelection(),
which can race because replaceData() returns a Promise; change the flow to await
this.tabulator.replaceData(newData) (making the enclosing method async if it
isn't already), call this.pool.releaseAll() as before, then call
this.setSelection() only after the awaited replaceData completes so selection is
applied after rows are rebuilt (mirror the updateData path behavior).

---

Duplicate comments:
In `@src/components/table/table.spec.ts`:
- Around line 77-89: The test should also assert that the pool is cleared when
structural changes occur: in the 'replaces data when the dataset changes' spec,
mock or spy on the component's pool.releaseAll and assert it was called (and
called before invoking tabulator.replaceData) when calling (component as
any).updateData(newData, oldData); keep references to the updateData method, the
pool.releaseAll spy, and tabulator.replaceData to ensure the test verifies pool
cleanup happens prior to replaceData.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9bcd9fae-5355-4845-9992-5f66265495fd

📥 Commits

Reviewing files that changed from the base of the PR and between b27462c and eb26104.

📒 Files selected for processing (2)
  • src/components/table/table.spec.ts
  • src/components/table/table.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/components/table/table.tsx (1)

334-338: ⚠️ Potential issue | 🟠 Major

Await replaceData() before reapplying selection.

At Line 336, replaceData() is async; calling setSelection() at Line 337 can race table rebuild and lose selection state.

Suggested fix
             if (shouldReplace) {
                 this.pool.releaseAll();
-                this.tabulator.replaceData(newData);
-                this.setSelection();
+                this.tabulator.replaceData(newData).then(() => {
+                    if (!this.tabulator || this.destroyed) {
+                        return;
+                    }
+                    this.setSelection();
+                });
 
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 334 - 338, The call to
this.tabulator.replaceData(newData) is asynchronous and setSelection() is being
invoked immediately after, causing a race; update the surrounding method (the
function containing the shouldReplace branch) to be async and await
this.tabulator.replaceData(newData) before calling this.setSelection(), keeping
this.pool.releaseAll() before the await so the sequence is: releaseAll(), await
replaceData(...), then setSelection(); ensure any callers handle the method's
async signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/table.tsx`:
- Around line 456-457: The condition currently uses a truthy check
(!!newRow?.id) which excludes valid IDs like 0 or '' and prevents reformatting;
change the guard in the block that compares newRow to oldData (around newRow,
oldData, isEqual and this.tabulator.getRow) to explicitly check for
non-null/undefined IDs (e.g. newRow?.id != null) before calling
this.tabulator.getRow(newRow.id) so rows with ID 0 or empty-string are handled
correctly.
- Around line 344-351: The updateData() promise lacks a rejection handler;
update the call to this.tabulator.updateData(patched) to add a .catch(...) that
checks this.destroyed before doing anything, logs or ignores the error as
appropriate, and ensures the reconciliation steps
(this.reformatChangedRows(newData, oldData) and this.setSelection()) only run in
the successful .then branch. Keep the .then(...) block unchanged but append a
.catch(error => { if (!this.destroyed) { /* handle or log error */ } }) to
prevent unhandled promise rejections.

---

Duplicate comments:
In `@src/components/table/table.tsx`:
- Around line 334-338: The call to this.tabulator.replaceData(newData) is
asynchronous and setSelection() is being invoked immediately after, causing a
race; update the surrounding method (the function containing the shouldReplace
branch) to be async and await this.tabulator.replaceData(newData) before calling
this.setSelection(), keeping this.pool.releaseAll() before the await so the
sequence is: releaseAll(), await replaceData(...), then setSelection(); ensure
any callers handle the method's async signature.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a0349d9e-b3d8-4128-9d2f-23b6b583f11d

📥 Commits

Reviewing files that changed from the base of the PR and between eb26104 and cff202d.

📒 Files selected for processing (1)
  • src/components/table/table.tsx

Comment on lines +344 to +351
this.tabulator.updateData(patched).then(() => {
if (!this.tabulator) {
return;
}

this.reformatChangedRows(newData, oldData);
this.setSelection();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file and the specific lines mentioned
cd src/components/table && wc -l table.tsx && echo "---" && sed -n '340,360p' table.tsx

Repository: Lundalogik/lime-elements

Length of output: 680


🏁 Script executed:

# Also search for the tabulator.updateData pattern to see if it's used elsewhere
rg "updateData\(" src/components/table/table.tsx -B 2 -A 5

Repository: Lundalogik/lime-elements

Length of output: 939


🏁 Script executed:

# Check for the destroyed property definition and other promise handling
rg "destroyed" src/components/table/table.tsx -B 1 -A 1

Repository: Lundalogik/lime-elements

Length of output: 806


🏁 Script executed:

# Look for other .catch() patterns in the component to understand error handling strategy
rg "\.catch\(" src/components/table/table.tsx -B 2 -A 2

Repository: Lundalogik/lime-elements

Length of output: 50


🏁 Script executed:

# Let's see what updateOrAddData does and check the broader context
rg "updateOrAddData|\.then\(" src/components/table/table.tsx -B 1 -A 3

Repository: Lundalogik/lime-elements

Length of output: 432


🏁 Script executed:

# Look at the full method signature and context around line 344
sed -n '325,370p' src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 1346


🏁 Script executed:

# Check if there's any global error handling or error listeners in the component
rg "onerror|error|reject" src/components/table/table.tsx -i | head -20

Repository: Lundalogik/lime-elements

Length of output: 104


🏁 Script executed:

# Understand the tabulator library - search for its type definitions or imports
rg "import.*tabulator|from.*tabulator" src/components/table/table.tsx -i

Repository: Lundalogik/lime-elements

Length of output: 143


Add rejection handler to updateData() promise to prevent unhandled promise rejections.

The promise chain at line 344 lacks a rejection handler. If Tabulator's updateData() rejects, it will surface as an unhandled promise rejection, preventing the reconciliation logic from running. Add a .catch() handler to handle rejection gracefully, consistent with the component's existing pattern of checking the destroyed property before operations.

Suggested fix
-                this.tabulator.updateData(patched).then(() => {
-                    if (!this.tabulator) {
-                        return;
-                    }
-
-                    this.reformatChangedRows(newData, oldData);
-                    this.setSelection();
-                });
+                this.tabulator
+                    .updateData(patched)
+                    .then(() => {
+                        if (!this.tabulator) {
+                            return;
+                        }
+
+                        this.reformatChangedRows(newData, oldData);
+                        this.setSelection();
+                    })
+                    .catch(() => {
+                        if (!this.tabulator || this.destroyed) {
+                            return;
+                        }
+                    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 344 - 351, The updateData()
promise lacks a rejection handler; update the call to
this.tabulator.updateData(patched) to add a .catch(...) that checks
this.destroyed before doing anything, logs or ignores the error as appropriate,
and ensures the reconciliation steps (this.reformatChangedRows(newData, oldData)
and this.setSelection()) only run in the successful .then branch. Keep the
.then(...) block unchanged but append a .catch(error => { if (!this.destroyed) {
/* handle or log error */ } }) to prevent unhandled promise rejections.

Comment on lines +456 to +457
if (!!newRow?.id && !isEqual(newRow, oldData[i])) {
const row = this.tabulator.getRow(newRow.id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t use a truthy check for row IDs.

Line 456 uses !!newRow?.id, which skips valid IDs like 0 (and ''). Those rows will not be reformatted even when content changes.

Suggested fix
-            if (!!newRow?.id && !isEqual(newRow, oldData[i])) {
+            if (newRow?.id != null && !isEqual(newRow, oldData[i])) {
                 const row = this.tabulator.getRow(newRow.id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!!newRow?.id && !isEqual(newRow, oldData[i])) {
const row = this.tabulator.getRow(newRow.id);
if (newRow?.id != null && !isEqual(newRow, oldData[i])) {
const row = this.tabulator.getRow(newRow.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 456 - 457, The condition
currently uses a truthy check (!!newRow?.id) which excludes valid IDs like 0 or
'' and prevents reformatting; change the guard in the block that compares newRow
to oldData (around newRow, oldData, isEqual and this.tabulator.getRow) to
explicitly check for non-null/undefined IDs (e.g. newRow?.id != null) before
calling this.tabulator.getRow(newRow.id) so rows with ID 0 or empty-string are
handled correctly.

Befkadu1 and others added 2 commits April 9, 2026 12:28
Use updateData + row.reformat() on changed rows instead of replaceData
for row content changes. This reduces table update cost from O(N) to O(k)
where k is the number of changed rows.

- fillMissingFields patches fields dropped by the backend as undefined,
  so updateData clears them from Tabulator's internal data model
- After updateData resolves, reformatChangedRows calls row.reformat()
  only on rows where content actually differs, forcing custom component
  formatters to re-run with the updated data
- replaceData is still used when row IDs or order change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Expect updateData (not replaceData) for row content changes
- Assert pool.releaseAll is not called for content-only changes
- Add test for fillMissingFields patching dropped fields as undefined
- Mock updateData to return a Promise and getRow to return reformat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Befkadu1 Befkadu1 force-pushed the fix/table-replace-data-on-row-updates branch from cff202d to 331c2b4 Compare April 9, 2026 10:28
@Befkadu1 Befkadu1 enabled auto-merge (rebase) April 9, 2026 10:28
@Befkadu1 Befkadu1 merged commit 6892178 into main Apr 9, 2026
9 of 10 checks passed
@Befkadu1 Befkadu1 deleted the fix/table-replace-data-on-row-updates branch April 9, 2026 10:31
@lime-opensource
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 39.12.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants