How to fetch multiple conditional queries in Apollo Client React? - javascript

I'm using Apollo Client, and for fetching queries I'm using useQuery from the package #apollo/react-hooks.
I would like to accomplish the following:
List of Steps:
Step 1: Fetch a query stage
const GetStage = useQuery(confirmStageQuery, {
variables: {
input: {
id: getId.id
}
}
});
Step 2: Based on the response that we get from GetStage, we would like to switch between 2 separate queries
if (!GetStage.loading && GetStage.data.getGame.stage === "Created") {
Details = useQuery(Query1, {
variables: {
input: {
id: getId.id
}
}
});
} else if (!GetStage.loading && GetStage.data.getGame.stage === "Confirmed") {
Details = useQuery(Query2, {
variables: {
input: {
id: getId.id
}
}
});
}
Step 3: Also when the page loads every time, I'm re-fetching the data.
useEffect(() => {
//Fetch for Change in the Stage
GetStage.refetch();
//Fetch for Change in the Object
if (Details) {
Details.refetch();
if (Details.data) {
setDetails(Details.data.getGame);
}
}
});
Problem?
Rendered more hooks than during the previous render.
Details.data is undefined
So how can we call multiple async queries in Apollo Client?

As Philip said, you can't conditionally call hooks. However conditionally calling a query is very common, so Apollo allows you to skip it using the skip option:
const { loading, error, data: { forum } = {}, subscribeToMore } = useQuery(GET_FORUM, {
skip: !forumId,
fetchPolicy: 'cache-and-network',
variables: { id: forumId },
});
The hook is called, but the query isn't. That's a lot simpler and clearer than using a lazy query for your use case in my opinion.

The rules of hooks say you can't conditionally call hooks, whenever you find yourself in a situation where you want to use an if/else around a hook you're probably on the wrong track.
What you want to do here is to use a lazyQuery for everything that's "optional" or will be fetched later - or for queries that depend on the result of another query.
Here's a quick example (probably not complete enough to make your entire code work):
// This query is always called, use useQuery
const GetStage = useQuery(confirmStageQuery, {
variables: {
input: {
id: getId.id
}
}
});
const [fetchQuery1, { loading1, data1 }] = useLazyQuery(Query1);
const [fetchQuery2, { loading2, data2 }] = useLazyQuery(Query2);
// Use an effect to execute the second query when the data of the first one comes in
useEffect(() => {
if (!GetStage.loading && GetStage.data.getGame.stage === "Created") {
fetchQuery1({variables: {
input: {
id: getId.id
}
}})
} else if (!GetStage.loading && GetStage.data.getGame.stage === "Confirmed") {
fetchQuery2({variables: {
input: {
id: getId.id
}
}})
}
}, [GetState.data, GetStage.loading])

Related

How can I make API calls conditionally using Redux Toolkit query/createApi?

