Relay Modern updater ConnectionHandler.getConnection() returns undefined when parent record is root - relayjs

Debugging update:
So, we went a bit further in debugging this and it seems like 'client:root' cannot access the connection at all by itself.
To debug the complete store, we added this line in the updater function after exporting the store variable from the relay/environment.
console.log(relayEnvStore.getSource().toJSON())
If I use .get() with the specific string client:root:__ItemList_items_connection, I can access the records I have been looking for but it's definitely not pretty.
const testStore = store.get('client:root:__ItemList_items_connection')
console.log(testStore.getLinkedRecords('edges'))
Original:
I'm using Relay Modern and trying to update the cache after the updateItem mutation is completed with the updater. The call to ConnectionHandler.getConnection('client:root', 'ItemList_items') returns undefined.
I'm not sure if it's because I'm trying to use 'client:root' as my parent record or if there's a problem with my code. Has anyone found themselves with a similar issue?
Here's the paginationContainer:
const ItemListPaginationContainer = createPaginationContainer(
ItemList,
{
node: graphql`
fragment ItemList_node on Query
#argumentDefinitions(count: { type: "Int", defaultValue: 3 }, cursor: { type: "String" }) {
items(first: $count, after: $cursor) #connection(key: "ItemList_items") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
},
{
direction: 'forward',
getConnectionFromProps: props => props.node && props.node.items,
getVariables(props, { count, cursor }) {
return {
count,
cursor
}
},
query: graphql`
query ItemListQuery($count: Int!, $cursor: String) {
...ItemList_node #arguments(count: $count, cursor: $cursor)
}
`
}
)
Here's the mutation:
const mutation = graphql`
mutation UpdateItemMutation($id: ID!, $name: String) {
updateItem(id: $id, name: $name) {
id
name
}
}
`
Here's the updater:
updater: (store) => {
const root = store.getRoot()
const conn = ConnectionHandler.getConnection(
root, // parent record
'ItemList_items' // connection key
)
console.log(conn)
},

Turns out that I was setting my environment incorrectly. The store would reset itself every time I would make a query or a mutation, hence why I couldn't access any of the connections. I initially had the following:
export default server => {
return new Environment({
network: network(server),
store: new Store(new RecordSource())
})
}
All connections are accessible with this change:
const storeObject = new Store(new RecordSource())
export default server => {
return new Environment({
network: network(server),
store: storeObject
})
}

Related

unable to get correct data using `useLazyLoadQuery` when compose fragment into query

I do see a sucessfull API request
const data = useLazyLoadQuery<brandQuery>(
graphql`query
brandQuery {
...brand_autoBrands
}
`,
{
first: 10,
},
{
fetchPolicy: "network-only",
}
);
console.log(data);
But I got following output. it is supposed to be a json object returned from my API.
brand_autoBrands fragment somewhere
const autoBrands = graphql`
fragment brand_autoBrands on AdminQuery
#argumentDefinitions(
first: { type: "Int", defaultValue: 10 }
after: { type: "String", defaultValue: "" }
last: { type: "Int" }
before: { type: "String" }
filters: { type: "[Filter]" }
sorters: { type: "[Sorter]"}
)
#refetchable(queryName: "BrandListPaginationQuery") {
autoBrands(first: $first, after: $after, last: $last, before: $before, filters: $filters, sorters: $sorters)
#connection(key: "BrandList_autoBrands") {
edges {
node {
...brandFragment
}
}
pageInfo {
startCursor
}
totalCount
}
}
`;
const {
data,
loadNext,
hasNext
} = usePaginationFragment<BrandListPaginationQuery, _>(
autoBrands,
props.autoBrands,
);
You can't log a fragment data object in the parent. That data belongs to the component that defined a fragment. For performance, relay needs to know that this data is fetched for a child component but the parent doesn't need to know what the actual data object is. You just need to pass the data to its component then you should be able to log it in the child component.
const data = useLazyLoadQuery<brandQuery>(
graphql`query
brandQuery {
...brand_autoBrands
}
`,
{
first: 10,
},
{
fetchPolicy: "network-only",
}
);
data && <Brand autoBrands={data} />

how to optimisticResponse a connection property in relay modern mutation?

i have a comments connection in a mutation, this is the query:
export default mutationFromQuery(graphql`
mutation AddBookMutation($input: AddBookInput! $count: Int $cursor: String ) {
addBook(input: $input) {
book {
__typename
cursor
node {
id
title
owner
createdAt
comments(first: $count, after: $cursor)
#connection(key: "BookComments_comments", filters: []) {
__typename
edges {
node {
id
}
}
}
}
}
}
}
`)
This is how i did my optimisticUpdater that don't work:
optimisticUpdater: (store) => {
const userProxy = store.get(viewerId)
const owner = userProxy.getValue('username')
const id = uuidv1();
const book = store.create(id, 'Book');
book.setValue(id, 'id');
book.setValue(bookTitle, 'title');
book.setValue(owner, 'owner');
book.setValue(Date.now(), 'createdAt');
const comments = store.create(uuidv1(), 'comments')
comments.setLinkedRecords([], 'edges')
const pageInfo = store.create(uuidv1(), 'pageInfo')
pageInfo.setValue(null, 'endCursor')
pageInfo.setValue(false, 'hasNextPage')
pageInfo.setValue(false, 'hasPreviousPage')
pageInfo.setValue(null, 'startCursor')
comments.setLinkedRecord(pageInfo, 'pageInfo')
book.setLinkedRecord(comments, 'comments')
const bookEdge = store.create(uuidv1(), 'BookEdge');
bookEdge.setLinkedRecord(book, 'node');
console.log('bookEdge ', bookEdge)
booksUpdater(userProxy, bookEdge);
},
The problem i have is that comments always ends up on undefined as you can see above i've already set it. I also did this but i am still not getting an optimistic UI:
optimisticResponse: {
addBook: {
book: {
__typename: 'BookEdge',
cursor: uuidv1(),
node: {
id: uuidv1(),
title: bookTitle,
owner: username,
createdAt: Date.now(),
comments: {
__typename: 'CommentConnection',
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false
}
}
}
}
}
},
App don't crash with optimisticResponse code but no optimistic UI effect, but with the optimisticUpdater it's crashing with comments being undefined, for now I am settling with my updater:
updater: (store) => {
const userProxy = store.get(viewerId)
const payload = store.getRootField('addBook');
booksUpdater(userProxy, payload.getLinkedRecord('book'));
},
since the comments is undefined I guess we cannot use this for optimistic effect:
const comments = store.create(uuidv1(), 'comments')
comments.setLinkedRecords([], 'edges')
book.setLinkedRecord(comments, 'comments')
on my Book, this is the query which has the comments fragment that is undefined on optimistic update with the code above:
export default createRefetchContainer(
BookItem,
{
book: graphql`
fragment BookItem_book on Book
#argumentDefinitions(
count: { type: "Int", defaultValue: 5 }
cursor: { type: "String", defaultValue: null }
) {
id
title
owner
createdAt
...BookComments_book
}
`
},
graphql`
query BookItemQuery($id: ID!, $count: Int, $cursor: String) {
book: node(id: $id) {
...BookItem_book #arguments(count: $count, cursor: $cursor)
}
}
`
);
and now the query for the comments component where it gets the book.comments.edges is undefined:
export default createPaginationContainer(
BookComments,
{
book: graphql`
fragment BookComments_book on Book
#argumentDefinitions(
count: { type: "Int", defaultValue: 3 }
cursor: { type: "String", defaultValue: null }
) {
id
title
comments(first: $count, after: $cursor)
#connection(key: "BookComments_comments", filters: []) {
__typename
edges {
node {
id
text
owner
createdAt
}
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
`
},
{
direction: 'forward',
getConnectionFromProps: (props) => props.book && props.book.comments,
getFragmentVariables: (prevVars, totalCount) => ({
...prevVars,
count: totalCount
}),
getVariables: (props, { count, cursor }, _fragmentVariables) => ({
count,
cursor,
id: props.book.id
}),
query: graphql`
query BookCommentsQuery($id: ID!, $count: Int, $cursor: String) {
book: node(id: $id) {
...BookComments_book #arguments(count: $count, cursor: $cursor)
}
}
`
}
);
maybe this is an anti pattern? but i just wanted to have a optimistic effect for this
Some things are still not very clear to me about how those components and queries work, so I'll update this answer later. (I don't know if you want to return new book optimistically from node() query or add it to some list/connection of books)
Please check if I used correct type names (CommentConnection / CommentsConnection, etc)
optimisticUpdater: (store) => {
const userProxy = store.get(viewerId)
const owner = userProxy.getValue('username')
const commentsParams = { // must be same keys and values as in comments(first: $count, after: $cursor)
first: count,
after: cursor
}
// Create Book
const id = uuidv1();
const book = store.create(id, 'Book');
book.setValue(id, 'id');
book.setValue(bookTitle, 'title');
book.setValue(owner, 'owner');
book.setValue(Date.now(), 'createdAt');
// Create comments connection
const comments = store.create(uuidv1(), 'CommentConnection')
comments.setLinkedRecords([], 'edges')
// Create PageInfo
const pageInfo = store.create(uuidv1(), 'PageInfo')
pageInfo.setValue(null, 'endCursor')
pageInfo.setValue(false, 'hasNextPage')
pageInfo.setValue(false, 'hasPreviousPage')
pageInfo.setValue(null, 'startCursor')
// Link created records
comments.setLinkedRecord(pageInfo, 'pageInfo')
book.setLinkedRecord(comments, 'comments', commentsParams) // don't forget commentsParams with same values as are used in comments graphql query
// I'm not sure about this final part, because I don't really get how that app works, but if you want this book to show as optimistic response for `node(id: $id)`, you'll do something like this:
store.getRoot().setLinkedRecord(book, 'node', { id: id }) // same id as used in BookItemQuery
}

Apollo Subscription doesn't seem to get called on Mutation

New to Apollo, so I decided to take the most simple example I found and try to work it in a slightly different way. My code can be found here.
The problem I am having is that the Subscription doesn't seem to get called when I call the Mutation createTask(). The Mutation and Subscription are defined in schema.graphql as:
type Mutation {
createTask(
text: String!
): Task
}
type Subscription {
taskCreated: Task
}
And in resolvers.js as:
Mutation: {
createTask(_, { text }) {
const task = { id: nextTaskId(), text, isComplete: false };
tasks.push(task);
pubsub.publish('taskCreated', task);
return task;
},
},
Subscription: {
taskCreated(task) {
console.log(`Subscript called for new task ID ${task.id}`);
return task;
},
},
What I am expecting to happen is that I would get a console.log in the server every time I run the following in the client:
mutation Mutation($text: String!) {
createTask(text:$text) {
id
text
isComplete
}
}
But nothing happens. What am I missing?
The subscription resolver function is called when there is actually a subscription to the GraphQL Subscription.
As you did not add a client which uses subscriptions-transport-ws and the SubscriptionClient for subscribing to your websocket and the subscription it will not work.
What you could do is add the subscription Channel to the setupFunctions of the SubscriptionManager and therein you get the value that the pubsub.publish function delivers.
Could look like this:
...
const WS_PORT = 8080;
const websocketServer = createServer((request, response) => {
response.writeHead(404);
response.end();
});
websocketServer.listen(WS_PORT, () => console.log( // eslint-disable-line no-console
`Websocket Server is now running on http://localhost:${WS_PORT}`
));
const subscriptionManager = new SubscriptionManager({
schema: executableSchema,
pubsub: pubsub,
setupFunctions: testRunChanged: (options, args) => {
return {
taskCreated: {
filter: (task) => {
console.log(task); // sould be log when the pubsub is called
return true;
}
},
};
},
,
});
subscriptionServer = new SubscriptionServer({
subscriptionManager: subscriptionManager
}, {
server: websocketServer,
path: '/',
});
...

