Home > database >  RTK Query endpoints and RTK actions happening out of order
RTK Query endpoints and RTK actions happening out of order

Time:07-22

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 Routes. 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 your useGetSportsQuery, then add an extraReducers for api.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 a query and passing it in as useGetPropsDashboardQuery(sports). That way that getPropsDashboard will update if the sports argument changes - which will never happen if you take the extra logic with the getState() 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.

  • Related