I am only using RTK to make API calls. Not using for any state management stuff like using Slices or Redux Thunk etc. I don't have a clear understanding of those yet... (just a full disclosure)
I have tried this:
export const gymApi = createApi({
reducerPath: 'gymApi',
baseQuery: fetchBaseQuery({ baseUrl }),
endpoints: (builder) => ({
getAllWorkouts: builder.query({
query: () => makeApiCall(`/exercises`),
}),
getWorkoutByBodyPart: builder.query({
query: (bodyPart) => {
console.log('BodyPart in GymApi:', bodyPart);
if (bodyPart !== 'all') {
return makeApiCall(`/exercises/bodyPart/${bodyPart}`);
} else {
return null;
}
},
})
});
And tried this:
getWorkoutByBodyPart: builder.query({
query: (bodyPart) => {
console.log('BodyPart in GymApi:', bodyPart);
if (bodyPart !== 'all') {
return makeApiCall(`/exercises/bodyPart/${bodyPart}`);
} else {
return Promise.resolve({ data: [] }); // or Promise.resolve({});
}
},
})
with no luck. I'm calling the hooks from the home page like this:
const Exercises = ({ exercises, setExercises, bodyPart }) => {
// THE FOLLOWING WORKS. ONLY USGING STATIC DATA DUE TO API HARD LIMITS
const { data: exercisesData, isFetching: isFetchingAllWorkouts } =
useGetAllWorkoutsQuery();
const { data: exercisesDataByCategory, isFetching: isFetchingByCategory } =
useGetWorkoutByBodyPartQuery(bodyPart);
useEffect(() => {
if (exercisesData && exercisesDataByCategory) {
if (bodyPart === 'all') {
setExercises(exercisesData);
} else {
setExercises(exercisesDataByCategory);
}
}
}, [exercisesData, bodyPart, isFetchingAllWorkouts, isFetchingByCategory]);
It is working but with every refresh, I get a Warning: "Category not found ..." from the API ... basically, there is no endpoint called "all" in the ExerciseDB API (in RapidAPIs). So every time an "all" is passed as a Category, it gives me a 401. Now the App works fine. I was just wondering if there is a cleaner way to do this. I mean, I don't wanna make a call to the API when the Category is "all".
I must say that I'm new to this. Trying to get out of using Fetch all the time and take advantage of RTK caching. Any help in the correct direction will be highly appreciated. Thanx in advance.
You can either pass {skip: true} as an option to the query hook, or import the special skipToken value from RTK and pass that as the query argument.
See the RTK docs for more details:
https://redux-toolkit.js.org/rtk-query/usage/conditional-fetching

Custom merge function is not being called after updating field with cache.modify

I have written a custom merge function for the field products on type Session. It seems the merge function is only being called when I initialise the object Session:1 with its products, and not when I update products later using cache.modify.
My merge function:
const client = new ApolloClient({
uri: 'http://localhost:8081/graphql',
cache: new InMemoryCache({
typePolicies: {
Session: {
fields: {
products: {
merge (existing, incoming) {
// this is only being called on useQuery(HydrateSession), not useMutation(UpsertProduct)
console.log('existing', JSON.stringify(existing, null, 2))
console.log('incoming', JSON.stringify(incoming, null, 2))
// remove duplicates when latestProduct has the same id as an existing product — [..., latestProduct]
if (incoming.filter(p => p.id === incoming[incoming.length - 1].id).length > 1) return existing
return incoming
}
}
}
}
}
})
})
Initialisation of Session:
const HydrateSession = gql`
query {
session {
id
products {
id
}
}
}
`
...
useQuery(HydrateSession)
Updating products later using cache.modify:
const UpsertProduct = gql`
mutation UpsertProduct($product: ProductInput!) {
upsertProduct(product: $product) {
id
}
}
`
...
const [upsertProductMutation] = useMutation(UpsertProduct)
const onClick = async () => {
await upsertProductMutation({
variables: {
product: {
id: 2
}
},
update: (cache, mutationResult) => {
cache.modify({
id: 'Session:1',
fields: {
products: previous => [...previous, mutationResult.data.upsertProduct]
}
})
}
})
}
I have a full working example here https://github.com/jsindos/apollo-play, run npm i and then start two separate processes with npm start and npm serve. After clicking the button triggering the mutation, the merge function is not run (as seen by the absence of console.log statements in the console).
modify circumvents any merge functions you've defined, which means that fields are always overwritten with exactly the values you specify.
https://www.apollographql.com/docs/react/caching/cache-interaction/#using-cachemodify
Reading documentation is a good thing.

Apollo GraphQL merge cached data

I have a page that consists of 2 components and each of them has its own request for data
for example
<MovieInfo movieId={queryParamsId}/>
const GET_MOVIE_INFO = `gql
query($id: String!){
movie(id: $id){
name
description
}
}`
Next component
<MovieActors movieId={queryParamsId}/>
const GET_MOVIE_ACTORS = `gql
query($id: String!){
movie(id: $id){
actors
}
}`
For each of these queries I use apollo hook
const { data, loading, error } = useQuery(GET_DATA, {variable: {id: queryParamsId}}))
Everything is fine, but I got a warning message:
Cache data may be lost when replacing the movie field of a Query object.
To address this problem (which is not a bug in Apollo Client), either ensure all objects of type Movie have IDs, or define a custom merge function for the Query.movie field, so InMemoryCache can safely merge these objects: { ... }
It's works ok with google chrome, but this error affects Safari browser. Everything is crushing. I'm 100% sure it's because of this warning message. On the first request, I set Movie data in the cache, on the second request to the same query I just replace old data with new, so previous cached data is undefined. How can I resolve this problem?
Here is the same solution mentioned by Thomas but a bit shorter
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
YOUR_FIELD: {
// shorthand
merge: true,
},
},
},
},
});
This is same as the following
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
YOUR_FIELD: {
merge(existing, incoming, { mergeObjects }) {
return mergeObjects(existing, incoming);
},
},
},
},
},
});
Solved!
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
YOUR_FIELD: {
merge(existing = [], incoming: any) {
return { ...existing, ...incoming };
// this part of code is depends what you actually need to do, in my
case i had to save my incoming data as single object in cache
}
}
}
}
}
})
});
The other answers still work, but as of Apollo Client >= 3.3 there's an easier option that doesn't require specifying specific fields or a custom merge function. Instead, you only have to specify the type and it will merge all fields for that type:
const cache = new InMemoryCache({
typePolicies: {
YOUR_TYPE_NAME: {
merge: true,
}
}
});
From your example query, I'd guess that an id field should be available though? Try requesting the ID in your query, that should solve the problem in a much more ideal way.
Had same issue with inconsistency of data values vs. our schema. A value type within an entity was missing the id value. Caused by an incomplete data migration.
Temporary solution:
const typePolicies = {
PROBLEM_TYPE: {
keyFields: false as false,
},
PARENT_TYPE: {
fields: {
PROBLEM_FIELD: {
merge: true
}
}
}
}

Querying child node fields in Gatsby

