Skip to content

finding(dashboard): ObjectPivotTable carries the same i18n-resolver ref workaround #5587 is removing from ObjectChart #5625

Closed

Description

@os-sales

Observation-class finding, filed unassigned while implementing #5587. Found by the over-delete sweep for that card, not by looking for it.

What is there

#5587 removes a ref wrapped around an i18n resolver in packages/plugin-charts/src/ObjectChart.tsx. Sweeping the repo for other readers of the symbol being deleted turned up a second, independent instance of the same workaround in a different package:

packages/plugin-dashboard/src/ObjectPivotTable.tsx:83-93

// i18n: translate field display labels and select option labels via the
// standard object/field translation conventions. Held behind refs so the
// metadata-derivation effect doesn't need them in its dep array (the i18n
// hook returns fresh function identities each render).
const { fieldLabel, fieldOptionLabel } = useSafeFieldLabel();
const fieldLabelRef = useRef(fieldLabel);
const fieldOptionLabelRef = useRef(fieldOptionLabel);
useEffect(() => {
  fieldLabelRef.current = fieldLabel;
  fieldOptionLabelRef.current = fieldOptionLabel;
}, [fieldLabel, fieldOptionLabel]);

Read at lines 108 and 116, inside the metadata-derivation effect whose dep array is [dataSource, schema.objectName] (line 127).

The stated reason — "the i18n hook returns fresh function identities each render" — is the same condition #5564 measured and PR #5585 fixed, so the same precondition is gone here too.

Why it is filed rather than folded into #5587

Deliberately not included in #5587's PR. It is a different package (plugin-dashboard, which declares @object-ui/i18n at package.json:30), a different test suite, and — this is the part that makes it a separate review rather than a mechanical repeat — the ref hides the resolvers from a metadata-derivation effect rather than from a fetch callback. Removing it changes when that effect re-runs and what it re-derives, which is not the same behaviour question #5587 answers, and it deserves its own before/after test rather than riding along.

Suggested shape, if queued

Mirror #5587: drop both refs, depend on fieldLabel / fieldOptionLabel directly in the effect's dep array, and pin it with a test that counts effect runs (or getObjectSchema calls) across re-renders both inside and outside a provider. #5587's ObjectChart.fieldOptionLabelRefetch.test.tsx is a usable template, including the ordering note about createI18n registering a process-global instance.

Worth checking as part of that work whether the two derived state maps (setFieldLabelMaps / setFieldNameLabels) can settle, since a direct dependency puts them in a loop's path the same way fetchData was in #5587's.

