Chain multiple dependent queries with RTK Query - javascript

so I am trying to 'chain' togther multiple queries that are somewhat dependent on each other, using RTK Query and I'm not getting very far...
APIS
import { baseApi } from '#/lib/rtkQuery/baseApi';
export const personContactApi = baseApi
.enhanceEndpoints({ addTagTypes: ['mail_packs'] })
.injectEndpoints({
endpoints: (build) => ({
createList: build.mutation({
query: (body) => {
return {
url: `/person/list/`,
method: 'POST',
body,
};
},
}),
addPersonsToList: build.mutation({
query: ({ ListId, personArray }) => {
return {
url: `/person/list/${ListId}/add-persons/`,
method: 'POST',
body: { persons: personArray },
};
},
}),
sendList: build.mutation({
query: ({ ListId }) => {
return {
url: `/person/list/${ListId}/submit/`,
method: 'POST',
};
},
}),
}),
});
export const { useCreateListMutation, useAddpersonsToListMutation, useSendListMutation } =
personContactApi;
Query functions
const [createList, { data: listResponseObject, isSuccess: createListSuccess, isError: createListError }] = useCreateListMutation();
const [addPersonsToListMutation, { isSuccess: addPersonsToListSuccess, isError: addPersonsToListError }] = useAddPersonsToListMutation();
const [sendList, { isSuccess: sendListSuccess, isError: sendListError }] = useSendListMutation();
useEffect
useEffect(() => {
// When list successfully created, add persons to list
if (createListSuccess) {
addPersonsToListMutation({
ListId: listResponseObject?.id,
personsArray: selectedPersons,
});
}
}, [
addPersonsToListMutation,
createListSuccess,
listResponseObject,
selectedPersons,
]);
useEffect(() => {
// When persons have been successfully added to mailing list, send mailing list
if (addPersonsToListSuccess) {
sendList({
listId: listResponseObject?.id,
});
}
}, [
addPersonsToListSuccess,
listResponseObject,
sendList,
]);
These are the 3 queries / mutations and they need to go in order, once the create query is success we fire the add, once that is a success we fire the send
The add and send queries are also dependent on an id returned in the response from the createList query, and the add query required an array of ids representing the person objects being added to the list
I've hacked togther a solution using multiple useEffects but it is very brittle and obviously not the ideal way to handle this situation, any one have a better way I'm all ears.

One way of solving this would be to follow one of their official suggestions:
If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
In your example, the code would be something like this
createList(input_object_goes_here)
.unwrap()
.then(createListResponse => {
addPersonsToListMutation(createListResponse.values_you_need)
.unwrap()
.then(addPersonsResponse => sendList({ listId: addPersonsResponse.id }))
})
Hope I got the syntax right, but you should get the idea.
Source: https://redux-toolkit.js.org/rtk-query/usage/error-handling

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

How to wrap Sequelize result in another object - Adding parent "params" for Nextjs getStaticPaths()

I am trying to fulfill the getStaticPaths of NextJS that demand records to be formatted as:
Which demand the format like so:
[
{
params: {
id: recordId
}
}
]
I can achieve this result with:
const res = await myModel.findAll({
raw: true,
attributes: ['id'],
});
const fixedFormat = res.map(singleRes => {
return {
params: {
id: singleRes.id,
},
};
});
But I was wondering if there's a way that doesn't involve calling map() on such a big array.

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.

Defining custom merge function to resolve InMemoryCache merge in GraphQL

I'm getting a warning:
Cache data may be lost when replacing the parts field of a Query object.
To address this problem (which is not a bug in Apollo Client), define a custom merge function for the Query.parts field, so InMemoryCache can safely merge these objects:
existing: [{"__ref":"Part:53"},{"__ref":"Part:55"},{"__ref":"Part:56"},{"__ref":"Part:57"},{"__ref":"Part:58"}]
incoming: [{"__ref":"Part:53"},{"__ref":"Part:55"},{"__ref":"Part:56"},{"__ref":"Part:57"}]
Now here is my Part type:
type Part {
id: ID!
created_at: DateTime!
updated_at: DateTime!
partName: String
partDescription: String
partQuantity: Long
usePercentage: Boolean
partPercentage: Float
type: String
published_at: DateTime
products(sort: String, limit: Int, start: Int, where: JSON): [Product]
partImage(sort: String, limit: Int, start: Int, where: JSON): [UploadFile]
stockevents(sort: String, limit: Int, start: Int, where: JSON): [Stockevent]
}
This warning triggers after I remove one part using mutation to delete a single part. Here it is:
const [partDelete] = useMutation(DELETE_PART, {
update(cache, { data }) {
const newData = Object.values(data)
const refresh = newData.map(name => name.part)
const refined = refresh.map(item => item.id)
cache.evict({
id: cache.identify({
id: refined.id
})
})
cache.writeQuery({
query: GET_PARTS
})
},
refetchQueries: [
{ query: GET_PARTS }
]
})
I am passing payload in a separate function and everything works but I keep getting this cache warning so I want to deal with it now.
I've went with updating InMemoryCache in my index.js but it still doesn't work:
export const client = new ApolloClient({
link,
cache: new InMemoryCache({
typePolicies: {
Part: {
merge(existing = [], incoming = []) {
return [...existing, ...incoming];
}
}
}
})
});
I've also tried to return only ...incoming but nothing different happens.
Thanks in advance, cheers!
The issue was in the structure of InMemoryCache config. After I changed it to this it worked:
export const client = new ApolloClient({
link,
cache: new InMemoryCache({
typePolicies: {
Query: {
Part: {
parts: {
fields: {
merge(existing, incoming) {
return incoming;
}
}
}
}
}
}
})
});
I have also removed update option from the mutation that includes evict and modify.
when you see this warning:
To address this problem (which is not a bug in Apollo Client), define a custom merge function for the Query.your_query_name field, so InMemoryCache can safely merge these objects:
try this for shorten:
const client = new ApolloClient({
uri: "your_API_link",
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
your_Query_Name: {
merge: (existing = [], incoming) => {
return incoming;
},
},
},
},
},
}),
});

