AWS AppSync mutation rerendering the view on mutation - reactjs

I have test app where I'm subscribing to a list of todos. But my add new todo mutation also optimistically updates the UI.
The problem is it's now refreshing the entire react view and I have no idea why.
This is the culprit, it happens when I run this mutation:
export default graphql(addItem, {
options: {
fetchPolicy: 'cache-and-network'
},
props: props => ({
onAdd: item =>
props.mutate({
variables: item,
optimisticResponse: {
__typename: 'Mutation',
addItem: { ...item, __typename: 'Item' }
},
update: (proxy, { data: { addItem } }) => {
let data = proxy.readQuery({ query: listItems });
data.listItems.items.push(addItem);
proxy.writeQuery({ query: listItems, data });
}
})
})
})(AddItem);

It turns out that mutating items locally without all the queried fields (in my case "created" is added by the server) causes the entire query mutation to fail but silently. There's a fix on the way: https://github.com/apollographql/apollo-client/issues/3267
Hopefully this helps others starting out!

Related

Undo action in RTK query for optimistic update

I am using React with Redux Toolkit and RTK Query. For optimistic updates, I would need possibility to undo any dispatched action. RTK Query allows this using undo() method on dispatch result.
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query'
const api = createApi({
endpoints: (build) => ({
// Action for getting the list of all items.
getItems: build.query({ query: () => 'items' }),
// Action for adding new item.
addItem: build.mutation({
query: (body) => ({ url: 'item', method: 'POST', body }),
onQueryStarted({ id, ...body }, { dispatch, queryFulfilled }) {
const patch = dispatch(api.util.updateQueryData('getItems', undefined, (items) => [...items, body]))
queryFulfillled.catch(patch.undo) // On fail, undo the action (so remove the added item)
}
})
})
})
However, the problem is when I have an array of items, to which some action adds new item. How can I revert it? undo method just set array to the state in which it was before dispatching the action. But what if I dispatch the action multiple times at same time? All items will be removed, even when just 1 fails.
// items: []
dispatch(addItem({ id: '1' }))
// items: [{ id: '1' }]
dispatch(addItem({ id: '2' }))
// items: [{ id: '1' }, { id: '2' }]
// Now, first dispatch asynchronously fail, so undo the first action
// items: [], but should be [{ id: '2' }]
Of course, I can make undo myself and manually modify the list of items. But is there any way how to automate undo action for arrays? If adding of some items fail, undo (remove) only this item, not the others.

Vue.js Object in array undefined in data from store

Sorry if this is really obvious but I’m new to Vue and could use some help.
I’m grabbing an array of data (posts) from my store and trying to console log just one of the objects in the array, but it’s showing undefined every time. If I console log the whole array it returns fine.
I’m guessing this is something to do with the data not loading before the console.log in the created hook? I’ve tried everything I can and it’s driving me nuts. Any help appreciated (simplified code below).
<script>
export default {
components: {},
computed: {
posts() {
return this.$store.state.posts;
}
},
created() {
this.$store.dispatch("getPosts");
console.log(this.posts[0])
},
};
</script>
//Store code Below
export const state = () => ({
posts: [],
})
export const mutations = {
updatePosts: (state, posts) => {
state.posts = posts
}
}
export const actions = {
async getPosts({
state,
commit,
dispatch
}) {
if (state.posts.length) return
try {
let posts = await fetch(
`${siteURL}/wp-json/wp/v2/video`
).then(res => res.json())
posts = posts
.filter(el => el.status === "publish")
.map(({
acf,
id,
slug,
video_embed,
title,
date,
content
}) => ({
acf,
id,
slug,
video_embed,
title,
date,
content
}))
commit("updatePosts", posts)
} catch (err) {
console.log(err)
}
}
}
Console behavior explained
When you log an object or array to the console, then click to expand/view the properties, the console shows you the properties as they are now, at the time of the click, not the time of the log. So if they've changed after logging, you'll see the new values.
How? Since objects and arrays are reference variables, the console stores the reference, and then updates itself when you click. This is only true of objects and arrays.
Conversely, When you log a primitive to the console, you see it only as it was at the time of the log.
In Chrome, you will also see a little blue square next to the object in the console. When you mouse over, it tells you, "Value below was evaluated just now."
This is why you never saw the value when logging one item, because it was an item that didn't exist yet. But posts always has a reference, since it's initialized to an empty array. So when logging the posts reference, by the time you click, the data has arrived.
Here is a demo that tries to make that very clear.
you get an undefined because the asynchronous functions have not yet filled the state.
With asynchronous data you should always use getters.
getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.
Vuex Getters
// Store
export const getters = {
get_posts(state) {
return state.posts;
}
}
-
// component
computed: {
posts() {
return this.$store.getters['get_posts'];
}
};
You need to use promises as per the docs https://vuex.vuejs.org/guide/actions.html#composing-actions. Your action was not returning its promise to get data, nor were you waiting for the promise to resolve before you console.logged the result. Promises are a very important concept to master. Here's an example that basically matches your code, though I used setTimeout instead of a real fetch call.
const store = new Vuex.Store({
state: {
posts: []
},
getters: {
itemList: (state) => {
return state.items;
}
},
mutations: {
updatePosts: (state, posts) => {
state.posts = posts
}
},
actions: (actions = {
async getPosts({ state, commit, dispatch }) {
if (state.posts.length) {
return Promise.resolve();
}
try {
return new Promise(resolve => setTimeout(() => {
const posts = [{ id: 42 }];
commit("updatePosts", posts);
resolve();
}, 1000));
} catch (err) {
console.log(err);
}
}
})
});
const app = new Vue({
el: "#app",
computed: {
posts() {
return this.$store.state.posts;
}
},
created() {
this.$store.dispatch("getPosts").then(() => console.log(this.posts[0]));
},
store
});
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<script src="https://unpkg.com/vuex#2.5.0/dist/vuex.js"></script>
<div id="app">
</div>
If you want to get the correct data on created() hook, the action should be executed before displaying the data.
In created() hook, you can dispatch the action asynchronously.
async created() {
await this.$store.dispatch("getPosts");
console.log(this.posts[0])
},
You can use JavaScript Promises.
async created() {
this.$store.dispatch("getPosts").then(()=> {
console.log(this.posts[0]);
});
},

