paint-brush
Are React and RTK Query a New Easy Way for Redux?by@@AlexShulgin000
New Story

Are React and RTK Query a New Easy Way for Redux?

by Alex Shulgin5mApril 10th, 2025
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The standard request template looks like with manipulation of statuses and data display. It is versatile as a Swiss knife and can be completely customized. Using this template, we can perform requests of any complexity and control all the details. Let's see what our request looks like now.
featured image - Are React and RTK Query a New Easy Way for Redux?
Alex Shulgin HackerNoon profile picture
0-item
1-item

First of all, let's remember what the classic request and data storage looks like with redux-saga and @reduxjs/toolkit.

// slice
// We use an enum with statuses to easily manipulate them in the future.
enum EOperationStatus {
  initial = "initial",
  pending = "pending",
  error = "error",
  success = "success",
}

const initialState: IInitialState = {
  items: [],
  itemsStatus: EOperationStatus.initial,
};

export const itemsSlice = createSlice({
  name: "items",
  initialState,
  reducers: {
    getItems: state => {
      state.itemsStatus = EOperationStatus.pending;
    },
    setItems: (state, {payload}: PayloadAction<IInitialState["items"]>) => {
      state.items = payload;
    },
    setItemsStatus: (
      state, { payload}: PayloadAction<EOperationStatus>,
    ) => {
      state.itemsStatus = payload;
    },
    ...

// api
const getItems = async () => await HttpHandler.get<IItem[]>("/items");
export const API = {
  getItems, 
};

// saga
function* getItemsSaga() {
  try {
    const res = yield* call(API.getItems);
    yield* put(itemsSlice.setItems(res));
    // На простых запросах нам постоянно нужно будет писать одно и тоже:
    // сохраняем результат и обновляем статусы.
    yield* put(itemsSlice.setItemsStatus(EOperationStatus.success));
  } catch (e) {
    yield* put(itemsSlice.setItemsStatus(EOperationStatus.error));
  }
}

// selector
export const itemsSelector = selector(s => s.items);
export const itemsStatusSelector = selector(s => s.itemsStatus);

// Component.tsx
export const Component = () => {
  const items = useSelector(itemsSelector);
  const itemsStatus = useSelector(itemsStatusSelector);
  
  if(itemsStatus === EOperationStatus.initial || 
     itemsStatus === EOperationStatus.pending){
       return 'loading';
     }
     return <>{items.map(el => <div key={el.id}>{el.id}</div>)}</>
}


This is what the standard request template looks like with manipulation of statuses and data display. It is versatile as a Swiss knife and can be completely customized. Using this template, we can perform requests of any complexity and control all the details.


But what if the requests are as easy as in the example above: we just take the data and show it. Do we really need to do this routine every time if we can make our lives easier?


Just for such cases, there is an rtk query tool (included in the reduxjs/toolkit). Let's see what our request looks like now.

// rtk
interface IItem {
  id: number;
}

const apiWithTags = rtkApi.enhanceEndpoints({});

export const itemsRtkApi = apiWithTags.injectEndpoints({
  endpoints: (build) => ({
    getItems: build.query<
    IItem[],
    undefined>({
      query: () => ({
        url: `/items?q=true`,
      }),
      async onQueryStarted(_, { queryFulfilled }) {
        try {
          await queryFulfilled;
        } catch (_e) {
          noop();
        }
      },
      keepUnusedDataFor: 0,
    }),
  }),
});

// Component
...
  const { data, isError, isFetching, isLoading, status,
   error, refetch } = itemsRtkApi.useGetItemsQuery(
    undefined,
    {refetchOnMountOrArgChange: true}
  );
...


In build.query<>, we pass two arguments, 1 — the type that will be returned and 2 — the parameters that we accept in the hook, rtk parameter.


Next, in the component, we call the hook and pass the data to call the api and the second argument, the parameters for the hook itself. For example, the refetchOnMountOrArgChange property will re-request data when the component is unmounted. You can read more about each of them in the documentation.


But most importantly, in hook's response, we get all the statuses/data/other features. Each of them can be useful for a specific situation, but together they cover almost all cases.

// Component.tsx
export const Component = () => {
  const { data: items, isError, isFetching } = itemsRtkApi.useGetItemsQuery(
    undefined,
    {refetchOnMountOrArgChange: true}
  );
  
  if(isFetching){
     return 'loading';
   }
  if(isError){
     return 'isError';
   }
   return <>{items.map(el => <div key={el.id}>{el.id}</div>)}</>
}


Now the component looks light and elegant. Here we use isFetching (the difference from isLoading is that it is activated at any load, and isLoading only at the initial one). You can call refetch to re-query or check for isError in case of an error.


We can also trigger a delayed hook. The so-called lazy query.

// Component.tsx
export const Component = () => {
  const [fetchItems, 
  {isFetching, {isError, data: items, isUninitialized}] = itemsRtkApi.useLazyGetItemsQuery();
  
  useEffect(() => {
    fetchItems()
  }, [])
  
  if(isFetching || isUninitialized){
     return 'loading';
   }
  if(isError){
     return 'isError';
   }
   return <>{items.map(el => <div key={el.id}>{el.id}</div>)}</>
}


Here we make a request from the hook. It is usually necessary if we want to pass an argument to a hook that does not appear immediately. We also check isUninitialized that the hook has been initialized and then it will become false when loading.


Plus, rtk makes it possible to use mutations to update data. It will look something like this:

// rtk
...
updateItems: build.mutation<IItem[], { param: number }>({
  queryFn: async ({ param }, api, _extraOptions, baseQuery) => {
    const { data, error } = (await baseQuery(`/items/?param=${param}`)) as QueryReturnValue<
      IItem[],
      FetchBaseQueryError
    >;

    if (error) {
      return { error };
    }

    api.dispatch(
      itemsRtkApi.util.updateQueryData('getItems', undefined, (draft) => {
        return Object.assign(draft, data, [...(draft|| []), ...(data|| [])] });
      })
    );

    return { data };
  },
}),
...

What is rtk not suitable for?

For complex operations like pagination or storing variables between requests. For such cases, it is better to use sagas. After all, its main goal is to save us from routine requests.