I'm having this weird issue where my RTK Query requests are happening in a strange order.
We've got the RTK sports
slice, and in the same file I've defined the useLoadSports
hook
const sportsSlice = createSlice({
name: 'sports', initialState: {},
reducers: {
setSports: (state, action) => action.payload,
},
});
export const sports = sportsSlice.reducer;
export const { setSports } = sportsSlice.actions;
export const useLoadSports = () => {
const dispatch = useDispatch();
const { data: sports, ...result } = useGetSportsQuery();
useEffect(() => { console.log('useLoadSports'); }, []);
useEffect(() => {
if (sports) {
console.log('SETTING SPORTS');
dispatch(setSports(sports));
}
}, [sports]);
return result;
};
The Application
component uses this hook as it loads some data needed throughout the app.
const useInitialLoad = () => {
useEffect(() => {
console.log('useInitialLoad');
}, []);
const { isLoading: sportsLoading } = useLoadSports(); // below
const ready = !sportsLoading;
return { ready };
};
const Application: React.FC<Props> = () => {
const { ready } = useInitialLoad();
if (!ready) return <h1>Loading app data</h1>;
return (
<S.Wrapper>
<AppRouter />
</S.Wrapper>
);
};
The AppRouter
actually iterates over a config object to create Route
s. I'm assuming that's not our issue here.
Anyway, then the PlayerPropsPage
component calls useGetPropsDashboardQuery
.
const PlayerPropsPage = () => {
const { data, isLoading, isError, error } = useGetPropsDashboardQuery();
useEffect(() => {
console.log('LOADING PlayerPropsPage');
}, [])
return /* markup */
}
The query's queryFn
uses the sports that were saved into the store by useLoadSports
export const { useGetPropsDashboardQuery, ...extendedApi } = adminApi.injectEndpoints({
endpoints: build => ({
getPropsDashboard: build.query<PropAdminUIDashBoard, void>({
queryFn: async (_args, { getState }, _extraOptions, baseQuery) => {
console.log('PROPS ENDPOINT');
const result = await baseQuery({ url });
const dashboard = result.data as PropAdminDashBoard;
const { sports } = getState() as RootState;
if (!Object.entries(sports).length) {
throw new Error('No sports found');
}
// use the sports, etc.
},
}),
}),
});
I'd think it would use the setSports
action before even rendering the router, and I'd really think it would render the PlayerPropsPage
before calling the props query, but here's the log:
useInitialLoad
useLoadSports
PROPS ENDPOINT
LOADING PlayerPropsPage
SETTING SPORTS
Another crazy thing is if I move the getState()
call in the endpoint above the call to baseQuery
, the sports haven't been stored yet, and the error is thrown.
Why is this happening this way?
CodePudding user response:
The current behaviour is normal even if it's not what you expect.
- you have a main hook who do a query
- the first query start
- when the query is finish it does "rerender" and does this
- dispatch the result of the sports
- the second query start
So there is no guarantee that you have sports in the store when you start the second query. As all is done with hooks (you can technically do it with hooks but that's another topic)
How to wait a thunk result to trigger another one ?
You have multiple ways to do it. It depends also if you need to wait thoses two queries or not.
Listener middleware
If you want to run some logic when a thunk is finish, having a listener can help you.
listenerMiddleware.startListening({
matcher: sportsApi.endpoints.getSports.fulfilled,
effect: async (action, listenerApi) => {
listenerApi.dispatch(dashboardApi.endpoints.getPropsDashboard.initiate())
}
},
})
In addition, instead of setting sports in the store with a dispatch
inside the useEffect
. You can plug your query into the extraReducers. here is an example:
createSlice({
name: 'sports',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addMatcher(
sportsApi.endpoints.getSports.fulfilled,
(state, action) => {
state.sports = action.payload.sports
}
)
},
})
Injecting thunk arguments with a selector
If you use directly pass sports as a variable to the query when they change they'll re-trigger the query. here is an example:
const PlayerPropsPage = () => {
const { data: sports, ...result } = useGetSportsQuery();
const { data, isLoading, isError, error } = useGetPropsDashboardQuery(sports);
}
Doing the two query inside a single queryFn
If inside a single queryFn
you can chain theses query by awaiting them
queryFn: async (_args, { getState }, _extraOptions, baseQuery) => {
const resultFirstQuery = await baseQuery({ url: firstQueryUrl });
const resultSecondQuery = await baseQuery({ url: secondQueryUrl });
// Do stuff
},
Note:
- When you use
getState()
inside a thunk, if the store update this will not trigger your thunk "automatically" - I do not know if you need the sport to do the second query or to group the result of the two queries together.
CodePudding user response:
A bunch of random observations:
- you should really not dispatch that
setSports
action in a useEffect here. If you really want to have a slice with the result of youruseGetSportsQuery
, then add anextraReducers
forapi.endpoints.getSports.fulfilled
. See this example:
// from the example
extraReducers: (builder) => {
builder.addMatcher(
api.endpoints.login.matchFulfilled,
(state, { payload }) => {
state.token = payload.token
state.user = payload.user
}
)
},
I don't see why you even copy that data into the state just to use a complicated
queryFn
instead of just passing it down as props, using aquery
and passing it in asuseGetPropsDashboardQuery(sports)
. That way thatgetPropsDashboard
will update if thesports
argument changes - which will never happen if you take the extra logic with thegetState()
and all the other magic.you could even simplify this further:
const { data: sports } = useGetSportsQuery()
const result = useGetPropsDashboardQuery( sports ? sports : skipToken )
No need for a slice, no need to have that logic spread over multiple components, no need for a queryFn. queryFn is an escape hatch and it really doesn't seem like you need it.