Skip to content

Key UIData/UIRepeat per-row EVH state by clientId, not row index - #1034

Merged
tandraschko merged 1 commit into
apache:mainfrom
BalusC:fix-nested-row-state-keying
Jul 30, 2026
Merged

Key UIData/UIRepeat per-row EVH state by clientId, not row index#1034
tandraschko merged 1 commit into
apache:mainfrom
BalusC:fix-nested-row-state-keying

Conversation

@BalusC

@BalusC BalusC commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

A UIData or UIRepeat nested inside another iterating component loses the submitted values of every enclosing row but the last — silently: no conversion, no validation, no model update, no message.

Reproduced on a 5×10 grid (outer table over 5 groups, inner over 10 rows, 3 text inputs per row) by submitting a marker into every input and counting how many the response echoes back:

scenario before after
h:dataTable in h:dataTable 30/150 150/150
ui:repeat in ui:repeat 30/150 150/150
composite in h:dataTable 30/150 150/150
flat table (control) 105/105 105/105

The 30 that survived were exactly the last outer row.

Cause

Regression from 6336caf ([perf] optimize UIData/UIRepeat state saving), which changed the per-row EditableValueHolder state map from Map<String, …> keyed by getContainerClientId(context) to Map<Integer, …> keyed by the bare row index.

A nested iterating component is one instance reused across the enclosing rows, so the bare index makes every enclosing row map to the same key. getContainerClientId() is the clientId plus the current row index, carrying both dimensions; the index key kept only one. _rowDeltaStates and _rowTransientStates were never changed and still key on the clientId.

Fix

Key _rowStates on getContainerClientId() again in both components. The precollected iteration lists, the positional EditableValueHolderState list and its deferred allocation are untouched — only the key type and the two key expressions change.

The optimisation is not being asked back. Measured against current main with only UIData/UIRepeat reverted to their state before 6336caf, on the Mojarra test/perf suite (Tomcat, 32 scenarios, two alternating rounds):

pre-rewrite this PR Δ
whole suite 78.23 s 76.36 s −2.4%
APPLY_REQUEST_VALUES 3.43 s 3.09 s −10.0%
UPDATE_MODEL_VALUES 4.79 s 4.41 s −7.9%

Tests

UIDataTest.testNestedTableRowStateIsScopedPerOuterRow and UIRepeatTest.testNestedRepeatRowStateIsScopedPerOuterRow: a 2×2 nested grid, a distinct value per cell, iterate away and back, assert each cell kept its own. Both fail without the fix (expected: <v00> but was: <v10>) and pass with it. Full api + impl suite green, 1651 tests.

Notes

🤖 Generated with Claude Code (Opus 5)

A nested iterating component is a single instance reused across the
enclosing component's rows, so keying its per-row EditableValueHolder
state by the bare row index made every enclosing row map to the same
key and overwrite the previous one. Only the last enclosing row's
submitted values survived: the rest were dropped silently, without
conversion, validation, model update or a message.

Key on getContainerClientId() instead, which is this component's
clientId plus its current row index and therefore carries the
enclosing rows' indices too. That is what _rowDeltaStates and
_rowTransientStates key on, and what _rowStates itself keyed on
before 6336caf.

The positional EditableValueHolderState list, its deferred allocation
and the flat iteration lists are unchanged; only the map key differs.

Mojarra hit the same defect and resolved it the same way, see
eclipse-ee4j/mojarra#5874

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tandraschko

Copy link
Copy Markdown
Member

Thanks!

@tandraschko
tandraschko merged commit c65bda2 into apache:main Jul 30, 2026
4 checks passed
tandraschko pushed a commit that referenced this pull request Jul 30, 2026
A nested iterating component is a single instance reused across the
enclosing component's rows, so keying its per-row EditableValueHolder
state by the bare row index made every enclosing row map to the same
key and overwrite the previous one. Only the last enclosing row's
submitted values survived: the rest were dropped silently, without
conversion, validation, model update or a message.

Key on getContainerClientId() instead, which is this component's
clientId plus its current row index and therefore carries the
enclosing rows' indices too. That is what _rowDeltaStates and
_rowTransientStates key on, and what _rowStates itself keyed on
before 6336caf.

The positional EditableValueHolderState list, its deferred allocation
and the flat iteration lists are unchanged; only the map key differs.

Mojarra hit the same defect and resolved it the same way, see
eclipse-ee4j/mojarra#5874

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c65bda2)

# Conflicts:
#	api/src/main/java/jakarta/faces/component/UIData.java
@tandraschko

Copy link
Copy Markdown
Member

ported it to 4.1

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants