Apollo Client delete Item from cache - reactjs

Hy I'm using the Apollo Client with React. I query the posts with many different variables. So I have one post in different "caches". Now I want to delete a post. So I need to delete this specific post from all "caches".
const client = new ApolloClient({
link: errorLink.concat(authLink.concat(httpLink)),
cache: new InMemoryCache()
});
Postquery:
export const POSTS = gql`
query posts(
$after: String
$orderBy: PostOrderByInput
$tags: JSONObject
$search: String
$orderByTime: Int
) {
posts(
after: $after
orderBy: $orderBy
tags: $tags
search: $search
orderByTime: $orderByTime
) {
id
title
...
}
}
`;
I tried it with the cache.modify(), which is undefined in my mutation([https://www.apollographql.com/docs/react/caching/cache-interaction/#cachemodify][1])
const [deletePost] = useMutation(DELETE_POST, {
onError: (er) => {
console.log(er);
},
update(cache, data) {
console.log(cache.modify())//UNDEFINED!!!
cache.modify({
id: cache.identify(thread), //identify is UNDEFINED + what is thread
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
}
}
})
}
});
I also used the useApolloClient() with the same result.
THX for any help.

Instead of using cache.modify you can use cache.evict, which makes the code much shorter:
deletePost({
variables: { id },
update(cache) {
const normalizedId = cache.identify({ id, __typename: 'Post' });
cache.evict({ id: normalizedId });
cache.gc();
}
});

this option worked for me
const GET_TASKS = gql`
query tasks($listID: String!) {
tasks(listID: $listID) {
_id
title
sort
}
}
`;
const REMOVE_TASK = gql`
mutation removeTask($_id: String) {
removeTask(_id: $_id) {
_id
}
}
`;
const Tasks = () => {
const { loading, error, data } = useQuery(GET_TASKS, {
variables: { listID: '1' },
});
сonst [removeTask] = useMutation(REMOVE_TASK);
const handleRemoveItem = _id => {
removeTask({
variables: { _id },
update(cache) {
cache.modify({
fields: {
tasks(existingTaskRefs, { readField }) {
return existingTaskRefs.filter(
taskRef => _id !== readField('_id', taskRef),
);
},
},
});
},
});
};
return (...);
};

You can pass your updater to the useMutation or to the deletePost. It should be easier with deletePost since it probably knows what it tries to delete:
deletePost({
variables: { idToRemove },
update(cache) {
cache.modify({
fields: {
posts(existingPosts = []) {
return existingPosts.filter(
postRef => idToRemove !== readField('id', postRef)
);
},
},
});
},
});
You should change variables to match your mutation. This should work since posts is at top level of your query. With deeper fields you'll need a way to get the id of the parent object. readQuery or a chain of readField from the top might help you with that.

Related

Create and update inside map function

I'm trying to find the right way to create and consequently update inside a map function.
These are the steps I need:
Map function "reads" the array of elements ids
Create new record on "leads_status" table
Using the new record id (from "leads_status") "leads" table is updated using "leads_status.id" as foreign key related to "leads.id_ls"
This is the code I tried.
const [create, { isLoading: isLoadingCreate, error: errorCreate }] = useCreate();
const [record, setRecord] = React.useState(null);
leadsIDS.map((value, index) => {
create('leads_status', {
data: {
id_lead: value,
id_status: 5
}
}, {
onSuccess: ({ id }) => {
setRecord([id, value]);
},
onError: () => {
console.log();
}
});
update('leads', {
id: record[1],
data: {
id_ls: record[0]
}
}, {
enabled: !isLoadingCreate && record !== null
}, {
onSuccess: () => {
console.log(record);
},
onError: error => notify('Error', { type: 'warning' })
})
})
I tried also to put the "update" function inside the "create --> onSuccess" but also there the code is not working as I want.
In "leads_status" table records are always created for each element in "leadsIDS" array but in "leads" table only 1 records is updating.
Where am I wrong?
The useCreate and useUpdate hooks are designed for single actions. If you want to chain several actions, I suggest you use the useDataProvider hook, instead, which lets you manipulate Promises.
const dataProvider = useDataProvider();
const notify = useNotify();
try {
await Promise.all(leadsIDS.map(async (value, index) => {
const { data: leadStatus } = await dataProvider.create('leads_status', {
data: {
id_lead: value,
id_status: 5
}
});
await dataProvider.update('leads', {
id: value,
data: { id_ls: leadStatus.id }
});
}));
} catch (e) {
notify('Error', { type: 'warning' });
}

fetching data from Sanity returns undefined (Next.js + Sanity)

