payloadcms / payloadcms/payload

List view relationship cells stuck on "<No Reference>": useIntersect keeps the first IntersectionObserver entry instead of the last

Open
#18,108 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: ui Bug
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

In list views, a relationship (or upload) column cell can get permanently stuck on the <No Reference> placeholder even though the relationship is set and the API returns the related document. Which rows are affected looks random, and it comes back after reloading or re-sorting, so it is easy to mistake for a data problem.

Root cause is in useIntersect (packages/ui/src/hooks/useIntersect.ts):

new window.IntersectionObserver(([ent]) => updateEntry(ent), { root, rootMargin, threshold })

IntersectionObserver delivers its entries oldest-first, and a single callback invocation can contain several entries for the same target when the target crossed the threshold more than once between deliveries. This routinely happens while the admin panel is hydrating (layout shifts) or when the user scrolls during load. The hook destructures the first entry, so it keeps the stale "not intersecting" record and drops the current one. Since the element then stays visible, no further callback fires and the hook is stuck with the outdated entry.

RelationshipCell (packages/ui/src/elements/Table/DefaultCell/fields/Relationship/index.tsx) requests the related document's title only when entry.boundingClientRect.top < window.innerHeight. With the stale entry that request is never made, values stays empty and the cell renders t('general:noLabel'), i.e. <No Reference>. Note this is the "not requested yet" placeholder, not the "document not found" state (that one renders Untitled - ID: n), which is what makes the symptom confusing.

Tooltip and RenderIfInViewport use the same hook and are exposed to the same stale-entry problem.

The hook is identical on main and 3.x; the line has not changed since 2024.

Fix: keep the latest entry — updateEntry(entries[entries.length - 1]). I have a branch with this one-line change and will open a PR referencing this issue.

Link to the code that reproduces this issue

https://github.com/payloadcms/payload/blob/a58ad4595/packages/ui/src/hooks/useIntersect.ts#L22

Reproduction Steps

The natural reproduction depends on timing (a list longer than the viewport, then scroll while the page is still hydrating), so here is a deterministic one that works on any list view with a relationship column — e.g. the repo's own pnpm dev fields-relationship suite, or any project with a collection whose admin.defaultColumns includes a relationship field and at least ~20 documents.

  1. Open a list view with a relationship column and open DevTools → Console.

  2. Wrap IntersectionObserver so the hook's callbacks and observed targets can be reached:

    const Orig = window.IntersectionObserver
    window.__obs = []
    window.IntersectionObserver = class extends Orig {
      constructor(cb, opts) {
        super(cb, opts)
        this.__cb = cb
        this.__targets = new Set()
        window.__obs.push(this)
      }
      observe(t) { this.__targets.add(t); return super.observe(t) }
      disconnect() { this.__targets.clear(); return super.disconnect() }
    }
    
  3. Remount the table via client-side navigation: click another collection in the sidebar, then come back to the list. The new cells now create their observers through the wrapped class.

  4. Deliver a realistic batched callback — "not intersecting" followed by "intersecting" — to every relationship cell:

    for (const o of window.__obs) {
      for (const t of o.__targets) {
        if (!t.classList.contains('relationship-cell')) continue
        const mk = (isIntersecting, top) => ({ isIntersecting, boundingClientRect: { top }, target: t, time: performance.now() })
        o.__cb([mk(false, 1200), mk(true, 500)], o)
      }
    }
    

Expected: cells show Loading... and then the related documents' titles; a GET /api/<collection>?depth=0&draft=true&limit=250&where[id][in]=… request is made.

Actual: every cell stays on <No Reference> and no request is made. Delivering a single [mk(true, 500)] entry instead makes the same cells load correctly, which isolates the problem to the first-vs-last entry handling.

With the one-line fix applied (entries[entries.length - 1]), step 4 loads all cells.

Observed in the wild on a Postgres project with 21 redirects whose to.reference column pointed at posts: after a reload, rows visible at first paint loaded, rows scrolled into view during hydration stayed on <No Reference> until a full reload with a different viewport/sort order.

Which area(s) are affected?

area: ui

Environment Info
Binaries:
  Node: 22.18.0
  pnpm: 11.25.0
Relevant Packages:
  payload: 3.88.0
  next: 16.3.4
  @payloadcms/db-postgres: 3.88.0
  @payloadcms/ui: 3.88.0
  react: 19.2.8
  react-dom: 19.2.8
Operating System:
  Platform: darwin (macOS 15, arm64)
Browser: Chrome (stable)
Also verified against main @ a58ad4595 (same code in packages/ui/src/hooks/useIntersect.ts).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/ui/src/hooks/useIntersect.ts, then inspect the listed RelationshipCell, Tooltip, and RenderIfInViewport consumers. Reproduce the issue with the provided batched IntersectionObserver callback or the fields-relationship suite. Done means the latest entry is used and relationship cells load their document titles instead of remaining on .

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
1/5
Estimated time
Under an hour
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.