I'm currently converting an old redux-codebase to use Redux-Toolkit (RTK), and I can't seem to understand the difference between these approaches. Consider this slice:
interface MyState {
someItems: string[];
}
const myInitialState: MyState = {
someItems: []
};
const mySlice = createSlice({
name: 'mySlice',
initialState: myInitialState,
reducers: {
setItems(state, action: PayloadAction<string[]>) {
state.someItems = action.payload;
}
},
extraReducers: builder => {
builder.addCase(fetchItems.fulfilled, (state, action) => {
state.someItems = action.payload;
});
}
});
My traditional way to use redux has been to call the reducer directly from a Thunk, but I see that the way defined in extraReducers is also possible.
const fetchItems = createAppAsyncThunk(
'mySlice/fetchItems',
async (_, thunkAPI) => {
try {
//Get data from backend
const response = await getItems();
//Option 1
thunkAPI.dispatch(mySlice.actions.setItems(response));
//Option 2
return response;
} catch (e) {
return thunkAPI.rejectWithValue('failed');
}
}
);
What is the difference between these approaches? Are any of them preferred over the other?
Lastly when doing changes I often have to reload the data for latest updates. This time I believe I have to use dispatch
inside the Thunk. Or are there alternatives?
const editItem = createAppAsyncThunk<void, string>(
'mySlice/fetchItems',
async (changedValue, thunkAPI) => {
try {
//Send data to backend
await sendSomethingToBackend(changedValue);
thunkAPI.dispatch(fetchItems());
} catch (e) {
return thunkAPI.rejectWithValue('failed');
}
}
);
Given:
const fetchItems = createAppAsyncThunk( 'mySlice/fetchItems', async (_, thunkAPI) => { try { //Get data from backend const response = await getItems(); // Option 1 thunkAPI.dispatch(mySlice.actions.setItems(response)); // Option 2 return response; } catch (e) { thunkAPI.rejectWithValue('failed'); } } );
What is the difference between these approaches?
In the grand scheme of things there's not much difference between the two with regards to the single mySlice
state slice. The main difference comes down to what, or where, can handle the "resolved" thunks.
Dispatch an explicit action: thunkAPI.dispatch(mySlice.actions.setItems(response));
mySlice
setItems
reducer function can respond and handle this action.Dispatch an implicit resolved (fulfilled) action: return response;
extraReducers
functions can respond and handle the fetchItems.fulfilled
action.Are any of them preferred over the other?
This is subjective, but using the Thunk's .pending
, .fulfilled
, and .rejected
actions is likely the generally accepted preferred method as it provides greater flexibility. One of the paramount goals of RTK was to cut down on the amount of code you needed to write yourself. Using the automatically generated Thunk actions is part of this goal.
const editItem = createAppAsyncThunk<void, string>( 'mySlice/fetchItems', async (changedValue, thunkAPI) => { try { //Send data to backend await sendSomethingToBackend(changedValue); thunkAPI.dispatch(fetchItems()); } catch (e) { thunkAPI.rejectWithValue('failed'); } } );
Lastly when doing changes I often have to reload the data for latest updates. This time I believe I HAVE to use dispatch inside the thunk.
Yes, here you absolutely would need to dispatch the fetchItems
action to refetch data that sendSomethingToBackend
may've updated in the backend.
Or are there alternatives?
If you are fetching and updating data then an alternative for you may be to use Redux-Toolkit Query (RTK Query). You'd create an API slice that manages queries and mutations. Convert the fetchItems
Thunk to a query, and the editItem
Thunk to a mutation, and using cache tags the mutations can invalidate queried data and trigger queries to re-fetch automatically. In other words, further cutting down of code you write.