Apollo can't access queryVariables in update: after a mutation - reactjs

I am trying to use update: to update a query after performing a mutation. The problem is that the query in the store has several different variables applied and I would like to update the query and return it with the same variables.
I found in the docs that updateQueries has an option to include queryVariables which are the last set of variables that the query was executed with.
I have not found anything that describes how to retrieve the queryVariables or something like it from inside of update.
Inside of update: I can use
lastQuery = Object.keys(store.data.ROOT_QUERY).slice(-1)[0]
which will return a result like "userRecipes({"first":20,"minTime":0,"maxTime":500,"filterType":"Explore","searchTerm":""})"
The hacky way that I am doing this now is to parse that string to pull out the variables so I can finally use readQuery like so:
const lastQuery = Object.keys(store.data.ROOT_QUERY).slice(-1)[0]
const searchPosition = lastQuery.search("searchTerm")
const searchTerm = lastQuery.slice((searchPosition + 13),-3)
// also parsing the lastQuery string for filterType, minTime, maxTime
const data = store.readQuery({
query: QUERY_USER_RECIPES,
variables: {
filterType: filterType,
searchTerm: searchTerm,
minTime: minTime,
maxTime: maxTime,
}
});
This can't be the best way to do this. Is there a simpler way to access variables inside of update?
It seems like there should be a way to read the existing query and variables that are in the store without passing variables with readQuery.
Thanks for taking a look at this issue!
Version
apollo-client#1.4.0
react-apollo#1.4.2

For apollo 2, but should be the same in 1.x
In the docs, you see that you can also pass variables to readQuery.
Here is an example where a user can book an event clicking a BookEvent component, if the mutation succeeds, it is reflected automatically in the upper component EventDetail.
In the component that tiggers the mutation (BookEvent), I pass store and eventId to a function declared in the upper component (EventDetail) and passed through props of the child component:
const onClick = () => createEventTicketMutation({
variables: { eventId: event.id },
update: (store, { data: { createEventTicket } }) => {
updateStoreAfterBooking(store, event.id)
},
})
Here is the function that performs the cache update in the upper component:
const updateCacheAfterBooking = (store, eventId) => {
const data = store.readQuery({
query: EVENT_DETAIL_QUERY,
variables: { id: eventId },
})
data.eventDetail.bookings += 1
store.writeQuery({
query: EVENT_DETAIL_QUERY,
variables: { id: eventId },
data,
})
}
It is passed like so <BookEvent updateStoreAfterBooking={updateCacheAfterBooking} ... />.
Don't forget to pass also the needed variables to writeQuery.

Related

Graphql - Apollo Client/ React - Cache - fetchMore doesn't update the data state of the query