Relay uses initial variable during setVariables transition, not "last" variable

I have a page where a bunch of file ids get loaded from localStorage, then when the component mounts / receives new props, it calls setVariables. While this works and the new variables are set, the results from the initial variables is used during the transition, which causes an odd flickering result.
Why would Relay give me something different during the transition at all? My expectation would be that this.props.viewer.files.hits would be the same as the previous call while setVariables is doing its thing, not the result from using the initial variables.
const enhance = compose(
lifecycle({
componentDidMount() {
const { files, relay } = this.props
if (files.length) {
relay.setVariables(getCartFilterVariables(files))
}
},
}),
shouldUpdate((props, nextProps) => {
if (props.files.length !== nextProps.files.length && nextProps.files.length) {
props.relay.setVariables(getCartFilterVariables(nextProps.files))
}
return true
})
)
export { CartPage }
export default Relay.createContainer(
connect(state => state.cart)(enhance(CartPage)), {
initialVariables: {
first: 20,
offset: 0,
filters: {},
getFiles: false,
sort: '',
},
fragments: {
viewer: () => Relay.QL`
fragment on Root {
summary {
aggregations(filters: $filters) {
project__project_id {
buckets {
case_count
doc_count
file_size
key
}
}
fs { value }
}
}
files {
hits(first: $first, offset: $offset, filters: $filters, sort: $sort) {
${FileTable.getFragment('hits')}
}
}
}
`,
},
}
)
Ah I finally figured this out. prepareParams was changing the value
export const prepareViewerParams = (params, { location: { query } }) => ({
offset: parseIntParam(query.offset, 0),
first: parseIntParam(query.first, 20),
filters: parseJsonParam(query.filters, null), <-- setting filters variable
sort: query.sort || '',
})
const CartRoute = h(Route, {
path: '/cart',
component: CartPage,
prepareParams: prepareViewerParams, <--updating variable
queries: viewerQuery,
})

