A connection on a refetchable fragment: pages merged in the store, loadNext on the lens, and the edge directives that add to or remove from it.
A list that pages is a Relay connection in a fragment of its own. The fragment owns the pagination, the store owns the merged pages, a mutation adds or removes an edge with a directive, and the view reads the list like any other field. This chapter is that fragment, what the store holds after each page, and the edge directives.
Three directives make a connection: @connection(key:) names the list in the store; @refetchable(queryName:) gives the fragment a query of its own, which the next page is fetched with; @argumentDefinitions declares the page size and the cursor as the fragment's arguments, so the screen's query need not know them.
Plate 1. The connection fragment, the row it spreads, and the screen that spreads it. The screen's query never mentions first or after.
The lens of a connection reads nodes, the records in order across every page; hasNext and isLoadingNext; pageInfo with its cursors; connectionID, the key an edge directive names the list by; and loadNext(count), which fetches the next page with the fragment's query and its arguments. refetch() on the fragment's own lens fetches the first page again and replaces the list.
The lens of IssueList_repository, a fragment, reads a record of type Repository:
accessor
reads
as
issues
issues
a lens of IssueConnection
edges
edges
a lens of IssueEdge, a list, optional
node
node
a lens of Issue, optional
id
id
a scalar
issueRow
the parent's record
the lens of IssueRow_issue
cursor
cursor
a scalar
pageInfo
pageInfo
a lens of PageInfo
endCursor
endCursor
a scalar, optional
hasNextPage
hasNextPage
a scalar
id
id
a scalar
Plate 2. The connection as the compiler reported it. The paging surface is generated beside these accessors.
The connection lives under its key in the store, apart from the pages: a page is a slot of its own under the field's storage key, notes(first:2), and the connection record under the key, __TestNotes_notes_connection, links the edges of every page in order. A refetch replaces the merged edges with the first page; a page fetched after a cursor that is no longer the connection's end is ignored.
A view attaches a handle for TestNotesQuery(id: "1") under storeOrNetwork, named notes; the transport answers its fetch with tests/notes-page-1.json.
The server answers TestNotesPaginationQuery(count: 2, cursor: "c2", id: "1") with tests/notes-page-2.json, and the environment commits it as a server batch.
The server answers TestAddNote(characterId: "1", text: "Appended", connections: ["Character:1:__TestNotes_notes_connection"]) with tests/add-note-n9.json, and the environment commits it as a server batch.
The server answers TestNotesPaginationQuery(count: 2, cursor: "c1", id: "1") with tests/notes-page-3.json, and the environment commits it as a server batch.
Notifiednothing: no observed field changed, so no body runs
A lens reads
and finds
notes · character.notes.edges.2.node.text
Appended
notes · character.notes.edges.3.node.text
null
notes · character.notes.pageInfo.endCursor
c2
Plate 3. The specification's connection script, step by step: the first page, the second merged, a refetch that replaces, an edge appended by a mutation, and a page fetched after a cursor the connection has moved past, which the merge ignores and which notifies nothing.
A mutation that adds to or removes from a list says so on its payload, and the store does the edit in the same batch as the payload's own records:
@appendEdge(connections:) and @prependEdge(connections:) on a payload field that is an edge;
@appendNode(connections:, edgeTypeName:) and @prependNode(connections:, edgeTypeName:) on a payload field that is a node;
@deleteEdge(connections:) on a payload field holding the id of the node to remove from the lists;
@deleteRecord on a payload field holding the id of a record to delete outright, which every list skips and every link reads as null.
The connections variable carries the connection ids the view read from its lenses, so the same mutation can add to one list or three. Nothing edits the cache by hand, and nothing has to know which screens show the list.
CommentComposer.swift
importBatonimportSwiftUIstructCommentComposer: View {
let subjectID: Stringlet connectionID: String@Stateprivatevar text = ""@Mutation("""mutationCommentComposerAddComment($input: AddCommentInput!, $connections: [ID!]!) {
addComment(input: $input) {
commentEdge@appendEdge(connections: $connections) {
node { idbodyTextauthor { login } }
}
}
}
""")
var addComment: CommentComposerAddComment.Actionvar body: someView {
HStack {
TextField("Leave a comment", text: $text)
Button("Comment") {
let body = text
Task {
tryawaitaddComment(
input: AddCommentInput(body: body, subjectId: subjectID),
connections: [connectionID]
)
text = ""
}
}
.disabled(text.isEmpty || addComment.isInFlight)
}
}
}
Plate 4. A comment appended to the issue's comments. The composer takes the connectionID the list's lens read and passes it in connections.
The server answers TestAddNote(characterId: "1", text: "Appended", connections: ["Character:1:__TestNotes_notes_connection"]) with tests/add-note-n9.json, and the environment commits it as a server batch.
The Rick and Morty API pages by number, characters(page: 2), which is not a connection, and Baton does not pretend it is one: each page is a query of its own with the page as a variable, and the store keeps each under its storage key. A screen that shows twenty rows per page holds the current page's query; a screen that wants an endless list over such an API composes it from the pages itself; the samples show one page at a time. Page-based pagination as a concept of its own is on the watch list, with the trigger that would promote it; What earns a concept is why it is not a thirteenth word.
In Swift, ForEach takes a plural lens, a Baton.List, or a connection's nodes through initializers Baton adds, keyed by the record, so no id: is needed.