> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shortkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Item Replacement

> Replace part of a custom feed with replaceFeedItems, choose what the boundary is anchored to, and read the result.

`replaceFeedItems` replaces a trailing range of a custom feed queue. Items up to and including the boundary are preserved; everything after it is replaced.

Use it to revise upcoming content while the feed is in use — to apply updated recommendations, to reflect a user action, or to correct a ranking decision after playback has begun.

`setFeedItems` replaces the entire queue and `appendFeedItems` adds to the end. Use `replaceFeedItems` when the viewer's position must be preserved.

This operation requires a custom feed. Set `feedSource` to custom first — see [Configuration](/sdk/configuration) and [Custom feed input](/guides/content-types#custom-feed-input-feedinput-video).

## Boundary resolution

Every call takes a boundary: the last item to keep.

| Boundary | Keeps | Anchored to |
| - | - | - |
| `unseen` (default) | Everything the viewer has reached | The deepest position the viewer has reached |
| `afterCount` | The current item plus the next `keepUpcoming` entries | The viewer's current position |
| `afterItem` | Everything through the item id you supply | An item id you choose before the call |

**All three are resolved when the replacement is applied**, against the queue and playhead as they stand at that moment. The difference is what each one is anchored to.

`unseen` and `afterCount` are *relative*: they are recomputed from the live playhead, so they follow the viewer. `afterItem` is *absolute*: you name an item before the call, and the SDK locates it later.

<Warning>
  **Prefer a relative boundary when the viewer can move during the call.**

  With `afterItem` you commit to a boundary before your network request and the SDK resolves it after. If the viewer scrolls past that item in the meantime, the boundary lands behind the item on screen and the call is rejected with `boundary_behind_current` — correctly, since honouring it would replace the video being watched. The rejection rate scales with round-trip time: frequent over mobile connections, rare over a local network, which makes it straightforward to miss during development.

  Use `afterCount` or `unseen` for interaction-triggered replacement. Use `afterItem` when the queue position cannot move during the request — a server-driven replacement, a paused feed, a backgrounded surface.
</Warning>

`keepUpcoming` counts entries in the queue, which is what `itemIds` lists.

`afterItem` also accepts an optional `untilId` to bound the end of the range, replacing a window rather than the entire tail.

<Note>
  No boundary can replace the item on screen or anything before it: a replacement always keeps the queue up to at least the viewer's current position.

  `unseen` keeps more than that. It anchors to the deepest position the viewer has ever reached, and that marker also moves on programmatic activation, so `startAt` and `scrollToItem` advance it. If the viewer then scrolls back, `unseen` still preserves everything up to that high-water mark, while `afterCount` and `afterItem` ignore that marker — they are bounded only by the current position, so they can replace the region between the playhead and the mark.
</Note>

## Usage

<Tabs>
  <Tab title="iOS">
    ```swift RecommendationRefresh.swift theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    feedVC.replaceFeedItems(
        [
            .video(playbackId: "Xw5BJN2tZkM4q7vLdR9c"),
            .video(playbackId: "f8RkQ01dWcP3mY6aT2hs"),
        ],
        boundary: .afterCount(keepUpcoming: 2)
    ) { result in
        guard result.status == "applied" else {
            print("replace rejected: \(result.reason ?? "unknown")")
            return
        }
        print("kept \(result.keptCount), inserted \(result.insertedCount)")
    }
    ```

    `boundary` defaults to `.unseen`. The remaining cases are `.afterCount(keepUpcoming:)` and `.afterItem(afterId:untilId:)`. The method is main-actor bound.
  </Tab>

  <Tab title="Android">
    ```kotlin RecommendationRefresh.kt theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    feedFragment.replaceFeedItems(
        items = listOf(
            FeedInput.Video(playbackId = "Xw5BJN2tZkM4q7vLdR9c"),
            FeedInput.Video(playbackId = "f8RkQ01dWcP3mY6aT2hs"),
        ),
        boundary = ReplaceBoundary.AfterCount(keepUpcoming = 2),
    ) { result ->
        if (result.status != "applied") {
            Log.w("Feed", "replace rejected: ${result.reason}")
            return@replaceFeedItems
        }
        Log.d("Feed", "kept ${result.keptCount}, inserted ${result.insertedCount}")
    }
    ```

    `boundary` defaults to `ReplaceBoundary.Unseen`. The remaining variants are `ReplaceBoundary.AfterCount(keepUpcoming)` and `ReplaceBoundary.AfterItem(afterId, untilId)`. The method may be called from any thread and dispatches to the main thread, but calls made from different threads are not guaranteed to be ordered relative to one another.
  </Tab>

  <Tab title="React Native">
    ```tsx RecommendationRefresh.tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    const result = await feedRef.current?.replaceFeedItems(
      [
        { type: 'video', playbackId: 'Xw5BJN2tZkM4q7vLdR9c' },
        { type: 'video', playbackId: 'f8RkQ01dWcP3mY6aT2hs' },
      ],
      { mode: 'afterCount', keepUpcoming: 2 },
    );

    if (result?.status !== 'applied') {
      console.warn('replace rejected:', result?.reason);
    } else {
      console.log(`kept ${result.keptCount}, inserted ${result.insertedCount}`);
    }
    ```

    `options` is optional and defaults to `{ mode: 'unseen' }`. The full shape:

    ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    type ReplaceFeedItemsOptions =
      | { mode?: 'unseen' }
      | { mode: 'afterCount'; keepUpcoming: number }
      | { mode: 'afterItem'; afterId: string; untilId?: string };
    ```

    The promise never rejects — failures arrive as a `rejected` status. It resolves once the feed applies or refuses the call, so a call made against a feed that is not currently registered, such as one in a backgrounded tab, can stay pending until that feed is registered again. Do not await it on a path that must complete.
  </Tab>
</Tabs>

Passing an empty `items` array is legal, and truncates the queue at the boundary.

Give each item a stable `id`. Items are matched by id alone, so reusing an id for materially different content will not rebind the cell.

## Queue state

The SDK exposes a read-back of the queue as it currently stands: the ordered item ids, the viewer's position, and the deepest item reached. Use it to compute a boundary, or to reconcile the outcome of a call.

| Field | Description |
| - | - |
| `itemIds` | Ordered ids of the items in the queue |
| `currentIndex` | Index of the item on screen |
| `currentItemId` | Id of the item on screen, or null before any content exists |
| `deepestSeenItemId` | Furthest item the viewer has reached, or null |

<Tabs>
  <Tab title="iOS">
    ```swift theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    let state = feedVC.feedQueueState()
    print("\(state.itemIds.count) items, viewer at \(state.currentIndex)")
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    val state = feedFragment.feedQueueState()
    Log.d("Feed", "${state.itemIds.size} items, viewer at ${state.currentIndex}")
    ```

    Call this on the main thread.
  </Tab>

  <Tab title="React Native">
    ```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    const state = await feedRef.current?.getFeedQueueState();
    console.log(`${state?.itemIds.length} items, viewer at ${state?.currentIndex}`);
    ```

    A feed that is unregistered or dismissed resolves immediately with an empty snapshot, in which `currentIndex` is `-1`.
  </Tab>
</Tabs>

## Result fields

| Field | Description |
| - | - |
| `status` | `applied` or `rejected` |
| `reason` | Present only when rejected |
| `boundaryId` | The last item kept. For `unseen`, this reports the item the SDK selected |
| `keptCount` | Items kept, up to and including the boundary. Excludes any preserved tail beyond `untilId` |
| `removedCount` | Items removed from the replaced range |
| `insertedCount` | Items actually inserted |
| `removedIds` | Ids of the items removed from the queue |
| `droppedIds` | Incoming items that never entered the queue |
| `queueLength` | Queue length after the call |
| `remainingCount` | Entries after the viewer's position once the call has been applied |

Use `removedIds`, not `droppedIds`, to prune host-side state keyed by item id. `removedIds` lists items that were in the queue and are now gone; `droppedIds` lists incoming items that never entered it.

`queueLength` and `remainingCount` are both `0` on a rejection raised before the feed was consulted. Treat `0/0` on a rejection as unknown rather than as an empty queue.

A rejection never changes the queue. An applied replacement can still insert fewer items than you supplied.

### Dropped items

`insertedCount` can be lower than the number of items you supplied. An item is dropped when:

* the viewer dismissed it earlier in this feed session, which blocks both its `id` and its playback id — re-sending the same video under a new `id` is still dropped
* its `id` duplicates a kept item, or an earlier item in the same batch — the first occurrence wins
* its playback id duplicates that of a kept item, or of an earlier item in the same batch

Dropped items are identified by `id` where set, by playback id otherwise, and by the carousel id for a carousel. An item that the React Native bridge could not parse at all appears as `__sk_unparsable_<index>`, keyed to its position in the batch you sent.

`droppedIds` is empty on any rejection raised before the feed was consulted, including `invalid_items`, so an empty list does not by itself mean nothing was refused.

## Rejection reasons

| Reason | Meaning |
| - | - |
| `feed_not_ready` | The feed is not registered, is empty, is not a custom feed, its view was torn down, or the wait for the feed to settle was exhausted |
| `boundary_not_found` | `afterId` was not present in the queue at apply time |
| `until_not_found` | `untilId` was supplied but was not present in the queue |
| `until_not_after_boundary` | `untilId` resolved at or before `afterId` |
| `boundary_behind_current` | The boundary fell behind the item on screen, or `keepUpcoming` was negative |
| `superseded` | The queue was reset between the call and the apply |
| `invalid_items` | React Native only — the items or options payload was malformed at the bridge |

`superseded` warrants explicit handling. It indicates that the queue was replaced between the call and the apply, by a pull-to-refresh, a preload reseed, or your own `setFeedItems`. The replacement was discarded rather than applied to a queue that no longer existed. Re-read the queue state to determine whether the replacement remains appropriate.

Retry behaviour differs by reason. A `feed_not_ready` from an exhausted settle wait, and a `boundary_behind_current` from an `afterItem` boundary, are both worth retrying. A `boundary_behind_current` from `afterCount` means `keepUpcoming` was negative, which is a static bug — do not retry it.

<Warning>
  **Results differ if the surface is torn down during apply.**

  If the feed is dismissed while a replacement is being applied, iOS resolves `rejected` with `feed_not_ready`, while Android resolves `applied` with the real counts. This is deliberate rather than a defect: on Android the change is applied and persists, on iOS it is not. Do not treat the two results as equivalent in this case; read the queue state to establish the outcome.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.