Apollo client writeQuery updates stores, but UI componens only updates after the second function call - apollo

"apollo-cache-inmemory": "^1.6.2",
"apollo-client": "^2.6.3",
I setup a simple subscription with the client.subscribe method and try to update the store with the client.writeQuery method
export default class App extends Component<Props> {
componentDidMount() {
this.purchaseSubscription = client.subscribe({
query: PURCHASE_ASSIGNED_SUBSCRIPTION,
variables: { status: ['INPREPARATION', 'PROCESSED', 'READYFORCOLLECTION', 'ONTHEWAY', 'ATLOCATION'] },
}).subscribe({
next: (subscriptionData) => {
const { cache } = client;
const prev = cache.readQuery({
query: MY_PURCHASES,
variables: { status: ['INPREPARATION', 'PROCESSED', 'READYFORCOLLECTION', 'ONTHEWAY', 'ATLOCATION'] },
});
const newPurchase = subscriptionData.data.purchaseAssignedToMe;
const data = { myPurchases: [...prev.myPurchases, newPurchase] };
cache.writeQuery({
query: MY_PURCHASES,
variables: { status: ['INPREPARATION', 'PROCESSED', 'READYFORCOLLECTION', 'ONTHEWAY', 'ATLOCATION'] },
data,
});
},
error: (err) => { console.error('err', err) },
});
}
render() {
return (
<ApolloProvider client={client}>
<AppContainer />
</ApolloProvider>
);
}
}
The store gets updated after the call, however the UI component is only re-rendered only on the second publish event.
The UI components is setup the following way:
<Query
query={MY_PURCHASES}
variables={{ status: ['INPREPARATION', 'PROCESSED', 'READYFORCOLLECTION', 'ONTHEWAY', 'ATLOCATION'] }}
>
...
<Query />
By reading the cache after the writeQuery is called I was able to validate that the store reflect the proper state, however the UI component only gets updated at every second call.
What am I missing here?

ApolloClient.subscribe's next function is very similar to how updateQueries works in Apollo Client’s mutate function, but with the exception that cache.writeQuery does not broadcast the changes if it is not called from the the Mutation's update function.
SOLUTION: use client.writeQuery(...) instead of cache.writeQuery(...)
Note: The update function receives cache rather than client as its
first parameter. This cache is typically an instance of InMemoryCache,
as supplied to the ApolloClient constructor when the client was
created. In case of the update function, when you call
cache.writeQuery, the update internally calls broadcastQueries, so
queries listening to the changes will update. However, this behavior
of broadcasting changes after cache.writeQuery happens only with the
update function. Anywhere else, cache.writeQuery would just write to
the cache, and the changes would not be immediately broadcast to the
view layer. To avoid this confusion, prefer client.writeQuery when
writing to cache.
Source: https://www.apollographql.com/docs/react/essentials/mutations/#updating-the-cache

Related

Dropzone.js + AWS S3 stalling queue