How do you define parentName, parentId, and connectionName in getConfigs for a nested connection?

I am running into some issues writing a Relay Mutation on a nested connection. Here is the type structure:
{
viewer {
entity(id) {
events // mutate connection here
}
}
}
In the getConfigs I have both the parentName and parentID pointing to the "viewer", however the connectionName "events" does not exist on the viewer it exists on the "entity" type.
As you will see in the code snippet below I am also unsure how to use variables in the getFatQuery to fetch the mutated data with an entity ID.
getConfigs and getFatQuery:
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'events',
edgeName: 'eventEdge',
rangeBehaviors: {
'': 'append'
},
}];
}
getFatQuery() {
return Relay.QL`
fragment on addEventPayload {
viewer {
entity(id: $entityId) // how do I use variables in the getFatQuery {
events(first: 20) {
edges {
node {
status
}
}
}
}
},
eventEdge
}
`;
}
I am more than happy to help clarify my question if it makes no sense so please feel free to ask questions about my question.
Thanks for the help!
For anyone in the future getting snagged on this you don't have to worry about passing in the id again or dealing with nested queries/fields. Relay will just find and update the appropriate record on the client. This issue on Github was helpful for figuring this out, especially the comments from freiksenet.
In the example above - Instead of going through the viewer we just go straight to the entity.
outputFields:
outputFields: {
eventEdge: {
type: eventEdge,
resolve: async({event}) => {
const eventsByOwner = await Event.getEventsByOwnerId(event.ownerId)
const eventIndex = eventsByOwner.findIndex(evt => evt.id == event.id);
const cursor = offsetToCursor(eventIndex);
return {
cursor: cursor,
node: event
};
}
},
entity: {
type: entity,
resolve: async({event}) => {
return Entity.getEntity(event.ownerId)
}
},
}
getConfigs and getFatQuery:
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'entity',
parentID: this.props.entityId,
connectionName: 'events',
edgeName: 'eventEdge',
rangeBehaviors: {
'': 'append'
},
}];
}
getFatQuery() {
return Relay.QL`
fragment on addEventPayload #relay(pattern: true) {
entity {
events
},
eventEdge
}
`;
}
Note: Using #relay(pattern: true) will make sure you don't run into issues when you don't pass in arguments for connection queries and will fall back to your last query of this type.

Resources