I am having trouble with getting React+Apollo to update the store after I send a delete mutation. I am using the reactQL boiler plate which has apollo+react built in and an express graphQL server (I didn't install the apollo server- I just use the reference express-graphQL package). My data is stored in the mongoDB with a _id, but the actual data on the client side uses id as the id.
The apollo client is defined like this:
new ApolloClient(
Object.assign(
{
reduxRootSelector: state => state.apollo,
dataIdFromObject: o => o.id
},
opt
)
);
I have a parent component which uses
import courses from 'src/graphql/queries/courses.gql
#graphql(courses)
export default class CoursesPage extends Component {
constructor(props){
super(props)
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete(event) {
this.props.mutate({ variables: {id: selectedId}}
.catch(err => console.log(err))
}
render() {
return (
{ this.props.data.courses.map(course =>
<CourseDelete selectedId={course.id} key={course.id} />
})
}
)
}
}
and a child component that looks like:
import deleteCoursefrom 'src/graphql/mutations/deleteCourse.gql
#graphql(deleteCourse)
export default class CourseDelete extends Component {
constructor(props){
super(props)
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete(event) {
this.props.mutate({ variables: {id: this.props.selectedId}}
.catch(err => console.log(err))
}
render() {
return (
<button onClick={this.handleDelete}>Button</button>
)
}
}
where deleteCourse.gql:
mutation deleteCourse($id: String!) {
deleteCourse(id: $id) {
id
}
}
and my original query is in courses.gql:
query courses {
courses {
id
}
}
Apollo's dataIdFromObject is used to update objects already in the cache. So if you have a record and an ID, and you change other pieces of data against that same ID, React components listening to the store can can re-render.
Since your deleteCourse mutation seems to return the same ID, it still exists in the cache. Your store doesn't know it needs deleting- it just updates the cache with whatever data comes back. Since this mutation likely returns the same ID, there's nothing to signify that this should be removed.
Instead, you need to specify an update function (link goes to the official Apollo docs) to explicitly delete the underlying store data.
In my new ReactQL user auth example, I do the same thing (see the pertinent LOC here) to 'manually' update the store after a user logs in.
Since components are initially listening to a 'blank' user, I cannot rely on dataObjectFromId to invalidate the cache, since I'm starting with no users and therefore no IDs. So I explicitly overwrite store state manually, which triggers re-rendering of any listening components.
I explain the concept is the context of the above user auth in a YouTube video - this is the piece that's relevant: https://youtu.be/s1p4R4rzWUs?t=21m12s
Related
Good day everyone, I am working with watermelondb and I have the code below, but I don't know how to actually use it. I am new in watermelondb and I don't know how to pass data as props to the pullChanges and pushChanges objects. How do I pass necessary data like changes and lastPulledAt from the database into the sync function when I call it. And I need more explanation on the migrationsEnabledAtVersion: 1 too. Thanks in advance for your gracious answers.
import { synchronize } from '#nozbe/watermelondb/sync'
async function mySync() {
await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
const urlParams = `last_pulled_at=${lastPulledAt}&schema_version=${schemaVersion}&migration=${encodeURIComponent(JSON.stringify(migration))}`
const response = await fetch(`https://my.backend/sync?${urlParams}`)
if (!response.ok) {
throw new Error(await response.text())
}
const { changes, timestamp } = await response.json()
return { changes, timestamp }
},
pushChanges: async ({ changes, lastPulledAt }) => {
const response = await fetch(`https://my.backend/sync?last_pulled_at=${lastPulledAt}`, {
method: 'POST',
body: JSON.stringify(changes)
})
if (!response.ok) {
throw new Error(await response.text())
}
},
migrationsEnabledAtVersion: 1,
})
}
Watermelondb's documentation is terrible and its link to typescript even worse.
I spent almost a week to get 100% synchronization with a simple table, now I'm having the same problems to solve the synchronization with associations.
Well, the object you need to return in pullChanges is of the following form:
return {
changes: {
//person is the name of the table in the models
person: {
created: [
{
// in created you need to send null in the id, if you don't send the id it doesn't work
id: null,
// other fields of your schema, not model
}
],
updated: [
{
// the fields of your schema, not model
}
],
deleted: [
// is a string[] consisting of the watermelondb id of the records that were deleted in the remote database
],
}
},
timestamp: new Date().getTime() / 1000
}
In my case, the remote database is not a watermelondb, it's a mySQL, and I don't have an endpoint in my API that returns everything in the watermelon format. For each table I do a search with deletedAt, updatedAt or createdAt > lastPulledAt and do the necessary filtering and preparations so that the data from the remote database is in the schema format of the local database.
In pushChanges I do the reverse data preparation process by calling the appropriate creation, update or deletion endpoints for each of the tables.
It's costly and annoying to do, but in the end it works fine, the biggest problem is watermelon's documentation which is terrible.
using RTK Query, how can one use the fetching functionality to update another the state of another slice?
Essentially I'm trying to keep all related-state next to each other, and thus after querying data with the useLazyGetQuery, I'd like to use the result and store it in an existing slices state.
Whilst something like the below works from an component, it makes the whole code messy
const [trigger, result, lastArg] = useLazyGetFiltersQuery();
React.useEffect(() => {
if (result.status === 'fulfilled' && result.isSuccess === true && result.isLoading === false) {
dispatch(updateData(result.data.map((pg) => ({ label: pg, value: pg }))));
}
}, [dispatch, result, result.isLoading, result.isSuccess, result.status]);
I'd rather want to setup some logic directly in the createApi method so that the resulting data is stored from the beginning in the slice I want.
Thanks
Generally, I would advise against copying data out of RTK-Query unless you have a very good reason: you will have to hand-write a lot of stuff, pretty much defeating the purpose of RTK-Query and you lose stuff like automatic cache collection of unused cache entries - from that point on you have to do that yourself.
Also, that data is already in Redux. Usually, you should not duplicate data in the store when you can avoid it.
That said, of course you can do it. For example, it is shown in the "Using extraReducers" authentication example:
const slice = createSlice({
name: 'auth',
initialState: { user: null, token: null } as AuthState,
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(
api.endpoints.login.matchFulfilled,
(state, { payload }) => {
state.token = payload.token
state.user = payload.user
}
)
},
})
Keep in mind: this is Redux and everything happens via actions. You will never have to useEffect something to write it back into state. Just listen for the right actions in your reducers.
I wrote a small application that subscribes to DB changes using AWS Amplify CLI / AppSync. All amplify api calls work perfectly (mutations, queries) but unfortunately the observer does not receive events. I can see that the MQTT socket receives periodically binaries but I can't obtain changed objects.
I configured Amplify for amplify use. I can see in the debugger that the AppSyncProvider has been initisalised. Also tried API and PubSub but makes no difference.
const awsmobile = {
"aws_appsync_graphqlEndpoint": "https://[...].appsync-api.[...]/graphql",
"aws_appsync_region": "[...]",
"aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
};
Amplify.configure(awsmobile);
ngOnInit()
{
try {
this.apiService.OnUpdateA.subscribe(
{
next: (x) => {[...]},
error: (e) => {[...]},
complete: () => {[...]}
});
}
catch (error) {[...] }
}
***Schema***
type A
#model
#auth(rules: [
{allow: owner},
{allow: groups, groups: ["A"], operations: [create, update, read]},
{allow: groups, groups: ["B"], operations: [read]},
])
{
id: ID!
entry: AnotherType! #connection(name: "AnotherConnection")
[...]
}
OnUpdateAListener: Observable<
OnUpdateASubscription
> = API.graphql(
graphqlOperation(
`subscription OnUpdateA($owner: String) {
onUpdateA(owner: $owner) {
__typename
id
owner
[...]
}
}`
)
) as Observable<OnUpdateASubscription>;
Anyone for any ideas?
**Logs:**
{mqttConnections: Array(1), newSubscriptions: {…}, provider: Symbol(INTERNAL_AWS_APPSYNC_PUBSUB_PROVIDER)}
mqttConnections: Array(1)
0: {url: "wss://[...]-ats.iot.[...].amazonaws…[...]%3D%3D", topics: Array(2), client: "[...]"}
length: 1
__proto__: Array(0)
newSubscriptions:
onUpdate:
expireTime: 1573313050000
topic: "[....]/22tmaezjv5555h4o7yreu24f7u/onUpdate/1cd033bad555ba55555a20690d3e04e901145776d3b8d8ac95a0aea447b273c3"
__proto__: Object
__proto__: Object
provider: Symbol(INTERNAL_AWS_APPSYNC_PUBSUB_PROVIDER)
__proto__: Object
However, not sure whether it is suspicious that the subscription Object has no queue?
Subscription {_observer: {…}, _queue: undefined, _state: "ready", _cleanup: ƒ}
_cleanup: ƒ ()
_observer:
next: (x) => {…}
__proto__: Object
_queue: ***undefined***
_state: "ready"
closed: (...)
__proto__: Object
Many thanks in advance.
For those who are experiencing the same behaviour. It was due to the fact that I had the owner in the auth section of the schema. Deleted the {allow: owner}, part and the subscriptions started to work immediately.
here is a working example of AWS Amplify Subscriptions:
import Amplify from 'aws-amplify';
import API from '#aws-amplify/api';
import PubSub from '#aws-amplify/pubsub';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
API.configure(awsconfig);
PubSub.configure(awsconfig);
// put above in root
// below is example
import { API, graphqlOperation } from 'aws-amplify';
var onAddNote = `subscription OnCreateNote {
onCreateNote {
id
patient {
id
organization {
id
}
}
}
}
`;
listenForNoteAdd() {
return API.graphql(graphqlOperation(onAddNote) ).subscribe({next: (noteData) => {
console.log("new note so reload consider reload")
let note = noteData.value.data.onCreateNote
console.log(JSON.stringify(note))
// now that you have indication of something happening
// do what you must next
}})
}
I had the same problem with GraphQL. Thing is: we have to return Owner in mutation response in order to let Subscription know to whom to send this event.
removing {allow: owner} did worked for me but thats not the right way since we require it in order to have owner based access to data.
so the correct way i found is:
if subscription is:
subscription MySubscription {
onCreateuser(owner: "$userName") {
id
name
number
}
}
mutation should be:
mutation MyMutation {
createUser(input: {name: "xyz", id: "user123", number: "1234567890"}) {
id
name
number
owner
}
}
we must return owner in mutation's response in order to get the subscription to that event and all other properties as same as mutation response.
For those coming here experiencing this error, do not delete {allow: owner}. Allow owner ensures only a user authenticated with Cognito User Pool can run queries, mutations, etc.
It looks like the OP is using amplify codegen to generate his API service, and if you look the listener has param for owner. It's optional, but if your #auth is {allow: owner} it is required.
Additional note: Not sure if he is using the correct owner field stored in his datastore or not. If there is not already an owner field created (or a different field specified), it will create one with a unique uuid. So he could be passing the incorrect owner or none at all.
Run a simple call to get the owner...
export const GetOwner = async (id: string): Promise<GetOwnerQuery> => {
const statement = `query GetAppData($id: ID!) {
getAppData(id: $id) {
owner
}
}`;
const gqlAPIServiceArguments: any = {
id,
};
const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any;
return <GetOwnerQuery>response.data.getAppData;
};
...and pass that in your subscription.
const { owner } = await GetOwner(id);
if (owner) {
this.apiUpdateListener$ = this.api.OnUpdateAppDataListener(owner).subscribe((data) => {
console.log('api update listener ===> ', data);
});
}
I have a model that is scattered all around the application. I have a redux state tree:
{
page: {
modelPart1: ...,
... : {
modelPart2: ...
}
}
I need to keep a reference to mongoDb __v in my state too. Where is the best place to place it?
I was thinking about a separate branch model_metadata that would keep the metadata about docs (_id, __v, ...).
{
model_metadata: { <------------------------ HERE
model: {
_id: id,
__v: 2
}
}
page: {
modelPart1: ...,
... : {
modelPart2: ...
}
}
Is it a valid approach or would you recommend a different one?
Every reducer only can access its own part of state, so when you do
combineReducers({
one,
another
});
and access state in one, it is equivalent to doing store.getState().one, and the same for another. So, you need to split the data in page property of state into two parts: actual data and metadata. Just like the object you retrieve from Mongo.
The point in having metadata and actual data being processed by the same reducer is that every time a reducer function is performed, you have everything you need about your object in state argument of that function. Splitting the data into two different reducers would make things way more complicated.
So, the new data representation in page would look like
{
model_metadata: { <------------------------ HERE
model: {
_id: id,
__v: 2
}
}
page: {
modelPart1: ...,
... : {
modelPart2: ...
}
}
while connecting to page would look like
connect(state => ({
page: state.page
})(...)
I have two collections: users and tradeOffers. Each user has a unique steamid field. Each tradeOffer has field recipient.steamid. On server I publish tradeOffers collection like this:
import { Meteor } from 'meteor/meteor';
import { TradeOffers } from './collection';
if (Meteor.isServer) {
Meteor.publish("tradeOffers", function () {
let user = Meteor.users.findOne({_id: this.userId });
return TradeOffers.find({
'recipient.steamid': user.services.steam.steamid
});
});
}
On client side I subscribe to this collection and display current user's trade offers. Everything works fine until I update the user. Whenever current user is updated, data disappears from the view. After page reload I can see trade offers again. Any help would be appreciated.