This file shows the same “list + add item” flow implemented two ways: traditional Redux with manual thunks/reducers, and RTK Query with generated hooks. Use this to explain why RTK Query reduces boilerplate and adds caching.
// store/traditional/posts.ts
import {createSlice, createAsyncThunk} from '@reduxjs/toolkit';
import axios from 'axios';
export const fetchPosts = createAsyncThunk('posts/fetch', async () => {
const {data} = await axios.get('https://jsonplaceholder.typicode.com/posts');
return data;
});
export const addPost = createAsyncThunk('posts/add', async (body) => {
const {data} = await axios.post('https://jsonplaceholder.typicode.com/posts', body);
return data;
});
const postsSlice = createSlice({
name: 'posts',
initialState: {items: [], loading: false, error: null as string | null},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message ?? 'Error';
})
.addCase(addPost.fulfilled, (state, action) => {
// manual list update
state.items.unshift(action.payload);
});
},
});
export default postsSlice.reducer;// components/PostsTraditional.tsx
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {addPost, fetchPosts} from '../store/traditional/posts';
import {RootState} from '../store';
export default function PostsTraditional() {
const dispatch = useDispatch();
const {items, loading, error} = useSelector((s: RootState) => s.posts);
const [title, setTitle] = useState('');
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
const handleAdd = () => dispatch(addPost({title, body: 'demo', userId: 1}));
if (loading) return <Text>Loading…</Text>;
if (error) return <Text>{error}</Text>;
return (
<>
<Button title="Add" onPress={handleAdd} />
{items.map((p) => (
<Text key={p.id}>{p.title}</Text>
))}
</>
);
}Characteristics
- Boilerplate: multiple thunks, reducers, action types, selectors.
- Caching/deduping: none by default.
- Refetch after mutation: must be wired manually (e.g., dispatch fetch again).
- Loading/error flags: hand-rolled.
// services/api.ts (excerpt)
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({baseUrl: 'https://jsonplaceholder.typicode.com'}),
tagTypes: ['Posts'],
endpoints: (builder) => ({
getPosts: builder.query<any[], void>({
query: () => '/posts',
providesTags: ['Posts'],
}),
addPost: builder.mutation<any, Partial<any>>({
query: (body) => ({url: '/posts', method: 'POST', body}),
invalidatesTags: ['Posts'], // triggers refetch of getPosts
}),
}),
});
export const {useGetPostsQuery, useAddPostMutation} = api;// components/PostsRTKQuery.tsx
import React, {useState} from 'react';
import {useGetPostsQuery, useAddPostMutation, api} from '../services/api';
import {useDispatch} from 'react-redux';
export default function PostsRTKQuery() {
const dispatch = useDispatch();
const {data: posts, isLoading, isError, refetch} = useGetPostsQuery();
const [addPost, {isLoading: isAdding}] = useAddPostMutation();
const [title, setTitle] = useState('');
const handleAdd = async () => {
const res = await addPost({title, body: 'demo', userId: 1}).unwrap();
// optimistic insert (useful for mock APIs that don’t persist)
dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
draft.unshift({id: res?.id ?? Math.random().toString(), title, body: 'demo', userId: 1});
}),
);
};
if (isLoading) return <Text>Loading…</Text>;
if (isError) return <Text>Error</Text>;
return (
<>
<Button title={isAdding ? 'Adding…' : 'Add'} onPress={handleAdd} disabled={isAdding} />
<Button title="Refetch" onPress={refetch} />
{posts?.map((p) => (
<Text key={p.id}>{p.title}</Text>
))}
</>
);
}Characteristics
- Minimal boilerplate: endpoints + generated hooks.
- Built-in cache/deduping: no duplicate requests for the same query args.
- Auto-refetch:
invalidatesTagsrefreshes lists after mutations. - Loading/error: provided flags (
isLoading,isError,isFetching,isSuccess). - Extras out-of-the-box: polling, skip/conditional queries, prefetch, optimistic updates.
- Less code, fewer bugs: No hand-written request/success/failure reducers.
- Consistency: One API layer defines base URL, headers, retries, and tags.
- Performance: Cached responses and deduped requests reduce network churn.
- DX: Generated hooks with types; Redux DevTools support.