I try to implement cached pagination, in my react app using apollo client.
my query has filter argument, which should be the only argument that create a new key in the cache object.
for some reason, when fetchMore occurs with filter specified, the new data doesn't cause a re-render in the component.
I logged the existing and incoming argument in the merge function, and it seems that for each fetchMore that had filter, new data did arrive. so, i don't understand why the component didn't re-render.
to make things worst: calling fetchMore several times with or without filter send http request and merging the incoming data with the existing data. which i'd expect wouldn't happen as the client should see that it already has a key in the cache for that query with that key argument.
the following is the query:
query Shells($first: Int = 5, $offset: Int = 0, $filter: ShellFilter) {
shells(
orderBy: [STATUS_ASC, EXECUTION_FROM_DESC]
first: $first
offset: $offset
filter: $filter
) {
nodes {
...ShellData
}
totalCount
}
}
the apolloClient config is like this:
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
shells: {
keyArgs: ['filter'],
merge: (existing, incoming) => {
console.log('existing:', existing, 'incoming:', incoming);
return mergeObjectsAndNestedArrays<ShellsConnection>(
existing,
incoming,
'nodes',
);
},
},
},
},
},
})
and the component that displays it:
const ControlCenter = () => {
const { showModal } = useModalContext();
const [page, setPage] = useState(1);
const { data, loading, fetchMore } = useShellsQuery();
const [query, setQuery] = useURLQuery();
const onCounterpartiesChange = async (counterparties) => {
await fetchMore({
variables: {
filter: { shellCounterParties: { some: { orgId: { in: '20584' } } } },
},
});
setQuery({ counterparties });
};
const shells = data?.shells?.nodes;
console.log('hello from shells:', shells);
these are the logs:
EDIT 1 - docs reference
Following the docs: https://www.apollographql.com/docs/react/pagination/key-args/#setting-keyargs
any argument can be used as the keyArgs: limit, offset and filter.
In the documentation examples, the arg used as the key is a primitive value, but in your case, the filter arg is an object. This could be causing apollo to see all results as the same cached version. If your data depend only on the orgID I think you could try the nested array notation to set that field as the key.
keyArgs: ["filter", ["shellCounterParties", ["some", ["orgId", ["in"]]]]]
or the custom function
keyArgs: (args, context) => args.filter.shellCounterParties.some.orgId.in
If you really need to cache according to the whole filter object, I guess the simplest way would be stringifying it
keyArgs: (args, context) => JSON.stringify(args.filter)
But to be sure how apollo is caching the data, I highly recommend you to try the apollo devtools
related: https://github.com/apollographql/apollo-client/issues/7314
I think the problem lies where you have defined typePolicies in your code with keyArgs: ['filter'].
Please check official docs:
https://www.apollographql.com/docs/react/caching/cache-configuration/#customizing-cache-ids
You can customize how the InMemoryCache generates cache IDs for individual types in your schema
This is helpful especially if a type uses a field (or fields!) besides id or _id as its unique identifier.
Based on this, you have defined filter as a unique identifier even though that is a variable which is used for filtration purpose. It is not a field to customize the cache but a variable.
Note that these keyFields strings always refer to the actual field names as defined in your schema, meaning the ID computation is not sensitive to field aliases.
My suggestion first of all would be to modify the configuration that you have set up and see if it helps?
Instead of fetchMore use refetch inside useEffect and pass there new variables
function photo({ id }) {
const { data, refetch } = useQuery(GET_PHOTO, {
variables: { id },
});
useEffect(() => {
refetch({ id })
}, [id])
}

How to extract selectors from adapter from queries with arguments

I'm trying to extract selectors from queries in my apiSlice as said in this documentation: https://redux.js.org/tutorials/essentials/part-8-rtk-query-advanced
The documentation put this example:
const usersAdapter = createEntityAdapter()
const initialState = usersAdapter.getInitialState()
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUsers: builder.query({
query: () => '/users',
transformResponse: responseData => {
return usersAdapter.setAll(initialState, responseData)
}
})
})
})
export const { useGetUsersQuery } = extendedApiSlice
// Calling `someEndpoint.select(someArg)` generates a new selector that will return
// the query result object for a query with those parameters.
// To generate a selector for a specific query argument, call `select(theQueryArg)`.
**// In this case, the users query has no params, so we don't pass anything to select()**
export const selectUsersResult = extendedApiSlice.endpoints.getUsers.select()
const selectUsersData = createSelector(
selectUsersResult,
usersResult => usersResult.data
)
export const { selectAll: selectAllUsers, selectById: selectUserById } =
usersAdapter.getSelectors(state => selectUsersData(state) ?? initialState)
Now, i need to have selectById selector (that by default takes userid as second parameter).
I can't manage to have a working selectById selector when my query looks like this
endpoints: builder => ({
getUsers: builder.query({
query: (applicationId) => `/application/${applicationId}/users`,
transformResponse: responseData => {
return usersAdapter.setAll(initialState, responseData)
}
})
})
How do I extract selectById selector from adapter and how i use it in a component with useSelector when I have this kind of query with arguments?
Thanks anyone that will help me
I feel like you mismatch concepts a bit, so it leads to confusion.
First of all, let's clarify, that's selectors you a using- it's not a selector to your state in usersAdapter, but to RTK-Q's own state.
By handling a response in transformResponse you are just copying the data from it to your's, usersAdapter's state.
So, considering that, you should be specific about which state you are going to select from. If you want it from the "final destination", i.e. from the adaptor's state, it's should be done via:
const usersSelectors = **usersAdapter**.getSelectors(
(state) => state.users // or something like that
)
Otherwise, using the selectors from apiSlices like extendedApiSlice from your example - you are fetching the data from RTK-Q's cached state, which may not contain some old data after the cache invalidation. If it's still your goal, the limitation is that RTK-Q's store isn't a normalized store you may expect, with ids and values, but rather the key-value pairs, where keys are your requests, and the values - last results (users arrays in your case). So, if you have no API endpoint defined for selecting a particular user by ID, you won't be able to select it from RTK-Q's state directly. But you may select the cached users by applicationId, and find your user by id in the result array. I bet it's not what you actually want, so most probably you need just to prepare selectors for your own store, as I've mentioned above.