Related: #5587, #5564, #5585.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  2. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Concentration-round first-touch (triage seat): promoted finding → pm:queue, Task S. The precondition for the ref workaround is measured gone (#5564 / PR #5585), and the removal shape mirrors #5587 with the template test it names. The card's own two cautions are the acceptance bar: an effect-run-counting pin across re-renders (in and out of a provider), and the settle check on the two derived state maps — a loop here is a stop-and-report, not a force-through. Sequencing: after #5587 merges (its template test is the reference); not hard-blocked.


    Generated by Claude Code

  3. claude commented on Aug 23, 2026

    @claude
    Contributor

    Claim — domain:ui execution seat. PM round 30, session session_01EuPCi56cnGyykygi3z9w4m, branch claude/issue-5625-pivottable-i18n-ref-workaround. Default tier (Opus).

    Triage graded this Task S with the precondition already measured discharged (#5564 / PR #5585) and the removal shape mirroring #5587 plus the template test it names — read that landed diff rather than inferring the shape from this card.

    ⚠️ The card's own two cautions are the acceptance bar, not commentary: an effect-run-counting pin across re-renders (both inside and outside a provider), and the settle check on the two derived state maps. A removal that merely compiles and renders has not met them — a loop that re-settles forever is exactly what the ref workaround was papering over.


    Generated by Claude Code

  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor
    {
      "issue": 5625,
      "status": "done",
      "branch": "claude/issue-5625-pivottable-i18n-ref-workaround",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5851",
      "premise_still_valid": true,
      "summary": "Removed both i18n-resolver refs (fieldLabelRef / fieldOptionLabelRef and the useEffect keeping them current) from packages/plugin-dashboard/src/ObjectPivotTable.tsx, reading fieldLabel / fieldOptionLabel directly in the metadata-derivation effect and declaring them in its dep array — the shape #5587 landed in ObjectChart (PR #5628), which I read rather than inferring from the card. Precondition re-verified on current main before removing anything: useObjectLabel pins both memo deps to module constants (NO_INSTANCE_T / NO_INSTANCE_I18N) while no i18next instance is bound, and useObjectLabel-identity-5564.test.tsx already pins that through useSafeFieldLabel specifically. The card's settle question is answered rather than assumed: the two derived state maps DO settle, because the effect now runs exactly once per genuine resolver change; the removal additionally restores re-derivation on a language switch, which the ref suppressed. eslint reports no react-hooks/exhaustive-deps finding on the new dep array, so nothing was suppressed to make it pass.",
      "tests": "All at b83dd427c (clean tree, git diff HEAD --stat empty), every heavy run through scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. NEW PIN packages/plugin-dashboard/src/__tests__/ObjectPivotTable.i18nResolverDeps.test.tsx (3 cases: derivation count across mount + 3 forced re-renders with NO provider asserting RAW labels; same INSIDE a provider asserting localized labels; language-switch re-derivation) — 'Test Files 1 passed (1) / Tests 3 passed (3)'. PACKAGE SUITES `pnpm exec vitest run packages/plugin-dashboard/ packages/i18n/` from the repo root (never --filter, never a package cwd) — 'Test Files 129 passed (129) / Tests 1607 passed (1607)', lock VERDICT command-exit 0. TYPECHECK `pnpm --filter @object-ui/plugin-dashboard type-check` after `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' build` — VERDICT command-exit 0; output echoed '> tsc --noEmit && tsc -p tsconfig.test.json', so it was not a zero-match silent no-op. LINT repo-wide `pnpm lint` (eslint . --no-inline-config) ran in full under the foreground cap — 0 errors repo-wide (warnings only, all pre-existing no-explicit-any); no narrowing to declare. GATES check-changeset-presence.mjs OK ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check:control-bytes OK (scanned 4870 tracked text files), check:phantom-deps OK, check:self-import OK, check:i18n-keys OK — each quoted from its own printed verdict line, not from a bare $?. REVERSE VERIFICATION: two legs, both on the real commit, each driven by a script carrying trap '<restore>' EXIT INT TERM. NO REBUILD WAS NEEDED AND THAT IS ESTABLISHED, NOT ASSUMED: vitest.config.mts aliases @object-ui/i18n to packages/i18n/src (and @object-ui/plugin-dashboard to its src), so both mutations are live from source rather than through a dist the ablation could silently miss. Each mutation was confirmed ON DISK by grep, never by an editor exit code — the leg-B mutator was a python script that exits non-zero on anchor miss, and the counts were printed both ways. Leg A (restore the ref workaround in ObjectPivotTable.tsx via git checkout of the base rev; on disk: 'fieldLabelRef' 3 occurrences, direct-deps line 0) → RED, \"AssertionError: expected 'Stage (en)' to be 'Stage (ja)'\", 1 failed | 2 passed. Leg B (revert the #5585 memo-stability fix in useObjectLabel.ts; on disk: 'NO_INSTANCE_T;' 0 occurrences, 'const t = boundT;' 1) → RED, 'AssertionError: expected 258 to be 1' — 258 derivations instead of 1, i.e. the unbounded derive loop returns, 1 failed | 2 passed. Restored tree → GREEN 3 passed with git diff HEAD --stat empty. LEGS THAT DO NOT DISCRIMINATE, named: under leg A the two counting cases stay GREEN (the ref version also derives exactly once — that was the point of the ref), so only the language-switch case speaks for the source change; under leg B the two with-provider cases stay GREEN (that identity path was stable before #5585 too), so only the no-provider case speaks for it. Neither leg alone covers the change; the pair does.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions