Original article © thetwilightcoder.com

Fixing result overlaps caused by parallel requests in Redux

Created on November 1, 2025.

For this article, if you need full context about the parallel request problem, please check the previous post:
Fixing result overlaps caused by parallel requests in Redux

I will assume that you are already familiar with Redux.

Now, let’s update the code: instead of using local state, we will switch to Redux.

First, let's create a slice

1import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
2import type { PayloadAction } from '@reduxjs/toolkit'
3
4interface SearchResult {
5  bonus: number;
6  dateOffBonus: number;
7  dummyValue: number;
8}
9
10interface SearchState {
11  joinedYear: string;
12  loading: boolean;
13  result: SearchResult | null;
14  error: string | null;
15}
16
17const initialState: SearchState = {
18  joinedYear: '',
19  loading: false,
20  result: null,
21  error: null,
22}
23
24export const searchUser = createAsyncThunk(
25  'search/searchUser',
26  async (joinYear: string, { signal }) => {
27    const response = await fetch('http://localhost:3001/api/search', {
28      method: 'POST',
29      headers: {
30        'Content-Type': 'application/json',
31      },
32      body: JSON.stringify({ joinYear }),
33      signal
34    });
35    
36    const data = await response.json();
37    return data.result;
38  }
39)
40
41export const searchSlice = createSlice({
42  name: 'search',
43  initialState,
44  reducers: {
45    setJoinedYear: (state, action: PayloadAction<string>) => {
46      state.joinedYear = action.payload
47    },
48    clearResult: (state) => {
49      state.result = null
50      state.error = null
51    }
52  },
53  extraReducers: (builder) => {
54    builder
55      .addCase(searchUser.pending, (state) => {
56        state.loading = true
57        state.error = null
58        state.result = null
59      })
60      .addCase(searchUser.fulfilled, (state, action) => {
61        state.loading = false
62        state.result = action.payload
63      })
64      .addCase(searchUser.rejected, (state, action) => {
65        state.loading = false
66        if (action.error.name !== 'AbortError') {
67          state.error = action.error.message || 'Search failed'
68        }
69      })
70  },
71})
72
73export const { setJoinedYear, clearResult } = searchSlice.actions
74export default searchSlice.reducer

"searchUser" will represent the entire flow of calling the API.

Now we can use this slice in our app.

1import { useRef } from 'react';
2import { useSelector, useDispatch } from 'react-redux';
3import { RootState, AppDispatch } from '../Redux/store';
4import { searchUser, setJoinedYear } from '../Redux/slice/searchSlice';
5
6export const AbortControllerTest = () => {
7  const dispatch = useDispatch<AppDispatch>();
8  const { joinedYear, loading, result } = useSelector((state: RootState) => state.search);
9  const requestRef = useRef<any>(null);
10
11  const years = Array.from({ length: 25 }, (_, i) => 2000 + i);
12
13  const handleSearch = () => {
14    // Cancel previous request if exists
15    if (requestRef.current) {
16      requestRef.current.abort();
17    }
18    
19    // Dispatch and store the request reference
20    requestRef.current = dispatch(searchUser(joinedYear));
21  };
22  
23
24  return (
25    <div>
26      <select
27        value={joinedYear}
28        onChange={(e) => dispatch(setJoinedYear(e.target.value))}
29      >
30        <option value="">Select Joined Year</option>
31        {years.map(year => (
32          <option key={year} value={year}>{year}</option>
33        ))}
34      </select>
35      <button onClick={handleSearch}>
36        {'Search'}
37      </button>
38    
39      
40      {loading ? <div><p>loading</p></div> : result && (
41        <div>
42          <p>Bonus: {result.bonus}</p>
43          <p>Days Off: {result.dateOffBonus}</p>
44        </div>
45      )}
46    </div>
47  );
48};

At line 20, dispatch returns an async thunk promise that represents the entire async flow. It also includes an abort function, which can be used to cancel the whole process.

Now let’s test it. Search for 2003, then don’t wait — search for 2024 right after. You’ll see that the request for 2003 is canceled in the Network tab.

Enjoyed this post? Buy me a coffee ☕ Donate