I have page where I want to add some actualities. These actualities will be first set in the Sanity and then fetched via Next.js .
My Sanity schema
export default{
name:"actuality",
title:"Aktuality",
type:"document",
fields:[
{
name:"headline",
title:"Nadpis",
type:"string"
},
{
name:"publishedAt",
title:"Datum zveřejnění",
type:"datetime"
},
{
name:"body",
title:"Text",
type:"blockContent"
}
],
preview:{
select:{
title:"headline",
}
}
}
Problem is in fetching the data.
If I do this it will work, but will return only first actuality in the Sanity
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"][0]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
};
};
But if I remove the [0] it will throw error: "Reason: undefined cannot be serialized as JSON. Please use null or omit this value."
What do I need to change in order to get an array of Actualities?
Wrap the response in a data object to serialize and call {data} in your page props like this:
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
data: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
},
};
};
Few things:
it returns an array if you remove [0], you can do return just the data, regardless an array or not.
props: {
data: recipe
}
if you want to return single data with obj vallue as multiple props
props: {...recipe}

Handling Graphql Mutation update, cache read and writeQuery, if the query is dynamic?

Doing nightlife app on freecodecamp https://learn.freecodecamp.org/coding-interview-prep/take-home-projects/build-a-nightlife-coordination-app/
I am trying to implement 'Go' button, similarly 'Like' button on Youtube or Instagram. Users click the button the number(counting how many users go) goes up meaning users will go there and click again, it revokes, the number decreases, users will not go there.
It seems like working well except the issue, I have to refresh the page and then, the number has increased or decreased and throws the error like below so:
Invariant Violation: Can't find field getBars({}) on object {
"getBars({\"location\":\"vancouver\"})": [
{
"type": "id",
"generated": false,
"id": "Bar:uNgTjA9ADe_6LWby20Af8g",
"typename": "Bar"
},
{
"type": "id",
"generated": false,
"id": "Bar:CwL5jwXhImT_7K5IB7mOvA",
"typename": "Bar"
},
{
"type": "id",
"generated": false,
"id": "Bar:mdt1tLbkZcOS2CsEbVF9Xg",
"typename": "Bar"
},
.
.
.
I am assuming handling update function will fix this issue but unlike the example from Apollo documentation:
// GET_TODOS is not dynamic query
// nothing to pass as variables to fetch TODO list
<Mutation
mutation={ADD_TODO}
update={(cache, { data: { addTodo } }) => {
const { todos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { todos: todos.concat([addTodo]) },
});
}}
>
My query is dynamic:
// I have to pass location variable, otherwise it won't fetch anything.
const GET_BARS_QUERY = gql`
query getBars($location: String!) {
getBars(location: $location) {
id
name
url
rating
price
image_url
goings {
username
}
goingCount
}
}
`;
I believe I might need to handle to provide location using readQuery and writeQury but not too sure what I should do.
Here's my code:
const GoButton = ({ user, bar }) => {
const { token } = user;
const { id, goings, goingCount } = bar;
const [userGoes] = useMutation(GO_MUTATION, {
variables: { yelp_id: id },
update(proxy, result) {
const data = proxy.readQuery({
query: GET_BARS_QUERY
});
data.getBars = [result.userGoes, ...data.getBars];
proxy.writeQuery({ query: GET_BARS_QUERY, data });
}
});
return (
<Button onClick={userGoes}>
Go {goingCount}
</Button>
);
};
const GO_MUTATION = gql`
mutation go($yelp_id: String!) {
go(yelp_id: $yelp_id) {
id
goings {
id
username
}
goingCount
}
}
`;
export default GoButton;
Full code here https://github.com/footlessbird/Nightlife-Coordination-App
when you read/write the getBars query, you need to pass the location as a variable
const [userGoes] = useMutation(GO_MUTATION, {
variables: { yelp_id: id },
update(proxy, result) {
const data = proxy.readQuery({
query: GET_BARS_QUERY,
variables: {
location: 'New York'
}
});
data.getBars = [result.userGoes, ...data.getBars];
proxy.writeQuery({ query: GET_BARS_QUERY, data,
variables: {
location: 'New York'
}
});
}
});

graphql: how to handle prev page, next page, last page and first page properly?

