Apollo update after a mutation isn't triggering a rerender - reactjs

I am having troubles with a mutation in graphQL apollo. When a page loads, it will run a query lectureResponseQuery and if the query == null a mutation fetchLectureResponseMutation is run to create a new document. This mutation returns the new result and I do an update to the query and I expect that the component will re-render with the new data, but it doesn't. Does anyone know why that is? Thanks!
#graphql(fetchLectureResponseMutation, {
options: ownProps => ({
variables: { lectureName: ownProps.match.params.lectureName },
update: (proxy, { data: { fetchLectureResponse } }) => {
const data = proxy.readQuery({
query: lectureResponseQuery,
variables: { lectureName: ownProps.match.params.lectureName },
});
data.lectureResponse = fetchLectureResponse;
proxy.writeQuery({
query: lectureResponseQuery,
data,
});
},
}),
name: 'fetchLectureResponse',
})
#graphql(lectureResponseQuery, {
options: ownProps => ({
variables: { lectureName: ownProps.match.params.lectureName },
}),
})
class LecturePage extends React.PureComponent {
componentWillUpdate(nextProps) {
if (nextProps.data.lectureResponse === null) {
this.props.fetchLectureResponse();
}
}
render() {
const { data } = this.props;
if (data.loading || data.lectureResponse === null) {
return <Loading />;
}
return <LectureLayout lectureResponse={data.lectureResponse} />
}
}

For anyone looking into this issue in the future- the central issue is that I wanted to do a find OR create operation. This works much better when the query just returns the new object if it doesn't exist because then you only make 1 backend call which means that you don't have to synchronize the timings between a query and a mutation.
TLDR: Use a query for a findOrCreate operation!

Related

React component uses old data from previous API call

I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);

Cache data may be lost when replacing the my field of a Query object