React: Data not showing until page refreshes

I am currently building a simple CRUD workflow using React and GraphQL. After I create an object (an article in this case which just has an id, title and description.), I navigate back to an Index page which displays all of the currently created articles. My issue is that after an article is created, the index page does not display the created article until I refresh the page. I am using apollo to query the graphql api and have disabled cacheing on it so I'm not sure why the data isn't displaying. I've set breakpoints in my ArticlesIndex's componentDidMount function and ensured that it is executing and at the time of executing, the database does include the newly added article.
My server side is actually never even hit when the client side query to retrieve all articles executes. I'm not sure what is cacheing this data and why it is not being retrieved from the server as expected.
My ArticlesCreate component inserts the new record and redirects back to the ArticlesIndex component as follows:
handleSubmit(event) {
event.preventDefault();
const { client } = this.props;
var article = {
"article": {
"title": this.state.title,
"description": this.state.description
}
};
client
.mutate({ mutation: CREATE_EDIT_ARTICLE,
variables: article })
.then(({ data: { articles } }) => {
this.props.history.push("/articles");
})
.catch(err => {
console.log("err", err);
});
}
}
then my ArticlesIndex component retrieves all articles from the db as follows:
componentDidMount = () => {
const { client } = this.props; //client is an ApolloClient
client
.query({ query: GET_ARTICLES })
.then(({ data: { articles } }) => {
if (articles) {
this.setState({ loading: false, articles: articles });
}
})
.catch(err => {
console.log("err", err);
});
};
and I've set ApolloClient to not cache data as in my App.js as follows:
const defaultApolloOptions = {
watchQuery: {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
}
export default class App extends Component {
displayName = App.name;
client = new ApolloClient({
uri: "https://localhost:44360/graphql",
cache: new InMemoryCache(),
defaultOptions: defaultApolloOptions
});
//...render method, route definitions, etc
}
Why is this happening and how can I solve it?
It looks like this is an issue with ApolloBoost not supporting defaultOptions as noted in this github issue. To resolve the issue I changed:
const defaultApolloOptions = {
watchQuery: {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
}
export default class App extends Component {
displayName = App.name;
client = new ApolloClient({
uri: "https://localhost:44360/graphql",
cache: new InMemoryCache(),
defaultOptions: defaultApolloOptions
});
//...render method, route definitions, etc
}
To:
const client = new ApolloClient({
uri: "https://localhost:44360/graphql"
});
client.defaultOptions = {
watchQuery: {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
};
export default class App extends Component {
//....
}
I can see that you are getting the data and setting your components state on initial mount. Most probably when you redirect it doesn't fire the componentDidMount lifecylcle hook as it is already mounted, if that is the issue try using componentDidUpdate lifecycle hook so that your component knows there was an update and re-set the data.

How to pass multiple queries into refetchQueries in apollo/graphql

I have a mutation named deleteSong. I was wondering after the mutation has passed through how can I pass multiple queries into refetchQueries?
this.props
.deleteSong({
variables: { id },
refetchQueries: [{ query: fetchSongs }] //<-- I only know how to pass 1 query
})
.then(() => {})
.catch(err => {
this.setState({ err });
});
The Apollo Client documentation says the following about refetchQueries: A function that allows you to specify which queries you want to refetch after a mutation has occurred. So it should be possible for you to pass either a single or multiple queries to refetchQueries().
Basically it should be:
this.props
.deleteSong({
variables: { id },
refetchQueries: [{ query: FETCH_SONGS }, { query: FETCH_FOLLOWERS }]
})
.then(() => {})
.catch(err => {
this.setState({ err });
});
Whereas FETCH_SONG and FETCH_FOLLOWERS should be defined by graphql-tag. Maybe let others know if this solution works for you.

React Apollo Component props in `options.update` method

guys, need your help
I have the following question:
Imagine, we have a blog website. We have an author page with lists of posts he created /author/:authorId. And author wants to add another post to the end of this list.
We have a mutation for this:
mutation PostCreateMutation($id: ID!, $title: String!) {
createPost(id: $id, title: $title, text: $text) { … }
}
And after the mutation is called, i want to update UI without refetching all posts. I cannot use options.update method, because my update function looks like this:
update: (proxy, { data: { createPost } }) => {
const data = proxy.readQuery({
query: AUTHOR_QUERY,
variables: {
id: ‘1’ // => i cant get authorId here =(
}
});
}
I cant get authorId in update method, because I don't have an access to component props there… How should i handle this?
In Apollo, options can be a function instead of an object. So the config object you pass to your HOC can look like this:
{
options: ({authorId}) => ({
update: (proxy, { data: { createPost } }) => {
const data = proxy.readQuery({
query: AUTHOR_QUERY,
variables: {
id: authorId,
}
});
}
}),
}

Resources