I'm trying to impliment a dropzone.js uploader to amazon S3 using the aws-sdk.js for the browser. But when I exceed the 'parallelUploads' maximum in the settings, the queue never completes. I'm using the approach in the following link:
amazon upload
relevant parts of my code:
var dz = new Dropzone("#DZContainer", {
acceptedFiles: "image/*,.jpg,.jpeg,.png,.gif",
autoQueue: true,
autoProcessQueue: true,
parallelUploads: 10,
clickable: [".uploadButton"],
accept: function(file, done){
let params = {
"Bucket": "upload-bucket",
"Key": getFullKey(file.name),
Body: file,
Region: "us-east-1,
ContentType: file.type
}
file.s3upload = AWS.S3.ManagedUpload(params);
if (typeof(done) === 'function') done();
},
canceled: function(file) {
if (file.s3upload) file.s3upload.abort();
},
init: function () {
this.on('removedfile', function (file) {
if (file.s3upload) file.s3upload.abort();
});
}
)
dz.uploadFiles = function (files) {
for (var j = 0; j < files.length; j++) {
var file = files[j];
dz.SendFile(file);
}
};
dz.SendFile = function(file) {
file.s3upload.send(function (err, data) {
if (err) {
console.err(err)
dz.emit("error", file, err.message);
} else {
dz.emit("complete", file);
}
});
if I drag in (or use the clickable) more than 10 files, the first 10 complete but it never processes the rest of the queue. What am I missing? All help is appreciated
EDIT: With a little more digging into Dropzone, it looks as though the file status is never getting set to complete. I see a function called _finished() in the dropzone code, but I'm having a hard time figuring out what specifically is supposed to trigger that function. I have tried dz.emit("complete", file) listed below as well as adding dz.emit("success",file) but my breakpoint at the first line of the _finished() function never triggers. Thus the file.status never gets set to completed.
Does anyone know when/what/how _finished() is supposed to be run?
As mentioned in the edit, I was able to track down where the .status was not properly getting set. This seemed to be in a private Dropzone function called _finished()
With further examination, I noticed that _finished() seemed to also be calling emit("complete", file) after setting file.status to Dropzone.SUCCESS and also emitting "success". It then checks if autoProcessQueue is set and if it is, returns the result of a processQueue() call.
I had a hard time figuring out what triggered this function as it was on an onload event that eventually realized was tied to an XHTTPRequest object used by the internal uploader (which is being overridden by the S3 uploader)
So I modified the function to emulate what the Dropzone._finished() was doing and it's behaving as expected:
dz.SendFile = function(file) {
file.s3upload.send(function (err, data) {
if (err) {
console.err(err)
dz.emit("error", file, err.message);
} else {
file.status = Dropzone.SUCCESS;
dz.emit("success", file, data, err);
dz.emit("complete", file);
if(dz.options.autoProcessQueue)
dz.processQueue()
}
});

ApolloGraphQL: useSubscription Hook Syntax with onSubscriptionData?

I'm trying to build an Apollo useSubscription hook that uses onSubscriptionData.
I've looked in the Apollo docs, but I haven't yet an example.
E.g. something like:
const { loading, error, data } = useSubscription(
INCOMING_MESSAGES_SUBSCRIPTION_QUERY,
{
variables: {"localUserId": Meteor.userId()},
onSubscriptionData: myFunctionThatRunsWhenSubscriptionDataArrives
}
);
That can't be right yet, because it doesn't include OnSubscriptionDataOptions<TData>, which is mentioned in the Apollo docs.
What is the correct way to build a useSubscription hook that uses onSubscriptionData?
The onSubscriptionData function is passed a single options parameter of the type OnSubscriptionDataOptions. The options object has two properties:
client -- the ApolloClient instance used to query the server
subscriptionData -- an object with the following properties: loading, data, error
Example usage:
const { loading, error, data } = useSubscription(
INCOMING_MESSAGES_SUBSCRIPTION_QUERY,
{
variables: {"localUserId": Meteor.userId()},
onSubscriptionData: ({ subscriptionData: { data } }) => {
// do something with `data` here
}
},
)

Apollo client mutation with writeQuery not triggering UI update

I have a mutation to create a new card object, and I expect it should be added to the user interface after update. Cache, Apollo Chrome tool, and console logging reflect the changes, but the UI does not without a manual reload.
const [createCard, { loading, error }] = useMutation(CREATE_CARD, {
update(cache, { data: { createCard } }) {
let localData = cache.readQuery({
query: CARDS_QUERY,
variables: { id: deckId }
});
localData.deck.cards = [...localData.deck.cards, createCard];
;
client.writeQuery({
query: CARDS_QUERY,
variables: { id: parseInt(localData.deck.id, 10) },
data: { ...localData }
});
I have changed cache.writeQuery to client.writeQuery, but that didn't solve the problem.
For reference, here is the Query I am running...
const CARDS_QUERY = gql`
query CardsQuery($id: ID!) {
deck(id: $id) {
id
deckName
user {
id
}
cards {
id
front
back
pictureName
pictureUrl
createdAt
}
}
toggleDeleteSuccess #client
}
`;
I managed the same result without the cloneDeep method. Just using the spread operator solved my problem.
const update = (cache, {data}) => {
const queryData = cache.readQuery({query: USER_QUERY})
const cartItemId = data.cartItem.id
queryData.me.cart = queryData.me.cart.filter(v => v.id !== cartItemId)
cache.writeQuery({query: USER_QUERY, data: {...queryData}})
}
Hope this helps someone else.
Ok, finally ran into a long Github thread discussing their solutions for the same issue. The solution that ultimately worked for me was deep cloning the data object (I personally used Lodash cloneDeep), which after passing in the mutated data object to cache.writeQuery, it was finally updating the UI. Ultimately, it still seems like there ought to be a way to trigger the UI update, considering the cache reflects the changes.
Here's the after, view my original question for the before...
const [createCard, { loading, error }] = useMutation(CREATE_CARD, {
update(cache, { data: { createCard } }) {
const localData = cloneDeep( // Lodash cloneDeep to make a fresh object
cache.readQuery({
query: CARDS_QUERY,
variables: { id: deckId }
})
);
localData.deck.cards = [...localData.deck.cards, createCard]; //Push the mutation to the object
cache.writeQuery({
query: CARDS_QUERY,
variables: { id: localData.deck.id },
data: { ...localData } // Cloning ultimately triggers the UI update since writeQuery now sees a new object.
});
},
});

Apollo GraphQL client doesn't return cached nested types in a query

I'm performing a query to get PowerMeter details in which contains another type inside called Project. I write the query this way:
query getPowerMeter($powerMeterId: ID!) {
powerMeter: powerMeter(powerMeterId: $powerMeterId) {
id
name
registry
project {
id
name
}
}
}
When I perform the query for the first time, project is successfully returned. The problem is that when I perform subsequent queries with the same parameters and default fetchPolicy (cache-first), project isn't returned anymore.
How may I solve this problem?
Also, I call readFragment to check how powerMeter is saved in the cache and the response shows that powerMeter has project saved.
const frag = client.readFragment({
fragment: gql`
fragment P on PowerMeter {
id
name
registry
project {
id
name
}
}
`,
id: 'PowerMeter:' + powerMeterId,
});
Power Meter returned first time
{
"powerMeter":{
"id":"7168adb4-4198-443e-ab76-db0725be2b18",
"name":"asd123123",
"registry":"as23",
"project":{
"id":"41d8e71b-d1e9-41af-af96-5b4ae9e492c1",
"name":"ProjectName",
"__typename":"Project"
},
"__typename":"PowerMeter"
}
}
Fragment after calling power meter first time
{
"id":"7168adb4-4198-443e-ab76-db0725be2b18",
"name":"asd123123",
"registry":"as23",
"project":{
"id":"41d8e71b-d1e9-41af-af96-5b4ae9e492c1",
"name":"ProjectName",
"__typename":"Project"
},
"__typename":"PowerMeter"
}
Power Meter returned second time
{
"powerMeter":{
"id":"7168adb4-4198-443e-ab76-db0725be2b18",
"name":"asd123123",
"registry":"as23",
"__typename":"PowerMeter"
}
}
Fragment after calling power meter second time
{
"id":"7168adb4-4198-443e-ab76-db0725be2b18",
"name":"asd123123",
"registry":"as23",
"project":{
"id":"41d8e71b-d1e9-41af-af96-5b4ae9e492c1",
"name":"ProjectName",
"__typename":"Project"
},
"__typename":"PowerMeter"
}
Edit 1: Fetching Query
The code below is how I'm fetching data. I'm using useApolloClient and not a query hook because I'm using AWS AppSync and it doesn't support query hook yet.
import { useApolloClient } from '#apollo/react-hooks';
import gql from 'graphql-tag';
import { useEffect, useState } from 'react';
export const getPowerMeterQuery = gql`
query getPowerMeter($powerMeterId: ID!) {
powerMeter: powerMeter(powerMeterId: $powerMeterId) {
id
name
registry
project {
id
name
}
}
}
`;
export const useGetPowerMeter = (powerMeterId?: string) => {
const client = useApolloClient();
const [state, setState] = useState<{
loading: boolean;
powerMeter?: PowerMeter;
error?: string;
}>({
loading: true,
});
useEffect(() => {
if (!powerMeterId) {
return setState({ loading: false });
}
client
.query<GetPowerMeterQueryResponse, GetPowerMeterQueryVariables>({
query: getPowerMeterQuery,
variables: {
powerMeterId,
},
})
.then(({ data, errors }) => {
if (errors) {
setState({ loading: false, error: errors[0].message });
}
console.log(JSON.stringify(data));
const frag = client.readFragment({
fragment: gql`
fragment P on PowerMeter {
id
name
registry
project {
id
name
}
}
`,
id: 'PowerMeter:' + powerMeterId,
});
console.log(JSON.stringify(frag));
setState({
loading: false,
powerMeter: data.powerMeter,
});
})
.catch(err => setState({ loading: false, error: err.message }));
}, [powerMeterId]);
return state;
};
Edit 2: Fetching Policy Details
When I use fetchPolice equals cache-first or network-only, the error persists. When I use no-cache, I don't get the error.
I think this might have been the solution:
https://github.com/apollographql/apollo-client/issues/7050
Probably way too late, but it could help people coming to this issue in the future.
When using apollo client's InMemoryCache it seems you need to provide a list of possible types so the fragment matching can be done correctly when using the InMemoryCache.
You can do that manually when having few union types and a pretty stable API which doesn't change very often.
Or you automatically generate these types into a json file, which you can use directly in the InMemoryCache's possibleTypes config directly.
Visit this link to the official docs to find out how to do it.
Cheers.

How to route after calling commit()

I am struggling a little with what to do after I call commit(). I want to determine how to route the user, depending on commit() being successful or if the server returns an error.
I read somewhere that if there is an error on the server then it can return a status code of >400 and errors as follows:
{ 'errors' : [ { 'errorCode' : [ 'duplicate-user' ] } ] }
On the client-side I have the following:
App.UsersController = Ember.ObjectController.extend({
createUser : function () {
'use strict';
var user = App.User.createRecord({
firstName : $("#firstName").val(),
lastName : $("#lastName").val(),
primaryEmailAddress : $("#primaryEmailAddress").val(),
password : $("#password").val()
}),
commitObserver = Ember.Object.extend({
removeObservers : function (sender) {
sender.removeObserver('isError', this, 'error');
sender.removeObserver('isValid', this, 'success');
},
error : function (sender, key, value) {
this.removeObservers(sender);
App.Router.router.transitionTo('duplicateuser');
},
success : function (sender, key, value) {
this.removeObservers(sender);
App.Router.router.transitionTo('usercreated');
}
});
user.get('transaction').commit();
user.addObserver('isError', commitObserver.create(), 'error');
user.addObserver('isValid', commitObserver.create(), 'success');
}
});
(Note: I am not using 'Ember.TextField' in my HTML hence the use of jQuery)
I have a few questions:
Is this the correct/best approach for handling commit()?
I've found I have to remove both observers as isValid is called after isError - is this to be expected?
How/can I access the server response as I want to be able to make a routing decision based on the error code?
The only way I can reference the router is through App.Router.router - is there a cleaner way?
If there is an error, do I need to do anything to remove the record from the store so it doesn't re-committed in the future?
From within a controller, you can do this:
this.get('target').transitionTo('another.route')
or, you can send an event to the current route and transition from there:
this.get('target').send('eventName');
or if you need to pass a model:
this.get('target').send('eventName', this.get('content'));
Simply use controller.transitionToRoute("your.route");
See this link for the source code...