I tried so hard to update Apollo cache after running Mutation, but i couldn't be able to remove 1 second delay after the mutation.
I followed 'ac3-state-management-examples' for solve this problem, but still couldn't find any problem.
This is my client-side code.
export const DELETE_ITEM_IN_CART = gql`
mutation DeleteItemInCart($cartItemId: String!) {
DeleteItemInCart(cartItemId: $cartItemId)
}
`;
export function useDeleteItemInCart() {
console.log(`DELETION START! ${Date()}`);
const [mutate, { data, error }] = useMutation<
DeleteItemInCartType.DeleteItemInCart,
DeleteItemInCartType.DeleteItemInCartVariables
>(DELETE_ITEM_IN_CART, {
update(cache, { data }) {
const deletedCartItemId = data?.DeleteItemInCart;
const existingCartItems = cache.readQuery<myCart>({
query: MY_CART,
});
if (existingCartItems && deletedCartItem && existingCartItems.myCart) {
cache.writeQuery({
query: MY_CART,
data: {
myCart: {
cartItem: existingCartItems.myCart.cartItem.filter(
t => t.id !== deletedCartItemId,
),
},
},
});
console.log(`DELETION OVER! ${Date()}`);
}
},
});
return { mutate, data, error };
}
And here's my server-side mutation
export const DeleteItemInCart = mutationField('DeleteItemInCart', {
args: {cartItemId: nonNull('String')},
type: nonNull('String'),
description: 'Delete an item in my cart',
resolve: (_, {cartItemId}, ctx) => {
const {prisma} = ctx;
try {
prisma.cartItem.delete({
where: {
id: cartItemId,
},
});
return cartItemId;
} catch (error) {
return cartItemId;
}
},
});
This is an example of Apollo-remote-state-mananagement
export const DELETE_TODO = gql`
mutation DeleteTodo ($id: Int!) {
deleteTodo (id: $id) {
success
todo {
id
text
completed
}
error {
... on TodoNotFoundError {
message
}
}
}
}
`
export function useDeleteTodo () {
const [mutate, { data, error }] = useMutation<
DeleteTodoTypes.DeleteTodo,
DeleteTodoTypes.DeleteTodoVariables
>(
DELETE_TODO,
{
update (cache, { data }) {
const deletedTodoId = data?.deleteTodo.todo?.id;
const allTodos = cache.readQuery<GetAllTodos>({
query: GET_ALL_TODOS
});
cache.writeQuery({
query: GET_ALL_TODOS,
data: {
todos: {
edges: allTodos?.todos.edges.filter((t) => t?.node.id !== deletedTodoId)
},
},
});
}
}
)
return { mutate, data, error };
}
Any advice?
1 second delay is inevitable using apollo cache?
I took a short video of my issue. i dont think it's inevitable...
Related
[I am upgrading my current NextJS site to the latest version of NextJS 13 with the app directory and using generateStaticParams, I keep getting the following error when migrating my getStaticPaths to the new app directory page, I am using GraphQL:
Error: A required parameter (slug) was not provided as a string in generateStaticParams for /[slug]
I've consoled the returns and it seems to be returning the correct object with strings, not sure what I am missing.
// app/[slug]/page.tsx
export async function generateStaticParams() {
const { data } = await client.query({
query: gql`
query getSlugs {
posts(first: 100) {
nodes {
slug
}
}
}
`,
})
const response = data.posts.nodes.map((post: Post) => post.slug)
const slugs = await response.map((slug: string) => ({ params: { slug } }))
return [{ slugs }]
/* returns
[{ params: { slug: 'blog-post-1' } }, { params: { slug: 'blog-post-2' } }...]
*/
}
async function getPost(params) {
const { data } = await client.query({
query: gql`
query singlePost {
post(id: "${params.slug}", idType: SLUG) {
content
categories {
nodes {
name
}
}
featuredImage {
node {
sourceUrl
}
}
postId
}
}
`,
})
return {data.post}
}
export default async function PostPage({ params }) {
const post = await getPost(params)
return <Post post={post} />
}
You must remove the params key inside the object you return, you must only return the slug.
export async function generateStaticParams() {
const { data } = await client.query({
query: gql`
query getSlugs {
posts(first: 100) {
nodes {
slug
}
}
}
`,
})
const response = data.posts.nodes.map((post: Post) => post.slug)
const slugs = response.map((slug: string) => ({ slug }))
return slugs
/* returns
[{ slug: 'blog-post-1' }, { slug: 'blog-post-2' }...]
*/
}
I am trying to reload my table of data once my useMutation has completed.
On page load i am querying:
const { loading: appLoading, data: applicationsData } = useQuery(
applications.operations.GET_APPLICATIONS_BY_COMPANY,
{
client: applications.client,
variables: { companyId: userDetails.companyId },
}
)
when a user selects a button to clone a record:
const [
CloneApplication,
{ loading: cloneLoading, data: cloneData, error: cloneError },
] = useMutation(applications.operations.CLONE_APPLICATION_BY_COMPANY, {
client: applications.client,
onCompleted: (data) => {
setFinalData((prev) => [...prev, data]), console.log('data', data)
},
})
im adding a record to the list but when i refresh its not there. My assumption is instead of adding it to state, I need to refetch the applications and then save that to state which in turn will automatically refresh the table.
My question is how can i do that?
Edit:
const { applications } = apis
const { queryString, parameters } = getTemplatesListApiDetails()
const [finalData, setFinalData] = useState<any>([])
const [templatesList, setTemplatesList] = useState([])
const { loading, data } = useQuery(queryString, parameters)
const { loading: appLoading, data: applicationsData } = useQuery(
applications.operations.GET_APPLICATIONS_BY_COMPANY,
{
client: applications.client,
variables: { companyId: userDetails.companyId },
}
)
const [
CloneApplication,
{ loading: cloneLoading, data: cloneData, error: cloneError },
] = useMutation(applications.operations.CLONE_APPLICATION_BY_COMPANY, {
client: applications.client,
refetchQueries: [
{ query: applications.operations.GET_APPLICATIONS_BY_COMPANY },
],
})
useEffect(() => {
if (data && data.getCompanyTemplates)
setTemplatesList(
userDetails.globalTemplates === false
? data.getCompanyTemplates
: data.getAllTemplates
)
if (applicationsData && templatesList) {
const newFinalData = getFinalData({
applicationsList: applicationsData.getApplicationsByCompany,
templatesList: templatesList,
})
setFinalData(newFinalData)
}
}, [applicationsData, cloneData, data, templatesList])
getFinalData Function
export function getFinalData(request: {
templatesList: GetAllTemplate[]
applicationsList: GetApplicationsByCompany[]
}): FinalDataResponse[] {
const templates = request.templatesList.map((template) => {
const applicationsForTemplate = request.applicationsList.filter(
(app) => app.templateId === template.templateId
)
return { ...template, applications: applicationsForTemplate }
})
const groupedData = _.chain(templates)
.groupBy('templateId')
.map((value, key) => {
const templateName = _.chain(value)
.groupBy('templateName')
.map((value, key) => key)
.value()
const createdDate = _.chain(value)
.groupBy('dateCreated')
.map((value, key) => dayjs(key).format('ll'))
.value()
const lastModified = _.chain(value)
.groupBy('lastModified')
.map((value, key) => dayjs(key).format('ll'))
.value()
return {
templateId: key,
templateName: templateName[0],
createdDate: createdDate[0],
lastModified: lastModified[0],
applications: value[0].applications,
}
})
.value()
const finalData = groupedData.map((object, index) => {
return {
...object,
totalApplications: object.applications.length,
}
})
console.log('returning final data: ', finalData)
return finalData
}
To refetch the data automatically, you need to invalidate the previously cached results. In apollo, this is done using refetchQueries:
useMutation(applications.operations.CLONE_APPLICATION_BY_COMPANY, {
refetchQueries: [{ query: applications.operations.CLONE_APPLICATION_BY_COMPANY}]
})
More ways of solving this here: https://www.apollographql.com/blog/apollo-client/caching/when-to-use-refetch-queries/
I have the following ErrorLink set for Apollo Client.
export const errorLink = onError(
({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
notificationService.notify("An Error Occurred");
},
);
I need to test this implementation in a unit test.
I've the following to test Apollo Links
const MockQuery = gql`
query {
foo
}
`;
interface LinkResult<T> {
operation: Operation;
result: FetchResult<T>;
}
async function executeLink<T = ApolloLink>(
linkToTest: ApolloLink,
request: GraphQLRequest = { query: MockQuery },
) {
const linkResult = {} as LinkResult<T>;
return new Promise<LinkResult<T>>((resolve, reject) => {
execute(ApolloLink.from([linkToTest]), request).subscribe(
(result) => {
linkResult.result = result as FetchResult<T>;
},
(error) => {
reject(error);
},
() => {
resolve(linkResult);
},
);
});
}
it('triggers a notification on error', () => {
const testLink = new ApolloLink(() => {
await waitFor(() => expect(notificationSpy).toBeCalledWith('An Error Occurred'))
return null;
});
const link = ApolloLink.from([errorLink, testLink]);
executeLink(link);
});
These unit test work fine for other links like AuthLink where I test whether the auth token was set to the localStorage. But I cannot test the error link because I cannot trigger a GraphQL error.
You can create a mocked terminating link and provide a GraphQL operation result.
E.g.
errorLink.ts:
import { onError } from '#apollo/client/link/error';
type ErrorResponse = any;
export const errorLink = onError(({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
console.log('An Error Occurred');
console.log('graphQLErrors: ', graphQLErrors);
});
errorLink.test.ts:
import { ApolloLink, execute, Observable } from '#apollo/client';
import { gql } from 'apollo-server-express';
import { errorLink } from './errorLink';
const MockQuery = gql`
query {
foo
}
`;
describe('68629868', () => {
test('should pass', (done) => {
expect.assertions(1);
const mockLink = new ApolloLink((operation) =>
Observable.of({
errors: [
{
message: 'resolver blew up',
},
],
} as any),
);
const link = errorLink.concat(mockLink);
execute(link, { query: MockQuery }).subscribe((result) => {
expect(result.errors![0].message).toBe('resolver blew up');
done();
});
});
});
test result:
PASS apollo-graphql-tutorial src/stackoverflow/68629868/errorLink.test.ts (5.02s)
68629868
✓ should pass (14ms)
console.log src/stackoverflow/68629868/errorLink.ts:6
An Error Occurred
console.log src/stackoverflow/68629868/errorLink.ts:7
graphQLErrors: [ { message: 'resolver blew up' } ]
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.067s
package version: #apollo/client#3.3.20
I specifically needed to test handling NetworkError with TypeScript and it was a right pain to figure out, so here's how you can do it:
import {
ApolloLink,
execute,
FetchResult,
from,
gql,
GraphQLRequest,
Observable,
Operation,
} from '#apollo/client'
import { errorLink, notificationService } from './'
interface LinkResult<T> {
operation: Operation
result: FetchResult<T>
}
const MockQuery = gql`
query {
foo
}
`
class NetworkError extends Error {
bodyText
statusCode
result
message
response
constructor(networkErrorProps, ...params) {
super(...params)
const {
name,
bodyText,
statusCode,
result,
message,
response,
} = networkErrorProps
this.name = name
this.bodyText = bodyText
this.statusCode = statusCode
this.result = result
this.message = message
this.response = response
}
}
describe('errorLink', () => {
it('should handle error and send notification', async () => {
const mockLink = new ApolloLink((operation, forward) => {
let fetchResult: FetchResult = {
errors: [], // put GraphQLErrors here
data: null,
}
// Thanks https://stackoverflow.com/a/70936974/21217
let linkResult = Observable.of(fetchResult).map(_ => {
throw new NetworkError({
name: 'ServerParseError',
message: 'Unexpected token',
response: {},
bodyText: '<!DOCTYPE html><html><head></head><body>Error</body></html>',
statusCode: 503,
result: {},
})
})
return linkResult
})
async function executeLink<T = any, U = any>(
dataLink: ApolloLink
) {
const linkResult = {} as LinkResult<T>
return new Promise<LinkResult<T>>((resolve, reject) => {
execute(from([errorLink, dataLink]), {
query: MockQuery,
}).subscribe(
result => {
// We don't care
},
error => {
// We can resolve here to skip having a try / catch around the await below
resolve(linkResult)
},
)
})
}
const notificationSpy = jest.spyOn(notificationService, 'notify')
await executeLink(mockLink)
expect(notificationSpy).toHaveBeenCalledWith('An Error Occurred')
})
})
In the mutation below I'm updating the avatarId for a person and I would like the new imageUrl to propagate everywhere it's needed, namely the <UserAvatar /> component which can be in a lot of components on the same page.
I know this new URL before ever hitting the UpdatePersonMutation so I'm not sure what I'm doing wrong.
Do I need to involve the store and / or use optimisticUpdater, and / or use subscriptions to get a new avatar image to show up everywhere instantly?
import { commitMutation } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
import modernEnvironment from '../Environment';
const mutation = graphql`
mutation UpdatePersonMutation($input: UpdatePersonInput!) {
updatePerson(input: $input) {
imageByAvatarId {
id
imageUrl // I know this before I ever reach this mutation
}
person {
id
avatarId
}
}
}
`;
const commit = (payload, callback) => {
const input = {
id: payload.id,
personPatch: {
avatarId: payload.avatarId,
},
};
return commitMutation(modernEnvironment, {
mutation,
variables: {
input,
},
optimisticResponse: {
updatePerson: {
imageByAvatarId: {
id: payload.imageId,
imageUrl: payload.imageUrl,
},
person: {
id: payload.id,
avatarId: payload.avatarId,
},
},
},
onCompleted: response => {
callback(response);
},
onError: error => {
console.log(error);
},
});
};
export default commit;
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',
},
})