Fixing result overlaps caused by parallel requests in Redux
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.
