Skip to main content
Experimental. The timeScroll, zoom, onVisibleRangeChange, and onReachStart APIs may still change. The data-ownership pattern in this guide is intentionally transport-agnostic.
LiveChart does not open HTTP or websocket connections. It tells your data layer what is visible and when older history is needed; your app fetches or subscribes with its existing client, then writes sorted rows into Reanimated SharedValues. This is similar to a TradingView-style getBars / subscribeBars datafeed, but the adapter belongs to your application. LiveChart stays compatible with fetch, TanStack Query, Apollo, generated SDKs, NATS, or any other transport.
Runnable example: app/demo/loading-data-on-scroll.tsx uses a simulated paginated API and live subscription. Pan left repeatedly to watch pages prepend.

The two demand callbacks

  • onVisibleRangeChange reports { startSec, endSec, following } about once per second. Store the latest range so the loader knows how much time the next request must cover.
  • onReachStart fires once when the visible left edge comes within one window-width of the oldest retained row. It re-arms after the retained edge moves far enough into the past.
One fetched page should cover at least one visible window. If your backend can return sparse or empty pages, keep following its cursor until the retained edge reaches the requested time. Do not fire several page requests concurrently.

1. Define an optional app-level adapter

An interface can make several chart screens consistent without making networking part of LiveChart itself:
Keep cursors opaque. A cursor should encode the older page boundary and query scope; do not derive a new cursor from the oldest row because sparse time buckets can otherwise make pagination stall. Here is a minimal fetch + WebSocket implementation of that interface. Adapt the response fields at this boundary so every chart row is already numeric and uses unix seconds:
Keep the adapter instance stable—define it at module scope as above or wrap it in useMemo. Recreating source every render intentionally resets the history controller.

2. Normalize, sort, and deduplicate pages

LiveChart expects timestamps in unix seconds and rows in ascending order. Page boundaries and a simultaneous live update can overlap, so deduplicate by timestamp every time a page is merged.
Using .set(mergedRows) for a fetched page is appropriate: page loads are infrequent and replacing the array publishes one coherent snapshot. Use .modify(...) for high-frequency live appends so the growing array is not cloned across the JS/UI boundary every tick.

3. Fetch the initial window and older pages

The loader below provides the important production guards:
  • one history drain at a time;
  • abort and reset when the symbol or resolution changes;
  • follow cursors through sparse pages until the requested time is covered;
  • publish each successful page immediately, so a later failed page does not discard progress;
  • stop when the server is exhausted or the client retention cap prevents the oldest edge moving.
Wire the controller directly to the current chart props:
If a page fails while the user remains near the left edge, onReachStart will not spin retries—it is deliberately edge-triggered. Show a retry affordance that calls loadOlderHistory() again, or retry through your query client with its normal backoff policy.

4. Subscribe to live line updates

Subscribe after the initial request starts and unsubscribe when the chart identity changes. Replace an update at the same timestamp; append a newer update; ignore older out-of-order ticks and let the next REST page reconcile them.
The websocket does not need to be owned by the screen. A shared SDK or connection manager can multiplex one socket across charts; the screen only needs a scoped subscribe/unsubscribe method.

Candlestick bars: committed history plus one live bar

For candlesticks, pass closed buckets through candles and the currently forming bucket through liveCandle. Do not put the same bucket in both arrays.
Merge REST candle pages with the same mergeByTime helper before calling candles.set(...). On a bucket rollover, either receive an explicit closed update as above or commit the previous liveCandle before installing the new open bucket.

Using an infinite-query cache

If your SDK already exposes an infinite query, let it own request deduplication, retry, and cache lifetime. Flatten pages into chronological order and publish them when the cache changes:
Configure the query to prepend older pages (or sort after flattening), include symbol, resolution, and timeframe in its cache key, and remove or expire inactive large histories according to your app’s memory budget.

Production checklist

  • Send and store timestamps in unix seconds; convert millisecond APIs at the boundary.
  • Keep committed rows ascending by time and deduplicate page overlaps.
  • Scope cursors and cache keys by symbol, resolution, metric, and chart mode.
  • Seed about two visible windows, then fetch one additional window per scroll demand.
  • Guard against overlapping requests and abort stale chart identities.
  • Let retries happen through an explicit demand or visible retry control—not a render loop.
  • Set a retention cap and stop backfilling when that cap prevents the oldest edge from moving.
  • Keep closed candles separate from the single in-progress liveCandle.
  • Unsubscribe realtime listeners on unmount or identity change.
  • Log page failures, cursor stalls, and retention-cap stops with the chart identity attached.
See Time-scroll for gesture behavior and the LiveChart API reference for the callback contracts.