Why react useQuery() doesnt fetch the newest data after a mutation?

I have a code like this
const [inputComment, setInputComment] = useState('');
const [
commentPost,
{ data: data4, loading: loading4, errorCreate4 },
] = useMutation(COMMENT_POST);
const { error: error2, loading: loading2, data: data2 } = useQuery(
GET_POST_BY_ID,
{
variables: {
postid: item.id,
},
},
);
const doComment = () => {
commentPost({
variables: {
postId: item.id,
userEmail: email,
comment: inputComment,
},
})
.then(({ data }) => {
setInputComment('');
console.log('success');
})
.catch((e) => {
console.log('not success');
});
};
This is supposed to get the data, and when I do comment then it runs the mutation and re-render everything.
My problem is, it re-render alright BUT the data that the useQuery fetch is not the newest data a.k.a the data before I add a new comment.
Does anyone know how to fix this problem??
Please help :(
Your mutation modifies data on the server side.
Once your mutation is done, you should refetch your data in order to get the modified version in your local cache on the client side.
By guessing how your mutation and query actually work, here is how it would look like:
const [
commentPost,
{ data: data4, loading: loading4, errorCreate4 },
] = useMutation(COMMENT_POST, {
refetchQueries: [
{ query: GET_POST_BY_ID, variables: { postid: item.id } }
]
});
Otherwise, intead of refetching from the server, you could update the local cache directly.
More info can be found here in the official documentation.
I assume commentPost is an insert operation, not an update of a single record. In this case, Apollo useMutation will not update the cache for you. You need to modify the cache yourself. The official Apollo documentation has covered this use case with an example. You may want to revise the usage of writeFragment as well.
Below are directly from apollo docs on cache update for list fields.
In most cases, a mutation response should include any object(s) the
mutation modified. This enables Apollo Client to normalize those
objects and cache them according to their __typename and id fields (by
default).
...
When a mutation's response is insufficient to update all modified
fields in your cache (such as certain list fields), you can define an
update function to apply manual changes to your cached data after a
mutation.
const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data: { addTodo } }) {
cache.modify({
fields: {
todos(existingTodos = []) {
const newTodoRef = cache.writeFragment({
data: addTodo,
fragment: gql`
fragment NewTodo on Todo {
id
type
}
`
});
return [...existingTodos, newTodoRef];
}
}
});
}
});
EDIT
I noticed another answer suggests using refetch, which is not a bad option for starters. However, updating the cache is the recommended approach over refetch. You can refer to the Apollo blog article When To Use Refetch Queries in Apollo Client.
Below are some quotes you should note from this article.
If you’re just getting started with GraphQL, I think the mental model of passing in the queries that you’d like to re-run after a mutation is an easy one to wrap your head around.
...
The advantage here is that this approach is straightforward. The disadvantage is that we’re fetching the entire list of data again when we might not need to.
...
For a more efficient use of bandwidth and network round-trips, we can rely on cache normalization and update functions.

Apollo Mutation - UI not updated after useMutation update