this my code
const NewVerificationCode = () => {
const { loading, error, data = {}, refetch } = useQuery(CONFIRMATION_CODE, {
skip: true,
onError: (err) => {},
});
console.log(loading, error, data);
if (loading || error) {
return <ErrorLoadingHandler {...{ loading, error }} />;
}
return (
<form
onSubmit={(e) => {
refetch();
e.preventDefault();
}}
>
<div>
<button type="submit" className="signUpbutton">
{"Send the message again"}
</button>
</div>
</form>
);
};
const CONFIRMATION_CODE = gql`
query {
my {
sendNewTokenForConfirmation
}
}
`;
when i make a request i get a warning
Cache data may be lost when replacing the my field of a Query object.
To address this problem (which is not a bug in Apollo Client), either ensure all >objects of type My have IDs, or define a custom merge function for the Query.my >field, so InMemoryCache can safely merge these objects
existing:
{"__typename":"My","getUser{"__typename":"User","email":"shakizriker0022#gmail.com"}}
incoming: {"__typename":"My","sendNewTokenForConfirmation":"SUCCESS"}
For more information about these options, please refer to the documentation:
I followed the links.
I read the documentation and realized that the problem is in the apollo client cache (typePolicies).
But how should I solve this problem I just can't figure out.
What should i write in typePolicies to get rid of the warning ?.
You may need to return an id for Apollo to uniquely identify that object in the cache.
I think this issue is similar to yours:
link
const CONFIRMATION_CODE = gql`
query {
my {
id
sendNewTokenForConfirmation
}
}
`;
Every object should return id, _id or alternatively a custom id field (https://www.apollographql.com/docs/react/caching/cache-configuration) for automatic merges to function.
const cache = new InMemoryCache({
typePolicies: {
Product: {
keyFields: ["custom-id-field"],
},
},
});
(!)People often forget that cache operations require the same variables used for the initial query (!)
Mutation Sample (adding a purchase):
update(cache, { data: NewPurchase } }) => {
let productCache = cache.readQuery({
query: productQuery,
variables: {
id: productId
}
})
cache.writeQuery({
query: productQuery,
variables: { id: productId },
data: {
Product: {
...productCache.Product,
purchases: productCache.Product.purchases.concat(NewPurchase)
}
}
});
};
}
(Important: Each Purchase also requires it's own individual id that needs to be returned)

Store doesn't update after GET

After DELETE a resource via a Button I update the same resource with a GET, i get the response with the correct data but the redux store doesn't update (apparently the redux action see no diff), and so the props are not updated.
But when I do a CREATE and then a GET like before (same call) this time the redux store see the diff and update the props.
The resource are the same between the 2 calls (after DELETE or CREATE), and the calls are even the same. Why in one case the redux store see the diff and don't in another case ?
crudCreate(`${CAMPAIGNS}/${id}/${LINE_ITEMS}`, data, () =>
crudGetAll(LINE_ITEMS, {}, { campaignId: id }, 1000, () => this.displayModal()),
);
crudDelete(LINE_ITEMS, lineItem.id, { id }, lineItem, () =>
crudGetAll(LINE_ITEMS, {}, { campaignId: id }, 1000, ({ payload: { data } }) =>
this.updateLineItems(data),
),
);
I don't any error on Redux debugger or on Console. I have a custom DataProvider, in that case it only redirect for the good routes
case LINE_ITEMS: {
if (type === 'DELETE') {
return { urn: `${apiUrl}/campaigns/${filter.id}/${resource.toLowerCase()}/${id}` };
}
if (filter) {
if ('campaignId' in filter) {
return {
urn: `${apiUrl}/campaigns/${filter.campaignId}/${resource.toLowerCase()}`,
filter: {
...filter,
campaignId: undefined,
},
};
}
}
return {
urn: `${apiUrl}/campaigns/${id.campaignId}/${resource.toLowerCase()}/${data.lineItemId}`,
};
}
And I only use the reducer from react-admin, here my index.js:
import { connect } from 'react-redux';
import { crudDelete, crudGetAll, translate } from 'react-admin';
export default connect(
state => ({
lineItems: Object.values(state.admin.resources.lineItems.data) || {},
swordData: Object.values(state.admin.resources.sword.data),
}),
{ crudDelete, crudGetAll },
)(withRouter(WithPermissions(translate(MyComponent))));
Does anybody have an idea? Thanks for your help

Apollo seems to refresh, when state is mapped to props, how can i prevent it?

I've build a component which basically list entries in a table, on top of that, i have another component to which filters can be applied. It all works really great with Apollo.
I'm trying to add deep linking into the filters, which on paper seems incredible simple, and i almost had i working.
Let me share some code.
const mapStateToProps = ({ activeObject }) => ({ activeObject });
#withRouter
#connect(mapStateToProps, null)
#graphql(FILTER_REPORT_TASKS_QUERY, {
name: 'filteredTasks',
options: (ownProps) => {
const filters = queryString.parse(location.search, { arrayFormat: 'string' });
return {
variables: {
...filters,
id: ownProps.match.params.reportId,
},
};
},
})
export default class TasksPage extends Component {
static propTypes = {
filteredTasks: PropTypes.object.isRequired,
activeObject: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
const filters = queryString.parse(location.search, { arrayFormat: 'string' });
this.state = { didSearch: false, initialFilters: filters };
this.applyFilter = this.applyFilter.bind(this);
}
applyFilter(values) {
const variables = { id: this.props.match.params.reportId };
variables.searchQuery = values.searchQuery === '' ? null : values.searchQuery;
variables.categoryId = values.categoryId === '0' ? null : values.categoryId;
variables.cardId = values.cardId === '0' ? null : values.cardId;
/*
this.props.history.push({
pathname: `${ this.props.history.location.pathname }`,
search: '',
});
return null;
*/
this.props.filteredTasks.refetch(variables);
this.setState({ didSearch: true });
}
..... Render functions.
Basically it calls the apply filter method, when a filter is chosen.
Which all works great, my problem is that when the activeObject is updated (By selecting a entry in the list). It seems to run my HOC graphql, which will apply the filters from the URL again, ignoring the filters chosen by the user.
I tried to remove the query strings from the url, once filters are applied, but i get some unexpected behavior, basically it's like it doesn't fetch again.
How can i prevent Apollo from fetching, just because the redux pushes new state?
I actually solved this by changing the order of the HOC's.
#graphql(FILTER_REPORT_TASKS_QUERY, {
name: 'filteredTasks',
options: (ownProps) => {
const filters = queryString.parse(location.search, { arrayFormat: 'string' });
return {
variables: {
...filters,
id: ownProps.match.params.reportId,
},
};
},
})
#withRouter
#connect(mapStateToProps, null)

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