I'm using PostgraphQL (https://github.com/calebmer/postgraphql) with Relay and wired a UpdateQuestionMutation into my app. However, I do not get optimistic updating to work.(When I enable network throttling in chrome I can see that the the optimistic update gets handled but the component still shows the old title).Do I miss something? I have following pieces :
class QuestionClass extends Component<IQuestion, void> {
save = (item) => {
this.props.relay.commitUpdate(
new UpdateQuestionMutation({store: this.props.store, patch: item})
);
this.isEditing = false;
};
public render(): JSX.Element {
const item = this.props.store;
console.log(item);
...
const Question = Relay.createContainer(QuestionClass, {
fragments: {
// The property name here reflects what is added to `this.props` above.
// This template string will be parsed by babel-relay-plugin.
store: () => Relay.QL`
fragment on Question {
${UpdateQuestionMutation.getFragment('store')}
title
description
userByAuthor {
${User.getFragment('store')}
}
}`,
},
});
...
export default class UpdateQuestionMutation extends Relay.Mutation<any, any> {
getMutation() {
return Relay.QL `mutation { updateQuestion }`
}
getVariables() {
console.log(this.props);
return {
id: this.props.store.id,
questionPatch: this.props.patch
}
}
getFatQuery() {
return Relay.QL `fragment on UpdateQuestionPayload { question }`
}
getConfigs() {
return [{
type: "FIELDS_CHANGE",
fieldIDs: {
question: this.props.store.id
}
}]
}
getOptimisticResponse() {
return {
store: this.props.patch
}
}
// This mutation has a hard dependency on the question's ID. We specify this
// dependency declaratively here as a GraphQL query fragment. Relay will
// use this fragment to ensure that the question's ID is available wherever
// this mutation is used.
static fragments = {
store: () => Relay.QL`
fragment on Question {
id
}
`,
};
}
Edit: That's what I see in the postgraphql logs:
mutation UpdateQuestion($input_0: UpdateQuestionInput!) { updateQuestion(input: $input_0) { clientMutationId ...F1 } } fragment F0 on Question { id rowId title description userByAuthor { id rowId username } } fragment F1 on UpdateQuestionPayload { question { id ...F0 } }
Related
fragment commentFragment on Comment {
id
text
galleryId
commentUser {
id
firstName
lastName
}
}
fragment galleryFragment on Gallery {
id
path
label
comments {
...commentFragment
}
}
We first retrieve the getGalleries using the following gql :
query getGalleries($filters: galleryFilterInput) {
getGalleries(filters: $filters) {
galleries {
...galleryFragment
}
cursor
hasMore
}
}
Now when the user enters a comment on a single gallery item we run the following mutation :
mutation addCommentMutation($input: addCommentInput!) {
addComment(input: $input) {
...commentFragment
}
}
Now, we were previously using refetchQueries to update the Galleries but we have now decided to use cache.modify however we are having problem with updating the galleries
update: (cache, data: any) => {
cache.modify({
fields: {
getGalleries(existing, { readField }) {
const comment = data.data.addComment;
const newEventRef = cache.writeFragment({
fragment: commentFragment,
data: comment,
fragmentName: "commentFragment",
});
const index = existing.galleries.findIndex(
aGallery => aGallery.id === comment.galleryId
);
if (index !== -1) {
const existingCommentRef = readField("comments", existing.galleries[index])
as readonly Reference;
const newCommentsRefs = [...existingCommentRef, newRef];
cache.writeFragment({
id: "Gallery:" + readField("id", existing.galleries[index]),
fragment: gql`
fragment comments on Gallery {
comments {
...commentFragment
}
}
`,
data: newCommentsRefs,
});
}
return existing;
},
},
});
},
I am unsure how I update the newCommentsRefs in that Gallery
update(cache, data) {
const comment = data.data.addComment;
cache.writeFragment({
fragment: commentFragment,
data: comment,
fragmentName: "commentFragment",
});
const gallery: Gallery = cache.readFragment({
id: `Gallery:${comment.galleryId}`,
fragment: galleryFragment,
fragmentName: 'galleryFragment'
});
if (gallery) {
const newComments = [...gallery.comments, comment]
cache.writeFragment({
id: `Gallery:${comment.galleryId}`,
fragment: galleryFragment,
fragmentName: 'galleryFragment',
data: { ...gallery, comments: newComments }
});
}
}
I would like to create dynamic pages when I click a tag in an article or elsewhere on my website.
I'm using Next.js, SSG, and fetching the articles containing the tags from Contentful with the following GraphQL queries:
export async function getArticles() {
const articlesQuery = gql`
{
articleCollection(order: date_DESC) {
items {
title
slug
excerpt
date
contentfulMetadata {
tags {
name
id
}
}
featuredImage {
title
url
width
height
}
author {
name
photo {
fileName
url
width
height
}
title
twitterProfile
linkedInProfile
slug
}
}
}
}
`;
return graphQLClient.request(articlesQuery);
}
export async function getArticle(slug) {
const articleQuery = gql`
query getArticle($slug: String!) {
articleCollection(limit: 1, where: { slug: $slug }) {
items {
title
slug
excerpt
date
contentfulMetadata {
tags {
name
id
}
}
featuredImage {
title
url
width
height
}
author {
name
photo {
fileName
url
width
height
}
title
twitterProfile
linkedInProfile
slug
}
content {
json
links {
entries {
block {
sys {
id
}
__typename
... on VideoEmbed {
title
embedUrl
}
... on CodeBlock {
description
language
code
}
}
}
assets {
block {
sys {
id
}
url
title
width
height
}
}
}
}
}
}
}
`;
return graphQLClient.request(articleQuery, {
slug,
});
}
The contentfulMetadata is where the tags come from:
contentfulMetadata {
tags {
name
id
}
}
This is my [id].jsx file:
import { getArticles, getArticle } from "#utils/contentful";
export async function getStaticPaths() {
const data = await getArticles();
return {
paths: data.articleCollection.items.map((article) => ({
params: { id: article.contentfulMetadata.tags[0].id },
})),
fallback: false,
};
}
export async function getStaticProps(context) {
const data = await getArticle(context.params.id);
return {
props: { article: data.articleCollection.items[0] },
};
}
export default function TagPage({ article }) {
return (
<div>
<h1>{article.contentfulMetadata.tags.id}</h1>
</div>
);
}
I get the following error:
Error: Error serializing `.article` returned from `getStaticProps` in "/tags/[id]". Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.
When console.log(data.articleCollection.items.contentfulMetadata.tags.id); or console.log(data.articleCollection.items.contentfulMetadata.tags[0].id); within getStaticPaths function it provides the following error:
TypeError: Cannot read property 'tags' of undefined
Can anyone show how to create a dynamic page ([id].jsx) file, which shows the tag id as the header <h1> as well as all articles containing the same tag?
Contentful DevRel here 👋🏼.
article.contentfulMetadata.tags is an array, as an entry can have more than one tag. So you'll need to access the tag you want via article.contentfulMetadata.tags[0].id or article.contentfulMetadata.tags[desired_index].id and so on.
Here's an example GraphQL query:
query {
blogPostCollection {
items {
contentfulMetadata {
tags {
id
name
}
}
}
}
}
And here's the response with tags as an array:
"data": {
"blogPostCollection": {
"items": [
{
"contentfulMetadata": {
"tags": [
{
"id": "salmastag",
"name": "Salma s tag"
}
]
}
},
{
"contentfulMetadata": {
"tags": []
}
}
]
}
}
}
Notice how if a blog post doesn't have any PUBLIC tags assigned (the second entry in the response), an empty array is returned — you might want to do some safety checking in your code for this.
I have the following GRAPHQL subscription:
Schema.graphql
type Subscription {
booking: SubscriptionData
}
type SubscriptionData {
booking: Booking!
action: String
}
And this is the resolver subsrciption file
Resolver/Subscription.js
const Subscription = {
booking: {
subscribe(parent, args, { pubsub }, info) {
return pubsub.asyncIterator("booking");
}
}
};
export default Subscription;
Then I have the following code on the Mutation in question
pubsub.publish("booking", { booking: { booking }, action: "test" });
I have the follow subscription file in front end (React)
const getAllBookings = gql`
query {
bookings {
time
durationMin
payed
selected
activity {
name
}
}
}
`;
const getAllBookingsInitial = {
query: gql`
query {
bookings {
time
durationMin
payed
selected
activity {
name
}
}
}
`
};
class AllBookings extends Component {
state = { allBookings: [] }
componentWillMount() {
console.log('componentWillMount inside AllBookings.js')
client.query(getAllBookingsInitial).then(res => this.setState({ allBookings: res.data.bookings })).catch(err => console.log("an error occurred: ", err));
}
componentDidMount() {
console.log(this.props.getAllBookingsQuery)
this.createBookingsSubscription = this.props.getAllBookingsQuery.subscribeToMore(
{
document: gql`
subscription {
booking {
booking {
time
durationMin
payed
selected
activity {
name
}
}
action
}
}
`,
updateQuery: async (prevState, { subscriptionData }) => {
console.log('subscriptionData', subscriptionData)
const newBooking = subscriptionData.data.booking.booking;
const newState = [...this.state.allBookings, newBooking]
this.setState((prevState) => ({ allBookings: [...prevState.allBookings, newBooking] }))
this.props.setAllBookings(newState);
}
},
err => console.error(err)
);
}
render() {
return null;
}
}
export default graphql(getAllBookings, { name: "getAllBookingsQuery" })(
AllBookings
);
And I get the following response:
data: {
booking: {booking: {...} action: null}}
I get that I am probably setting up the subscription wrong somehow but I don't see the issue.
Based on your schema, the desired data returned should look like this:
{
"booking": {
"booking": {
...
},
"action": "test"
}
}
The first booking is the field on Subscription, while the second booking is the field on SubscriptionData. The object you pass to publish should have this same shape (i.e. it should always include the root-level subscription field).
pubsub.publish('booking', {
booking: {
booking,
action: 'test',
},
})
Below is my code for adding and removing a person from a group.
For some reason, getOptimisticResponse is not working for this mutation.
Could this be due to having an argument groupId for isInGroup field?
class GroupAddRemovePersonMutation extends Relay.Mutation {
static initialVariables = {
groupId: null,
}
static prepareVariables(prevVars) {
return prevVars;
}
static fragments = {
person: () => Relay.QL`
fragment on Person {
id
isInGroup(groupId: $groupId)
}
`,
}
getMutation() {
return this.props.isInGroup ?
Relay.QL`mutation { groupRemovePerson }` :
Relay.QL`mutation { groupAddPerson }`;
}
getVariables() {
const {groupId, person} = this.props;
return {
personId: person.id,
groupId,
};
}
getCollisionKey() {
const {groupId, person} = this.props;
return `groupPerson_${groupId}_${person.id}`;
}
getFatQuery() {
const {groupId, person, isInGroup} = this.props;
return isInGroup ?
Relay.QL`
fragment on GroupRemovePersonMutationPayload {
person {
id
groups { id }
isInGroup(groupId: "${groupId}")
}
group {
id
person
hasPerson(personId: "${person.id}")
}
}
` :
Relay.QL`
fragment on GroupAddPersonMutationPayload {
person {
id
groups { id }
isInGroup(groupId: "${groupId}")
}
group {
id
person
hasPerson(personId: "${person.id}")
}
}
`;
}
getConfigs() {
const {groupId, person} = this.props;
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
person: person.id,
group: groupId,
},
}];
}
getOptimisticResponse() {
const {groupId, person, isInGroup} = this.props;
return {
person: {
id: person.id,
isInGroup: !isInGroup,
},
group: {
id: groupId,
hasPerson: !isInGroup,
},
};
}
}
I would try adding the groupId to the optimistic response first. In my experience, the optimistic response has to match the shape of the fat query exactly.
If you don't have the groupIds at the time the optimistic response is generated, you could try substituting temporary values until the response is returned from the server. This scenario occurs often when you are rendering a connection and providing keys to the view to distinguish repeated React elements.
I'm writing a deletion mutation. The mutation should delete a Key node and update the viewer's keys collection (I'm using Relay-style collections: viewer { keys(first: 3) { edges { node { ... }}}}.
Following the advice here, I'm using the FIELDS_CHANGE config for simplicity, and it's actually working:
export class DeleteKeyMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on Viewer { id }
`,
};
getMutation() { return Relay.QL`mutation {deleteKey}`; }
getVariables() {
return {
id: this.props.id,
};
}
getFatQuery() {
return Relay.QL`
fragment on DeleteKeyPayload {
viewer { keys }
}
`;
}
getConfigs() {
return [
{
type: 'FIELDS_CHANGE',
fieldIDs: {
viewer: this.props.viewer.id,
},
},
];
}
}
Now, how should I write an optimistic mutation for this? I've tried different approaches but none worked.
Optimistic update in Relay is just a simulation of what the server will return if operation succeeds. In your case you are removing one key, meaning the result would be an object without that key.
getOptimisticUpdate() {
return {
viewer: {
id: this.props.viewer.id,
keys: {
edges: this.props.viewer.keys.edges.filter((keyEdge) => key.node.id !== this.props.id)
}
}
};
}
You will also need to include the keys to your fragments so they are available in the mutation.
static fragments = {
viewer: () => Relay.QL`
fragment on Viewer { id, keys { edges(first: 3) { node { id } }}
`,
};
The problem with this approach is that it relies on your mutation to know what's your current keys pagination. If you are operating on the whole Connection at once, it is fine, but if you are using Relay pagination you should consider using other mutation operations.
There is NODE_DELETE, which can delete all occurrences of your key from Relay store or you can use RANGE_DELETE to only delete it from your current connection.