mst-gql
mst-gql copied to clipboard
Refetch query after variables changes
I'm currently using mst-gql with next.js and cannot get it to refetch queries when variable from the route changes, has anyone had problems with this before? The component rerenders but it does not refetch the query.
I'm about to start using this with next.js so I'll let you know how it goes for me or if I run into anything.
@mattiasewers I've been bitten by cache first strategy in one of my apps, and had to force all my queries to { fetchPolicy: 'network-only' }. I am not using next.js but thought I would mention it
I have the same problem. Query is:
const {
data, error, loading,
} = useQuery((store) =>
store.queryHerkunft({
where: { id: { _eq: id } },
}),
)
When id changes due to navigating to different dataset, the component re-renders but the query is not re-run. So stale data is shown.
@barbalex have you tried with a different fetch policy? are you using data off data or out of the store?
Fetch policy is unchanged so I guess that would be "cache-and-network".
I am using the data off data. Having started to move from apollo to mst-gql only two days ago I haven't even thought of doing it differently yet.
As usual this dataset is accessible from a list that was loaded before. But for the list not all the fields are loaded. That is why I was using data from the query off data.
I have tried loading all fields for the list and then finding the dataset from that data to show it. While that does work (the find command runs when the component is re-rendered and returns the correct data), it seems like over-fetching is not a good general solution.
Also: because in my case references return undefined (https://github.com/mobxjs/mst-gql/issues/229), I can't do that when references are needed.
have you tried with a different fetch policy?
Yes: network-only and cache-first. Did not make any difference.
and all the things coming down have a typename and ID? sorry, trying to be helpful :)
@chrisdrackett that is very helpful. You are right: The response comes without __typename. So that will be the reason.
I am querying using a graphql-tag based query because otherwise referenced tables will not arrive (https://github.com/mobxjs/mst-gql/issues/229).
Unfortunately when I add __typename to my fragments that provokes an error. Will have to solve that:
[mobx-state-tree] Error while converting `{"id":"3d0734a0-9448-11ea-ad59-b16356755fa5","art_id":null,"kultur_option":{"kultur_id":"3d0734a0-9448-11ea-ad59-b16356755fa5","ev_datum":true,"ev_geplant":true,"ev_person_id":true,"ev_teilkultur_id":true,"tk":false,"tk_bemerkungen":true,"tz_andere_menge":true,"tz_anzahl......:null}}` to `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))`: snapshot `{"id":"3d0734a0-9448-11ea-ad59-b16356755fa5","art_id":null,"kultur_option":{"kultur_id":"3d0734a0-9448-11ea-ad59-b16356755fa5","ev_datum":true,"ev_geplant":true,"ev_person_id":true,"ev_teilkultur_id":true,"tk":false,"tk_bemerkungen":true,"tz_andere_menge":true,"tz_anzahl_mutterpflanzen":true,"tz_auspflanzbereit_beschreibung":true,"tz_teilkultur_id":true,"tz_bemerkungen":true,"z_bemerkungen":true,"_rev":null,"_parent_rev":null,"_revisions":null,"_depth":1,"_conflicts":null}}` is not assignable to type: `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` (No type is applicable for the union), expected an instance of `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` or a snapshot like `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` instead. snapshot `{"id":"3d0734a0-9448-11ea-ad59-b16356755fa5","art_id":null,"kultur_option":{"kultur_id":"3d0734a0-9448-11ea-ad59-b16356755fa5","ev_datum":true,"ev_geplant":true,"ev_person_id":true,"ev_teilkultur_id":true,"tk":false,"tk_bemerkungen":true,"tz_andere_menge":true,"tz_anzahl_mutterpflanzen":true,"tz_auspflanzbereit_beschreibung":true,"tz_teilkultur_id":true,"tz_bemerkungen":true,"z_bemerkungen":true,"_rev":null,"_parent_rev":null,"_revisions":null,"_depth":1,"_conflicts":null}}` is not assignable to type: `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` (Value is not a undefined), expected an instance of `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` or a snapshot like `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` instead. snapshot `{"id":"3d0734a0-9448-11ea-ad59-b16356755fa5","art_id":null,"kultur_option":{"kultur_id":"3d0734a0-9448-11ea-ad59-b16356755fa5","ev_datum":true,"ev_geplant":true,"ev_person_id":true,"ev_teilkultur_id":true,"tk":false,"tk_bemerkungen":true,"tz_andere_menge":true,"tz_anzahl_mutterpflanzen":true,"tz_auspflanzbereit_beschreibung":true,"tz_teilkultur_id":true,"tz_bemerkungen":true,"z_bemerkungen":true,"_rev":null,"_parent_rev":null,"_revisions":null,"_depth":1,"_conflicts":null}}` is not assignable to type: `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` (Value is not a null), expected an instance of `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` or a snapshot like `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` instead. snapshot `{"id":"3d0734a0-9448-11ea-ad59-b16356755fa5","art_id":null,"kultur_option":{"kultur_id":"3d0734a0-9448-11ea-ad59-b16356755fa5","ev_datum":true,"ev_geplant":true,"ev_person_id":true,"ev_teilkultur_id":true,"tk":false,"tk_bemerkungen":true,"tz_andere_menge":true,"tz_anzahl_mutterpflanzen":true,"tz_auspflanzbereit_beschreibung":true,"tz_teilkultur_id":true,"tz_bemerkungen":true,"z_bemerkungen":true,"_rev":null,"_parent_rev":null,"_revisions":null,"_depth":1,"_conflicts":null}}` is not assignable to type: `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` (Value is not a valid identifier, which is a string or a number), expected an instance of `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` or a snapshot like `(undefined | null | reference(late(function () { return _kulturModel__WEBPACK_IMPORTED_MODULE_5__["kulturModel"]; })))` instead.
The query is (with __typename added):
const eventQuery = gql`
query EventQueryForEvent(
$id: uuid!
$filter: event_bool_exp!
$isFiltered: Boolean!
) {
event(where: { id: { _eq: $id } }) {
...EventFields
__typename
kultur {
id
art_id
kultur_option {
...KulturOptionFields
}
}
}
rowsUnfiltered: event @include(if: $isFiltered) {
id
}
rowsFiltered: event(where: $filter) @include(if: $isFiltered) {
id
}
}
${eventFragment}
${kulturOptionFragment}
`
And the model is:
export const eventModelBase = ModelBase
.named('event')
.props({
__typename: types.optional(types.literal("event"), "event"),
_conflicts: types.union(types.undefined, types.null, types.frozen()),
_depth: types.union(types.undefined, types.null, types.integer),
_parent_rev: types.union(types.undefined, types.null, types.string),
_rev: types.union(types.undefined, types.null, types.string),
_revisions: types.union(types.undefined, types.null, types.frozen()),
beschreibung: types.union(types.undefined, types.null, types.string),
changed: types.union(types.undefined, types.null, types.frozen()),
changed_by: types.union(types.undefined, types.null, types.string),
datum: types.union(types.undefined, types.null, types.frozen()),
geplant: types.union(types.undefined, types.null, types.boolean),
id: types.identifier,
kultur: types.union(types.undefined, types.null, MSTGQLRef(types.late(() => kulturModel))),
kultur_id: types.union(types.undefined, types.null, types.frozen()),
person: types.union(types.undefined, types.null, MSTGQLRef(types.late(() => personModel))),
person_id: types.union(types.undefined, types.null, types.frozen()),
teilkultur: types.union(types.undefined, types.null, MSTGQLRef(types.late(() => teilkulturModel))),
teilkultur_id: types.union(types.undefined, types.null, types.frozen()),
tsv: types.union(types.undefined, types.null, types.frozen()),
})
.views(self => ({
get store() {
return self.__getStore()
}
}))
O.k., solved above error (__typename was missing in a nested fragment).
But the original problem remains, even though now all data is returned with id and __typename:
{
"data": {
"event": [
{
"id": "ca7019e0-9445-11ea-b529-e7e8445751c9",
"kultur_id": null,
"teilkultur_id": null,
"person_id": null,
"beschreibung": null,
"geplant": false,
"datum": null,
"changed": "2020-05-12",
"changed_by": null,
"_rev": "1-1441a7909c087dbbe7ce59881b9df8b9",
"_parent_rev": null,
"_revisions": ["1-1441a7909c087dbbe7ce59881b9df8b9"],
"_depth": 1,
"_conflicts": [],
"__typename": "event",
"kultur": null
}
],
"rowsUnfiltered": [
{ "id": "ca7019e0-9445-11ea-b529-e7e8445751c9", "__typename": "event" },
{ "id": "226ddce2-9444-11ea-b0ef-430f364f97aa", "__typename": "event" }
],
"rowsFiltered": [
{ "id": "ca7019e0-9445-11ea-b529-e7e8445751c9", "__typename": "event" },
{ "id": "226ddce2-9444-11ea-b0ef-430f364f97aa", "__typename": "event" }
]
}
}
~~After diligently adding __typename to all queries and fragments it seems this was the solution.~~ ~~Thanks for your help @chrisdrackett~~
Nope. Problem remains. Meanwhile I am a bit confused, as may be visible from this back and forth...
So to bring it to the point again:
This query:
const { data, error, loading } = useQuery((store) =>
store.queryHerkunft({
where: { id: { _eq: id } },
}),
)
receives this answer:
{
"data": {
"herkunft": [
{
"__typename": "herkunft",
"id": "05bb4fa0-93ca-11ea-ab80-97590c0e1653",
"_conflicts": [],
"_depth": 1,
"_parent_rev": null,
"_rev": "1-1441a7909c087dbbe7ce59881b9df8b9",
"_revisions": ["1-1441a7909c087dbbe7ce59881b9df8b9"],
"bemerkungen": null,
"changed": "2020-05-11",
"changed_by": null,
"gemeinde": null,
"geom_point": null,
"kanton": null,
"land": null,
"lokalname": null,
"lv95_x": null,
"lv95_y": null,
"nr": "3",
"tsv": "'3':1A",
"wgs84_lat": null,
"wgs84_long": null
}
]
}
}
And when the component is re-rendered with a new id due to a change of dataset, the query is not re-queried.
Some more info:
The type is:
type herkunft {
_conflicts: _text
_depth: Int
_parent_rev: String
_rev: String
_revisions: _text
bemerkungen: String
changed: date
changed_by: String
gemeinde: String
geom_point: geometry
# An array relationship
herkunft_files(
# distinct select on columns
distinct_on: [herkunft_file_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [herkunft_file_order_by!]
# filter the rows returned
where: herkunft_file_bool_exp
): [herkunft_file!]!
# An aggregated array relationship
herkunft_files_aggregate(
# distinct select on columns
distinct_on: [herkunft_file_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [herkunft_file_order_by!]
# filter the rows returned
where: herkunft_file_bool_exp
): herkunft_file_aggregate!
# An array relationship
herkunft_sums(
# distinct select on columns
distinct_on: [herkunft_sums_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [herkunft_sums_order_by!]
# filter the rows returned
where: herkunft_sums_bool_exp
): [herkunft_sums!]!
# An aggregated array relationship
herkunft_sums_aggregate(
# distinct select on columns
distinct_on: [herkunft_sums_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [herkunft_sums_order_by!]
# filter the rows returned
where: herkunft_sums_bool_exp
): herkunft_sums_aggregate!
id: ID!
kanton: String
# An array relationship
kulturs(
# distinct select on columns
distinct_on: [kultur_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [kultur_order_by!]
# filter the rows returned
where: kultur_bool_exp
): [kultur!]!
# An aggregated array relationship
kulturs_aggregate(
# distinct select on columns
distinct_on: [kultur_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [kultur_order_by!]
# filter the rows returned
where: kultur_bool_exp
): kultur_aggregate!
land: String
lokalname: String
lv95_x: numeric
lv95_y: numeric
nr: String
# An array relationship
sammlungs(
# distinct select on columns
distinct_on: [sammlung_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [sammlung_order_by!]
# filter the rows returned
where: sammlung_bool_exp
): [sammlung!]!
# An aggregated array relationship
sammlungs_aggregate(
# distinct select on columns
distinct_on: [sammlung_select_column!]
# limit the number of rows returned
limit: Int
# skip the first n rows. Use only with order_by
offset: Int
# sort the rows by one or more columns
order_by: [sammlung_order_by!]
# filter the rows returned
where: sammlung_bool_exp
): sammlung_aggregate!
tsv: tsvector
wgs84_lat: numeric
wgs84_long: numeric
}
herkunftModel.base.js is:
export const herkunftModelBase = ModelBase
.named('herkunft')
.props({
__typename: types.optional(types.literal("herkunft"), "herkunft"),
_conflicts: types.union(types.undefined, types.null, types.frozen()),
_depth: types.union(types.undefined, types.null, types.integer),
_parent_rev: types.union(types.undefined, types.null, types.string),
_rev: types.union(types.undefined, types.null, types.string),
_revisions: types.union(types.undefined, types.null, types.frozen()),
bemerkungen: types.union(types.undefined, types.null, types.string),
changed: types.union(types.undefined, types.null, types.frozen()),
changed_by: types.union(types.undefined, types.null, types.string),
gemeinde: types.union(types.undefined, types.null, types.string),
geom_point: types.union(types.undefined, types.null, types.frozen()),
herkunft_files: types.union(types.undefined, types.array(MSTGQLRef(types.late(() => herkunft_fileModel)))),
herkunft_files_aggregate: types.union(types.undefined, types.late(() => herkunft_file_aggregateModel)),
herkunft_sums: types.union(types.undefined, types.array(types.late(() => herkunft_sumsModel))),
herkunft_sums_aggregate: types.union(types.undefined, types.late(() => herkunft_sums_aggregateModel)),
id: types.identifier,
kanton: types.union(types.undefined, types.null, types.string),
kulturs: types.union(types.undefined, types.array(MSTGQLRef(types.late(() => kulturModel)))),
kulturs_aggregate: types.union(types.undefined, types.late(() => kultur_aggregateModel)),
land: types.union(types.undefined, types.null, types.string),
lokalname: types.union(types.undefined, types.null, types.string),
lv95_x: types.union(types.undefined, types.null, types.frozen()),
lv95_y: types.union(types.undefined, types.null, types.frozen()),
nr: types.union(types.undefined, types.null, types.string),
sammlungs: types.union(types.undefined, types.array(MSTGQLRef(types.late(() => sammlungModel)))),
sammlungs_aggregate: types.union(types.undefined, types.late(() => sammlung_aggregateModel)),
tsv: types.union(types.undefined, types.null, types.frozen()),
wgs84_lat: types.union(types.undefined, types.null, types.frozen()),
wgs84_long: types.union(types.undefined, types.null, types.frozen()),
})
The query's model is:
queryHerkunft(variables, resultSelector = herkunftModelPrimitives.toString(), options = {}) {
return self.query(`query herkunft($distinct_on: [herkunft_select_column!], $limit: Int, $offset: Int, $order_by: [herkunft_order_by!], $where: herkunft_bool_exp) { herkunft(distinct_on: $distinct_on, limit: $limit, offset: $offset, order_by: $order_by, where: $where) {
${typeof resultSelector === "function" ? resultSelector(new herkunftModelSelector()).toString() : resultSelector}
} }`, variables, options)
}
Meanwhile it is working for me. I am not sure but I think it is because now in the component I am not using the data returned by the query but rather directly grabbing the object from the store:
store.herkunfts.get(id)
Which gives this rather weird looking code:
const { error, loading } = useQuery((store) =>
store.queryHerkunft({
where: { id: { _eq: id } },
}),
)
const row = store.herkunfts.get(id)
So the query is still needed to ensure the data is fetched from the server to the store. And it's loading and error methods are also used. But it's data is ignored. Instead the data is grabbed from the store.
This needs some getting used to, for me. And please tell me if I am getting something completely wrong. But so far this works.
To come back to the original issue: Is it possible that while the data is re-fetched after variables change and the store is updated, the data key of the result is not?
This would be relevant to me because I have other queries that I seem to have to use fragments in because they filter depending on related data. Which may not exist in the store yet. So I (think I) can't easily grab them directly from the store and instead rely on the data being updated after component re-renders.
But I could simply be a bit confused and miss something obvious.
I found another case where this happens. This time it may happen because mst-gql does not allow combined primary keys/identifiers:
I am using a fragment to query:
const query = gql`
query herkunftForConflictQuery($id: uuid!, $rev: String!) {
herkunft_rev(where: { id: { _eq: $id }, _rev: { _eq: $rev } }) {
_rev
_deleted
_depth
_parent_rev
_revisions
bemerkungen
changed
changed_by
gemeinde
geom_point
id
kanton
land
lokalname
nr
}
}
`
This is the query:
const { data, error, loading } = useQuery(query, {
variables: { rev, id },
})
In this case herkunf_revModel.base.js contains:
export const herkunft_revModelBase = ModelBase
.named('herkunft_rev')
.props({
__typename: types.optional(types.literal("herkunft_rev"), "herkunft_rev"),
_deleted: types.union(types.undefined, types.null, types.boolean),
_depth: types.union(types.undefined, types.null, types.integer),
_parent_rev: types.union(types.undefined, types.null, types.string),
_rev: types.union(types.undefined, types.string),
_revisions: types.union(types.undefined, types.null, types.frozen()),
bemerkungen: types.union(types.undefined, types.null, types.string),
changed: types.union(types.undefined, types.null, types.frozen()),
changed_by: types.union(types.undefined, types.null, types.string),
gemeinde: types.union(types.undefined, types.null, types.string),
geom_point: types.union(types.undefined, types.null, types.frozen()),
id: types.identifier,
kanton: types.union(types.undefined, types.null, types.string),
kulturs: types.union(types.undefined, types.array(MSTGQLRef(types.late(() => kulturModel)))),
kulturs_aggregate: types.union(types.undefined, types.late(() => kultur_aggregateModel)),
land: types.union(types.undefined, types.null, types.string),
lokalname: types.union(types.undefined, types.null, types.string),
nr: types.union(types.undefined, types.null, types.string),
sammlungs: types.union(types.undefined, types.array(MSTGQLRef(types.late(() => sammlungModel)))),
sammlungs_aggregate: types.union(types.undefined, types.late(() => sammlung_aggregateModel)),
})
.views(self => ({
get store() {
return self.__getStore()
}
}))
herkunft_rev is defined in PostgreSQL as:
create table herkunft_rev (
id uuid default uuid_generate_v1mc(),
nr text default null,
lokalname text default null,
gemeinde text default null,
kanton text default null,
land text default null,
geom_point geometry(Point, 4326) default null,
bemerkungen text default null,
changed timestamp default now(),
changed_by text default null,
_rev text default null,
_parent_rev text default null,
_revisions text[] default null,
_depth integer default 1,
_deleted boolean default false,
primary key (id, _rev)
);
The graphql.schema looks like this (relationships removed):
type herkunft_rev {
_deleted: Boolean
_depth: Int
_parent_rev: String
_rev: String!
_revisions: _text
bemerkungen: String
changed: date
changed_by: String
gemeinde: String
geom_point: geometry
id: ID!
kanton: String
land: String
lokalname: String
nr: String
}
id: ID! is set because I replaced hasura's uuid! with ID!
But here the problem is that the primary key is combined by id and _rev. That is because the app is offline capable and using the CouchDB method to track conflicts.
I have a feeling that this could lead to the unexpected behavior we are seeing: That the query is not re-queried. After all, the key mst-gql has declared identifier id has not changed.
This problem goes back to mst itself: there can be only one identifier property: https://mobx-state-tree.js.org/concepts/references#identifiers
I am also running into this issue with plain create-react-app. The component re-renders with updated props, but those props don't get expose to useQuery.
I was able to get around it with useEffect and setQuery. It's kind of hacky, but it does work.
const { loading, data, error, store, setQuery } = useQuery((store) =>
store.queryFund(
{ where: { id: fund_id } },
(qb) => qb.capital_deployed.last_update.capital_deployed_change)
);
useEffect(() => {
// prop fund_id changed
const query = store.queryFund(
{ where: { id: fund_id } },
(qb) => qb.capital_deployed.last_update.capital_deployed_change);
setQuery(query);
}, [fund_id])
... render stuff
@chrisdrackett any thoughts on this issue? I'm facing same set of problems. An app bootstrapped with CRA. The component re-renders with updated props. However, the query doesn't get re-triggered. No issue of multiple IDs. Tried 'network-only' policy too. Nothing. If I replace mst useQuery with sth built on vanilla fetch, things work as normal.
export const BookDetailsRoute = observer((props: any) => {
const id = props.match.params.id!;
const { store, data } = useQuery(store =>
store.queryBook_by_pk({ id }, BOOK_FRAGMENT)
)
// m = store.books.get(id);
m = data?.book_by_pk;
if (m) {
return <BookDetails m={m} />;
}
return <EmptyItem />;
}
Does this line mean that in my case above, where I'm passing the query as a function (store => store.queryBook_by_pk({ id }, BOOK_FRAGMENT), the effect will be skipped?. Pardon my ignorance, I'm relatively new to react. I was going through this code trying to reason how setQuery would be called in case my id above changes.
@baskin I had the same problem and the line you linked is responsible for only fetching it once.
My solution to this problem is this:
const { data, loading, setQuery } = useQuery();
React.useEffect(() => {
setQuery((store) => store.queryBook_by_pk({ id }, BOOK_FRAGMENT));
}, [id]);
I ending up using React.useEffect too. In my case it was an array so I did something like that:
const query = (store: RootStoreType) =>
store.queryParticipants(
{
filter: {
id_in: ui.favorites,
},
},
(participant) =>
participant.firstname.lastname.dob.country(
(country) => country.emoji
)
);
const { loading, data, setQuery } = useQuery(query);
React.useEffect(() => {
setQuery(query);
}, [ui.favorites.length]);
What the heck!? How is this not easily answered? It seems like we are all trying to do something the wrong way?
any updates? :)