Scrolling to a field that does not exist yet
A “Go to field” action looks like a coordinate lookup. It is a timing problem. The target may be hidden inside a collapsed group, below content that has not rendered, or inside a virtualized row that has no native view yet. Opening the group changes the heights of everything below it. A single scroll call can run before there is anything to measure.
Treat the action as a small protocol:
- Reveal the target’s path.
- Ask the list to mount the likely row.
- Measure the target in content coordinates.
- Scroll to that measured position.
- Wait for layout changes and correct the position.
- Retry a bounded number of times.
- Cancel callbacks from older requests.
The protocol has three clear owners. The renderer turns data into views, exposes stable target IDs and native refs, and reports layout changes. The list owns virtualization and coarse movement through its public scrolling API. The coordinator owns request state, ordering, retries, correction, and cancellation. A caller supplies intent, usually a stable field ID and an optional location hint.
Start with intent and an index
The caller should send a target ID, not a copy of the form or document. A renderer can build a small index as it creates rows. The index records enough information to find a likely section and item. It contains navigation data, not field values or rendered components.
type RowLocation = { sectionIndex: number; itemIndex: number }
type TargetIndex = Map<string, RowLocation>
function buildIndex(rows: Array<{ id: string; location: RowLocation }>) {
return new Map(rows.map(row => [row.id, row.location]))
}
The hint can be stale after filtering, insertion, or reordering. That is acceptable. It helps the list get near the target; the mounted view supplies the final position.
Reveal before measuring
First resolve the target’s most specific path. If the requested control sits inside a collapsed group, open each required group. If the target is on another screen or tab, move to that surface before looking for its ref. The coordinator requests these changes through renderer-owned actions. It does not reach into the renderer’s state or recreate its view tree.
Revealing triggers new rows, new heights, and new layout passes. Wait for at least one layout opportunity before measuring. If the target ref is still absent and the index has a row location, ask the list to bring that row near the viewport.
In React Native, SectionList is a virtualized list that renders a window around the viewport. Its scrollToLocation method accepts a section index and an item index. The call requests coarse positioning. It does not prove that the target control has mounted. A request outside the measured render window can fail and invoke onScrollToIndexFailed.
The failure callback includes an estimated average item length. Use that estimate for a rough offset, then retry after more rows have rendered:
type ScrollFailure = { index: number; averageItemLength: number }
function onScrollToIndexFailed(info: ScrollFailure) {
const roughOffset = info.index * info.averageItemLength
listRef.current?.scrollToOffset({ offset: roughOffset, animated: false })
scheduleRetry(currentSequence)
}
Keep the request pending while the list or target ref is temporarily unavailable. Stop after a fixed retry count. A removed item or invalid hint then becomes a visible navigation failure instead of an infinite loop.
Measure in the right coordinate system
A React Native ref is a handle to a rendered native-backed view. measureLayout measures one view relative to another and returns an asynchronous callback with local x, y, width, and height values. Use a ref for the target and a ref for the scroll content container:
function scrollToTarget(targetRef: ViewRef, contentRef: ViewRef) {
targetRef.current?.measureLayout(contentRef.current, (_x, y) => {
const destination = Math.max(0, y - topPadding)
scrollRef.current?.scrollTo({ y: destination, animated: true })
})
}
The local y value belongs to the content container. Screen coordinates also include the container’s position, insets, and current scroll offset. Mixing those coordinate systems produces an offset that looks plausible and lands in the wrong place. Subtract only the desired top padding, then clamp the result to zero.
Wait for convergence
The first measurement can become stale immediately. A group animation may finish. A validation message may appear. An image or asynchronous child may change its row height. The renderer should expose the content container’s size-change callback, such as React Native’s onContentSizeChange. While navigation is pending, the coordinator turns each relevant change into another settle-and-measure pass.
This is a bounded convergence loop. Measure, scroll, wait, and measure again. If the destination moved beyond a small threshold, correct it. Stop when the movement is small or the correction budget is exhausted:
settle(sequence)
repeat up to maxCorrections
wait for a layout event or short delay
if sequence is stale, return
destination = measure target relative to content
if destination is within threshold of current scroll, return
scroll to destination
The renderer reports facts about layout. The coordinator decides whether those facts require another correction. Keep the listener active only during pending navigation. Ordinary editing can resize content without starting measurement work.
Cancel stale callbacks
The user can choose another result while the first target is mounting. Timers, layout callbacks, and native measurement callbacks may still run. Clearing visible pending state does not cancel them.
Give each request a monotonically increasing sequence ID. Every asynchronous continuation captures its ID and checks it before it changes scroll state:
let currentSequence = 0
function beginNavigation(targetId: string) {
const sequence = ++currentSequence
void runNavigation(targetId, sequence)
}
function isCurrent(sequence: number) {
return sequence === currentSequence
}
Starting a new request makes the previous sequence stale. A stale retry, measurement, or correction returns silently. The same rule works when a caller stores an intent, changes screens, and lets the renderer consume it after focus returns.
Test timing and tradeoffs
Test the protocol’s states, not only its final scroll call:
- mounted target scrolls once
- missing ref succeeds after a retry
- virtualization failure rough-scrolls and retries
- local measurement applies padding and handles a non-zero scroll offset
- content-size change moves the target and triggers correction
- stable layout stops within the settle window
- correction and retry budgets stop work
- a newer sequence prevents an older callback from scrolling
The protocol adds a coordinator, refs, an index, timers, layout events, and tests. Those pieces expose the real costs of dynamic layout and put limits around them. The index finds a likely row. The list mounts it. The renderer exposes the view and its changes. The coordinator turns those facts into one ordered interaction.
The coordinate is the final output of the protocol. It becomes trustworthy only after reveal, mount, measure, scroll, retry, correction, and cancellation have all respected the current layout and the current request.