Background: always able to make Apollo store cache updated, but not UI
Question:
what's the reason that makes the UI updated or not?
what's the right way to pass the data object in update?
"react": "~16.9.0"
"#apollo/react-hooks": "^3.1.3"
Both UI and cache updated codes in my project:
update: (store, { data: { newPhoto } }) => {
const { bookInfo } = store.readQuery({ query: GET_BOOK_BY_ID, variables: { bookId } });
bookInfo.photos = [...bookInfo.photos, newPhoto];
store.writeQuery({
query: GET_BOOK_BY_ID,
variables: { bookId },
data: {
bookInfo
}
});
}
In this line: bookInfo.photos = [...bookInfo.photos, newPhoto];, the bookInfo object is amended directly and just passed back to writeQuery's data
This doesn't look okay to me as I saw people saying it needs to be "immutable" or "passing new object", etc.
if you experience the same thing, please check following list:
go to check out https://github.com/apollographql/apollo-client/pull/4543. By applying the freezeResults & assumeImmutableResults into the ApolloClient will help detect the issue. For my case, the issue actually occurs inside the parent component, which I mutated the Apollo store objects, instead of the component that calling client.writeQuery, which is generally hard for others to notice too, in my opinion.
const client = new ApolloClient({
link: ...,
cache: new InMemoryCache({
freezeResults: true, // new
}),
assumeImmutableResults: true, // new
});
Ensure you mutate the data in an immutable fashion. (i.e. Apollo store object hasn't been changed until the end of update) https://github.com/immerjs/immer definitely helps to keep your change in immutable fashion. I used this to mutate my nested object and it's working so well.
Try to use client returned from the useMutation, then you get client.writeQuery to do the update. Although I'm not sure about this point, a lot of people spreading this message, probably help in some cases.
import { useMutation } from '#apollo/react-hooks';
import produce from "immer";
const [mutate, { client }] = useMutation(MUTATION_GQL);
const submit = () => {
mutate({
variables: { inputs },
update: (store, { data: { response }) => {
// get existing cache returned from the query
const cache = store.readQuery({ query: QUERY_GQL, variables: { id } });
// manipulate the cache in immutable fashion
const data = produce(cache, draftCache => {
draftCache.title = "new title";
draftCache.approval = response;
});
// write the cache back to that query
// REMEMBER the variables inside writeQuery too!
client.writeQuery({
query: QUERY_GQL,
variables: { id },
data,
});
}
})
}
Try to use useQuery to read the data from ApolloClient instead of readQuery so you will get the updated cache from Apollo's store

RefetchQueries does not correctly update the store

Intended outcome:
I am trying to refetch a query after a mutation. This works perfectly everywhere but I have a problem with this particular query.
const alertsSettingsQuery = gql`
query alertsSettingsQuery($issuerRoleNames: [String], $receiverRoleNames: [String]) {
alertsSettings(issuerRoleNames: $issuerRoleNames, receiverRoleNames: $receiverRoleNames) {
...
}
}
`;
Component querying the initial datas without variables :
export default compose(
graphql(alertsSettingsQuery, {name: "alertsSettingsData"}),
...,
)(Administration);
Component refetching the datas without variables :
mutate({
variables: updatedAlert,
refetchQueries: () => [{query: alertsSettingsQuery}]
}).then((data) => {
...
}).catch(err => {
...
});
Actual outcome:
If I check the network tab, the datas are correctly refetched and updated but the component doesn't receive the updated datas.
If I use the apollo devtools to see what's in my cache, I have this for the original datas :
And this from the refetch :
It seems like the two datas are not identified the same way.
If I change my refetchQueries to this :
refetchQueries: () => [{query: alertsSettingsQuery, variables: {issuerRoleNames: null, receiverRoleNames: null}}]
It works as expected.
I don't understand why I need to explicitly set the variables to null here, since I don't do that for any other refetch.
Version
apollo-client#2.3.4
react-apollo#<2.1.5>

Resources