For frontend I am using React + Relay. I have some connection at the backend that could be queried like:
query Query {
node(id: 123456) {
teams(first: 10) {
node {
id
name
}
page_info {
start_cursor
end_cursor
}
}
}
}
So for the traditional approach, I can use skip PAGE_SIZE * curr limit PAGE_SIZE to query for next page, prev page and first page and last page (In fact I can query for random page)
But how should I implement the frontend to make these requests elegantly?
#Junchao, what Vincent said is correct. Also, you must have a re-fetch query and send refetchVariables with your first value updated. I will try to provide you an example:
export default createRefetchContainer(
TeamsComponent,
{
query: graphql`
fragment TeamsComponent_query on Query
#argumentDefinitions(
first: { type: Int }
last: { type: Int }
before: { type: String }
after: { type: String }
) {
teams(
id: { type: "ID!" }
first: { type: Int }
last: { type: Int }
before: { type: String }
after: { type: String }
) #connection(key: "TeamsComponent_teams", filters: []) {
count
pageInfo {
endCursor
hasNextPage
}
edges {
node {
id
name
}
}
}
}
`,
},
graphql`
query TeamsComponent(
$after: String
$before: String
$first: Int
$last: Int
) {
...TeamsComponent_query
#arguments(
first: $first
last: $last
after: $after
before: $before
)
}
`,
);
I tried to build an example based on your code. This is basically the idea. The bottom query is the re-fetch one. Alongside with that, you must trigger this re-fetch somehow by calling this.props.relay.refetch passing your renderVaribles. Take a deep looker into the docs about this.
Hope is helps :)
UPDATE:
Just to add something, you could have a handleLoadMore function with something like this:
handleLoadMore = () => {
const { relay, connection } = this.props;
const { isFetching } = this.state;
if (!connection) return;
const { edges, pageInfo } = connection;
if (!pageInfo.hasNextPage) return;
const total = edges.length + TOTAL_REFETCH_ITEMS;
const fragmentRenderVariables = this.getRenderVariables() || {};
const renderVariables = { first: total, ...fragmentRenderVariables };
if (isFetching) {
// do not loadMore if it is still loading more or searching
return;
}
this.setState({
isFetching: true,
});
const refetchVariables = fragmentVariables => ({
first: TOTAL_REFETCH_ITEMS,
after: pageInfo.endCursor,
});
relay.refetch(
refetchVariables,
null,
() => {
this.setState({ isFetching: false });
},
{
force: false,
},
);
};
UPDATE 2:
For going backwards, you could have something like:
loadPageBackwardsVars = () => {
const { connection, quantityPerPage } = this.props;
const { quantity } = getFormatedQuery(location);
const { endCursorOffset, startCursorOffset } = connection;
const firstItem = connection.edges.slice(startCursorOffset, endCursorOffset)[0].cursor;
const refetchVariables = fragmentVariables => ({
...fragmentVariables,
...this.getFragmentVariables(),
last: parseInt(quantity || quantityPerPage, 10) || 10,
first: null,
before: firstItem,
});
return refetchVariables;
};

How do I update my apollo data after subscribing to a query using client.subscribe?

I'm trying to subscribe to different queries than I am performing as my root query. This is because subscriptions cannot watch the connections of a child node on my graphql server. So instead I subscribe to each child connection I need, and would like to update the view with the data I recieve. Here's what I have so far:
client.subscribe({
query: queries[queryName],
variables,
updateQuery: (previousResult, { subscriptionData }) => {
console.log('this never happens');
//this would be where I make my modifications if this function was ever called
return {};
},
}).subscribe({
next(resp) {
//this function is called however I still don't know how to update the view with the response.
that.newData(resp,queryName);
},
error,
complete,
})
Here's some relevant sample code:
subscribe(fromID, toID, updateQueryViaSubscription) {
const IM_SUBSCRIPTION_QUERY = gql`
subscription getIMsViaSubscription($fromID: String!, $toID: String!){
IMAdded(fromID:$fromID, toID: $toID){
id,
fromID,
toID,
msgText
}
}
`;
this.subscriptionObserver = this.props.client.subscribe({
query: IM_SUBSCRIPTION_QUERY,
variables: { fromID: this.fromID, toID: this.toID },
}).subscribe({
next(data) {
const newMsg = data.IMAdded;
updateQueryViaSubscription((previousResult) => {
// if it's our own mutation, we might get the subscription result
// after the mutation result.
if (isDuplicateIM(newMsg, previousResult.instant_message)) {
return previousResult;
}
// update returns a new "immutable" list with the new comment
// added to the front.
return update(
previousResult,
{
instant_message: {
$push: [newMsg],
},
}
);
});
},
error(err) {
console.error('err', err); },
});
}
You'll notice that updateQueryViaSubscription gets passed to subscribe as a parameter. Here's where it comes from in my app:
//NOTE: NAME OF 2ND PROPERTY IN DATA OBJECT ("instant_message" in this example) MUST BE IDENTICAL TO NAME OF RESOLVER
//OTHERWISE DATA WILL NOT LOAD
const CreateIMPageWithDataAndMutations = graphql(GETIMS_QUERY, {
options({ toID }) {
const fromID = Meteor.userId();
return {
variables: { fromID: `${fromID}`, toID: `${toID}`}
};
}
,
props({ data: { loading, instant_message, updateQuery } }) {
//debugger;
return { loading, instant_message, updateQueryViaSubscription: updateQuery };
},
});
export default compose(
CreateIMPageWithMutations,
CreateIMPageWithDataAndMutations,
withApollo
)(CreateIM);
export { GETIMS_QUERY };
Notice that the function updateQuery gets passed into the component from Apollo, and renamed by my code to updateQueryViaSubscription prior to being added to the component's props.
My code calls subscribe in componentDidMount:
componentDidMount() {
const userIsLoggedIn = Meteor.userId() ? true : false;
const {toID, ApolloClientWithSubscribeEnabled} = this.props;
if (userIsLoggedIn && toID){
this.fromID = Meteor.userId();
this.toID = toID;
this.subscribe(this.fromID, this.toID, this.props.updateQueryViaSubscription);
}
}
...and unsubscribes in componentWillUnmount:
componentWillUnmount() {
if (this.subscriptionObserver) {
this.subscriptionObserver.unsubscribe();
}
}
I hope this info is helpful.

Resources