VueX: How to architecture my store with nested objects

I am currently coding an application in VueJS (and with Vuex in particular). However, my question is not strongly linked to this library, but rather to the architecture to have with a store like flux/redux/Vuex.
To put it simply, I have several APIs (one API/database per team), and for each team/API, I have several users.These teams and users are represented by simple objects, and each has its own slug. Important note: the slugs of the teams are of course unique, but the slugs users are unique for their own team. The uniqueness constraint for a user would then be "teamSlug/userSlug". And given the large number of users, I can not simply load all the users of all the teams.
My question is how to properly architect my application/store in order to recover the data of a given user slug (with his team): if I have not already loaded this user, make an API request to retrieve it. Currently I have created a getter that returns the user object, which takes the slug from the user and the team. If it returns "null" or with a ".loading" to "false", I have to run the "loadOne" action that will take care of retrieving it:
import * as types from '../../mutation-types'
import users from '../../../api/users'
// initial state
const state = {
users: {}
}
// getters
const getters = {
getOne: state => (team, slug) => (state.users[team] || {})[slug] || null
}
// actions
const actions = {
loadOne ({ commit, state }, { team, slug }) {
commit(types.TEAM_USER_REQUEST, { team, slug })
users.getOne(team, slug)
.then(data => commit(types.TEAM_USER_SUCCESS, { team, slug, data }))
.catch(error => commit(types.TEAM_USER_FAILURE, { team, slug, error }))
}
}
// mutations
const mutations = {
[types.TEAM_USER_REQUEST] (state, { team, slug }) {
state.users = {
...state.users,
[team]: {
...(state.users[team] || {}),
[slug]: {
loading: true,
error: null,
slug
}
}
}
},
[types.TEAM_USER_SUCCESS] (state, { team, slug, data }) {
state.users = {
...state.users,
[team]: {
...(state.users[team] || {}),
[slug]: {
...data,
slug,
loading: false
}
}
}
},
[types.TEAM_USER_FAILURE] (state, { team, slug, error }) {
state.users = {
...state.users,
[team]: {
...(state.users[team] || {}),
[slug]: {
slug,
loading: false,
error
}
}
}
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
You imagine that a team does not only have users, I have many other models of that type, and I should link them together. This method works, but I find it rather cumbersome to put in place (especially that it is a simple get, I will have plenty of other actions of this kind). Would you have any advice on my architecture?
Thank you!
I've found that the best way to keep the Vuex store flexible is to normalize it and keep your data items as flat as possible. That means storing all of your users in one structure and finding a way to uniquely identify them.
What if we combine the team and user slug to create a unique identifier? Here's how I imagine your users with a red team and a blue team:
const state = {
users: {
allTeamSlugs: [
'blue1',
'blue2',
'blue3',
'red1',
'red2',
// etc...
],
byTeamSlug: {
blue1: {
slug: 1,
team: 'blue',
teamSlug: 'blue1'
},
// blue2, blue3, etc...
red1: {
slug: 1,
team: 'red',
teamSlug: 'red1'
},
// red2, etc...
}
}
}
And the teamSlug property doesn't need to exist for each user in your API. You can create it in your mutation as you load data into the store.
const mutations = {
[types.TEAM_USER_SUCCESS] (state, { team, slug, data }) {
const teamSlug = [team, slug].join('')
state.users.byTeamSlug = {
...state.users.byTeamSlug,
[teamSlug]: {
...data,
slug: slug,
team: team,
teamSlug: teamSlug
}
}
state.users.allTeamSlugs = [
...new Set([ // The Set ensures uniqueness
...state.users.allTeamSlugs,
teamSlug
])
]
},
// ...
}
Then your getters might work like this:
const getters = {
allUsers: state => {
return state.users.allTeamSlugs.map((teamSlug) => {
return state.users.byTeamSlug[teamSlug];
});
},
usersByTeam: (state, getters) => (inputTeam) => {
return getters.allUsers.filter((user) => user.team === inputTeam);
},
getOne: state => (team, slug) => { // or maybe "userByTeamSlug"?
const teamSlug = [team, slug].join('');
return state.users.byTeamSlug[teamSlug]; // or undefined if none found
}
}
Redux has a great article about normalization that I always find myself coming back to: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape#designing-a-normalized-state

Categories

Resources