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
Related
I currently have some issues trying to add the infinite query feature to a recipes app I'm working on using Edamam API.
All the examples I have looked for (even React Query's documentation) implement the infinite scroll using a page/cursor number system... I understand this is the ideal way, but... Edamam API doesn't work this way with paginated queries.
Instead, the API has the following structure for each recipe query we look for (let's assume we are searching for "chicken", this would be the JSON structure):
from: 1,
to: 20,
count: 10000,
_links: {
next: {
href: "https://api.edamam.com/api/recipes/v2?q=chicken&app_key=APIKEYc&_cont=CHcVQBtNNQphDmgVQntAEX4BYldtBAAGRmxGC2ERYVJ2BwoVX3cVBWQSY1EhBQcGEmNHVmMTYFEgDQQCFTNJBGQUMQZxVhFqX3cWQT1OcV9xBB8VADQWVhFCPwoxXVZEITQeVDcBaR4-SQ%3D%3D&type=public&app_id=APPID"
title: "Next Page"
}
},
hits: [{}] ... (This is where the actual recipes are)
As you can see, there is no numbering system for paginated queries, instead, it's a whole URL and it's giving me a hard time since I'm also new to React Query.
I tried the following, but it just fetches the same data over and over again as I reach the bottom of the page:
const getRecipes = async ({ pageParam }) => {
try {
const path = pageParam
? pageParam
: `https://api.edamam.com/api/recipes/v2?q=${query}&app_id=${process.env.REACT_APP_APP_ID}&app_key=${process.env.REACT_APP_API_KEY}&type=public`;
const response = await axios.get(path);
return response.data;
} catch (error) {
console.log(error);
}
const { ref, inView } = useInView();
useEffect(() => {
inView && fetchNextPage();
}, [inView]);
const {
data,
isFetching,
isFetchingNextPage,
error,
status,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery(
["recipes", query],
({ pageParam = "" }) => getRecipes(pageParam),
{
getNextPageParam: (lastPage) => lastPage._links.next.href,
}
);
Since the next page param is a whole URL, I just say that IF there is a pageParam, then use that URL for the request, if not, then do a normal request using the query value the user is searching for.
Please help!
Since the next page param is a whole URL, I just say that IF there is a pageParam, then use that URL for the request, if not, then do a normal request using the query value the user is searching for.
I'd say that this is the correct approach. The only code issue I can see in your example is that you destruct page param, and then pass the page param string to getRecipes:
({ pageParam = "" }) => getRecipes(pageParam),
but in getRecipes, you expect an object to come in (which you again destructure):
const getRecipes = async ({ pageParam }) => {
You can fix that by either changing the call side, or the function syntax, and then it should work.
hope you're well. I've been working on a company list component. I am having a problem with updating it in real time, because the axios call I'm sending to 'getById' after it renders is returning only a promise and not the actual data that it is supposed to and I don't have any idea as to why. So when I push the so called new company that I've just added into the array, which is in state, it is only pushing a promise into the array and not the actual Company. I don't have any idea why this is. What the code is supposed to be doing, is it is supposed to be putting the new company into the database, returning the success result, and then I'm using the item from that to make a fresh get call to the axios DB which is supposed to be returning the information I just entered so that I can then insert it into the same array in state that is within the list that is rendered in the company list. However, as I mentioned, only the promise is coming up for some reason.
At one point I was able to get this working, but I did that by essentially calling, 'componentDidMount' after the promise was pushed into the call back clause of the setState funciton of the push function - which was essentially causing the entire component to re-render. I'm a fairly new coder, but my understanding is is that that is a fairly unconventional way to code something, and contrary to good coding methodologies. I believe I should be able to push it into state, and then have it change automatically. I have attached the relevant code below for you to examine. If you believe you need more please let me know. If someone could please tell me why I am getting this weird promise instead of the proper response object, so that I can then insert that into state, I would greatly appreciate it. I've attached some images below the code snippets that I hope will be helpful in providing an answer. I have also left brief descriptions of what they are.
:
class Companies extends React.Component {
constructor(props) {
super(props);
this.state = {
Companies: [],
formData: { label: "", value: 0 },
};
}
componentDidMount = () => {
this.getListCompanies();
};
getListCompanies = () => {
getAll().then(this.listOfCompaniesSuccess);
};
listOfCompaniesSuccess = (config) => {
let companyList = config.items;
this.setState((prevState) => {
return {
...prevState,
Companies: companyList,
};
});
};
onCompListError = (errResponse) => {
_logger(errResponse);
};
mapCompanies = (Companies) => (
<CompaniesList Companies={Companies} remove={remove} />
);
handleSubmit = (values) => {
if (values.companyName === "PPP") {
this.toastError();
//THIS IS FOR TESTING.
} else {
add(values)
.getById(values.item) //I HAVE TRIED IN TWO DIFFERENT PLACES TO GET THE NEW COMPANY IN. HERE
.then(this.newCompanyPush)
.then(this.toastSuccess)
.catch(this.toastError);
}
};
//THIS CODE RIGHT HERE IS THE CODE CAUSING THE ISSUE.
newCompanyPush = (response) => {
let newCompany = getById(response.item); // THIS IS THE OTHER PLACE I HAVE TRIED.
this.setState((prevState) => {
let newCompanyList = [...prevState.Companies];
newCompanyList.push(newCompany);
return {
Companies: newCompanyList,
};
});
};
toastSuccess = () => {
toast.success("Success", {
closeOnClick: true,
position: "top-right",
});
};
toastError = (number) => {
toast.error(`Error, index is ${number}`, {
closeOnClick: true,
position: "top-center",
});
};
This is the axios call I am using.
const getById = (id) => {
const config = {
method: "GET",
url: companyUrls + id,
withCredentials: true,
crossdomain: true,
headers: { "Content-Type": "application/json" },
};
return axios(config).then(onGlobalSuccess).catch(onGlobalError);
};
After the promise is pushed into the array, this is what it looks like. Which is I guess good news because something is actually rendering in real time.
This is the 'promise' that is being pushed into the array. Please note, when I make the same call in post-man, I get an entirely different response, see below.
This is the outcome I get in postman, when I test the call.
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])
My question is a continuation of this excellent question and answer concerning the shape of paginated data in a redux store. I am using ngrx/store in an angular 2 app.
{
entities: {
users: {
1: { id: 1, name: 'Dan' },
42: { id: 42, name: 'Mary' }
}
},
visibleUsers: {
ids: [1, 42],
isFetching: false,
offset: 0
}
}
Based on the above shape I believe if the offset (or page, sort, etc.) from an incoming request payload changed then the visible users would change as well as the user entities by calling the DB. I have some actions and reducer functions to handle this and it works as expected. If the offset remains the same and the user is returning to the page the way they left it then the user entities should be returned by the store not the DB.
Where I am struggling is where to put that logic and which rxjs operators to use (still learning this).
I think the correct place is an effect. Here is what I have now in my angular2 app (I am injecting Actions, Store, and my UserService) that pulls new data every time the page is loaded.
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.switchMap(() => this.userService.query()
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
My best idea is something like this:
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.withLatestFrom(this.store.select(state => state.visibleUsers.offset))
.switchMap(([action, state]) => {
//something that looks like this??
//this syntax is wrong and I can't figure out how to access the action payload
state.offset === payload.offset
? this.store.select(state => state.entities.users)
: this.userService.query()
}
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
Not sure how to make this work. Thanks ahead.
I don't like answering my own questions but it took me quite a while to find the answer to this. I won't accept this answer as I am not sure this is the best way to go about things (still learning the ins/outs). It does however, work perfectly.
I found the correct syntax in a github gist. This code does not match my example but it clearly demonstrates "2 options for conditional ngrx effects" by either returning a store or api observable using some sort of condition.
Hope this helps someone.
Here it is:
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
return Observable.if(
() => existsInStore,
Observable.of(new storeActions.SetSelectedStore(storeName)),
this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store))
);
});
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
let obs;
if (existsInStore) {
obs = Observable.of(new storeActions.SetSelectedStore(storeName));
} else {
obs = this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store));
}
return obs;
});
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