I have the following GraphQL schema, which defines 3 types: a CondaPackage which hasmany CondaVersion, which hasmany CondaExecutable. I want to be able to query a CondaVersion and ask "how many CondaExecutables do you own which succeeded my analysis". Currently I've written a succeededExeCount and allExeCount which resolve this field by loading all children and manually counting the number of children that succeeded.
exports.createSchemaCustomization = ({ actions: { createTypes }, schema }) => {
createTypes([
schema.buildObjectType({
name: "CondaPackage",
fields: {
succeededExeCount: {
type: "Int!",
resolve(source, args, context){
// TODO
}
},
allExeCount: {
type: "Int!",
resolve(source, args, context){
// TODO
}
}
},
interfaces: ["Node"]
}),
schema.buildObjectType({
name: "CondaVersion",
fields: {
succeededExeCount: {
type: "Float!",
resolve(source, args, context){
const children = context.nodeModel.getNodesByIds({
ids: source.children,
type: "CondaExecutable"
})
return children.reduce((acc, curr) => acc + curr.fields.succeeded, 0)
}
},
allExeCount: {
type: "Int!",
resolve(source, args, context){
return source.children.length;
}
}
},
interfaces: ["Node"]
}),
schema.buildObjectType({
name: "CondaExecutable",
fields: {
succeeded: {
type: "Boolean!",
resolve(source, args, context, info) {
return source.fields.succeeded || false;
}
},
},
interfaces: ["Node"]
})
])
}
My first problem is that this seems incredibly inefficient. For each CondaVersion I'm running a separate query for its children, which is a classic N+1 query problem. Is there a way to tell Gatsby/GraphQL to simply "join" the two tables like I would using SQL to avoid this?
My second problem is that I now need to count the number of succeeding children from the top level type: CondaPackage. I want to ask "how many CondaExecutables do your child CondaVersions own which succeeded my analysis". Again, in SQL this would be easy because I would just JOIN the 3 types. However, the only way I can currently do this is by using getNodesByIds for each child, and then for each child's child, which is n*m*o runtime, which is terrifying. I would like to run a GraphQL query as part of the field resolution which lets me grab the succeededExeCount from each child. However, Gatsby's runQuery seems to return nodes without including derived fields, and it won't let me select additional fields to return. How can I access fields on a node's child's child in Gatsby?
Edit
Here's the response from a Gatsby maintainer regarding the workaround:
Gatsby has an internal mechanism to filter/sort by fields with custom resolvers. We call it materialization. [...] The problem is that this is not a public API. This is a sort of implementation detail that may change someday and that's why it is not documented.
See the full thread here.
Original Answer
Here's a little 'secret' (not mentioned anywhere in the docs at the time of writing):
When you use runQuery, Gatsby will try to resolve derived fields... but only if that field is passed to the query's options (filter, sort, group, distinct).
For example, in CondaVersion, instead of accessing children nodes and look up fields.succeeded, you can do this:
const succeededNodes = await context.nodeModel.runQuery({
type: "CondaExecutable",
query: { filter: { succeeded: { eq: true } } }
})
Same thing for CondaPackage. You might try to do this
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
query: {}
})
return versionNodes.reduce((acc, nodes) => acc + node.succeededExeCount, 0) // Error
You'll probably find that succeededExeCount is undefined.
The trick is to do this:
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
- query: {}
+ query: { filter: { succeededExeCount: { gte: 0 } } }
})
It's counter intuitive, because you'd think Gatsby would just resolve all resolvable fields on a type. Instead it only resolves fields that is 'used'. So to get around this, we add a filter that supposedly does nothing.
But that's not all yet, node.succeededExeCount is still undefined.
The resolved data (succeededExeCount) is not directly stored on the node itself, but in node.__gatsby_resolved source. We'll have to access it there instead.
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
query: { filter: { succeededExeCount: { gte: 0 } } }
})
return versionNodes.reduce((acc, node) => acc + node.__gatsby_resolved.succeededExeCount, 0)
Give it a try & let me know if that works.
PS: I notice that you probably use createNodeField (in CondaExec's node.fields.succeeded?) createTypes is also accessible in exports.sourceNodes, so you might be able to add this succeeded field directly.

How to tell vuefire's firestore to execute after "created" hook?

I am using vuefire for my new Vue.js project. But there is a problem with initialization...
One of my data property need to be intialized via a method. To do this, I use the created hook of Vue.js. Actually I need the value of this property for my firestore request.
Problem is : the firestore request seems to run before the created hook.
How can I run my firestore requests after the created hook as been run ?
Thank !
data: function () {
return {
currentWeek: null
}
},
firestore: function (currentWeek) {
return {
slots: db.collection('slots').where('start', '>=', this.currentWeek)
}
},
created: function () {
this.currentWeek = moment().startOf('week')
},
this is not the exact code but, basically you want to use the $firestoreRefs to make the query yourself inside of the created lifecycle method
data: function () {
return {
currentWeek: null,
slots : []
}
},
created: function () {
this.currentWeek = moment().startOf('week')
this.slots = this.$firestoreRefs['slots'].where('start', '>=', this.currentWeek)
},
I found that I can solve this issue with watch and binding :
data: function () {
return {
currentWeek: null,
slots: []
}
},
created: function () {
this.currentWeek = moment().startOf('week')
},
watch: {
currentWeek (currentWeek) {
this.$bind('slots', db.collection('slots')
.where('start', '>=', moment(currentWeek).toDate())
}
}
I am pretty sure a better solution can be found...

Categories

Resources