DAY 58: Mastering Redux Toolkit: Build a Weather App & CRUD Todo App with RTK Query | ReactJS

👨💻 Aspiring Software Developer | MERN Stack Developer.🚀 Documenting my journey in Full-Stack Development & DSA with Java.📘 Focused on writing clean code, building real-world projects, and continuous learning.
🚀 Introduction
Welcome to Day 58 of my Web Development Journey!
After building a solid foundation in HTML, CSS, and JavaScript, I transitioned to ReactJS — a powerful library for building dynamic and interactive user interfaces.
So far, I’ve explored different state management techniques in React, including useState, Context API, useReducer, and even external libraries like Zustand. More recently, I dove deeper into Redux, one of the most popular and robust solutions for managing state in modern web applications.
Over the past few days, after strengthening my understanding of the core concepts of Redux Toolkit (RTK) and RTK Query, I revised all the key concepts and built two projects to put my learning into practice:
- 🌦️ A Weather App to practice data fetching
- 📝 A CRUD Todo App to apply concepts like mutations, caching, and optimistic updates
📂 You can check out my complete Redux learning journey in my GitHub repository.
👉 I also share real-time updates and coding insights on Twitter.
I’m documenting this journey publicly to stay consistent and to share what I’ve learned. Whether you’re just starting with React or looking to strengthen your knowledge of state management and data fetching, I hope this blog adds value to your learning journey!
📅 Here’s What I Covered Over the Last 3 Days
Day 55
- Revision of Redux Toolkit & RTK Query
Day 56
- Built a 🌦️ Weather App using Redux Thunk & createAsyncThunk
Day 57
- Built a 📝 CRUD Todo App using RTK Query
Now, let’s break down each of these concepts with explanations and implementations 👇
1. Weather App with Redux Toolkit & Async Thunks
This Weather App allows users to search for any city and instantly fetch its temperature, humidity, wind speed, and weather conditions using the OpenWeather API.
The project is built using:
- ReactJS (for UI)
- Redux Toolkit (for centralized state management)
- createAsyncThunk (for handling async API requests)
The real highlight here is how we handle loading states, error states, and caching weather data using Redux Toolkit slice.
Redux Toolkit Setup
This is how Redux Toolkit is set up for managing state in the Weather App:
1. Store Configuration
We use configureStore from Redux Toolkit to register our weather slice:
// store/store.js
import { configureStore } from "@reduxjs/toolkit";
import weatherReducer from "./slices/weatherSlice.js";
export const store = configureStore({
reducer: {
weather: weatherReducer,
},
});
This ensures that the entire app can access the weather slice when wrapped in a <Provider> inside main.jsx.
2. Weather Slice with createAsyncThunk
The core logic for fetching weather data lives in the weatherSlice.
export const fetchWeatherData = createAsyncThunk(
"weather/fetchData",
async (city, { rejectWithValue }) => {
try {
const res = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${
import.meta.env.VITE_WEATHER_TOKEN
}&units=metric`
);
const data = await res.json();
if (!res.ok || data.cod === "404") {
return rejectWithValue(data.message);
}
return data;
} catch (err) {
return rejectWithValue("Network Error");
}
}
);
Here’s what happens:
createAsyncThunkautomatically generates pending, fulfilled, and rejected actions.- On success, it stores the weather data.
- On failure, it stores the error message (e.g., invalid city).
3. Slice Reducer
The slice defines how the state changes in response to the actions generated by createAsyncThunk
const weatherSlice = createSlice({
name: "weather",
initialState: {
data: null,
loading: false,
error: "",
},
extraReducers: (builder) => {
builder
.addCase(fetchWeatherData.pending, (state) => {
state.loading = true;
state.error = "";
state.data = null;
})
.addCase(fetchWeatherData.fulfilled, (state, action) => {
state.loading = false;
state.error = "";
state.data = action.payload;
})
.addCase(fetchWeatherData.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || "Something Went Wrong";
});
},
});
Benefits of this approach:
- No manual action types needed.
- Loading, success, and error states are automatically managed.
- Cleaner and predictable state transitions.
State Management Flow
Here’s how the Redux state flows in our app:
- User enters a city name → triggers
dispatch(fetchWeatherData(city)). - Redux Toolkit handles async flow:
pending→ setsloading = true.fulfilled→ saves API response instate.data.rejected→ saves error message instate.error.
- Components consume state using
useSelector.
UI Integration
This is how Redux state integrates with UI components in the app:
1. App.jsx
- Maintains local state for
inputCity. - Dispatches
fetchWeatherDatawhenever a new city is searched.
const App = () => {
const [inputCity, setInputCity] = useState("");
const [city, setCity] = useState("");
const dispatch = useDispatch();
useEffect(() => {
if (!city) return;
dispatch(fetchWeatherData(city));
}, [city]);
function handleSearch() {
const trimmedCity = inputCity.trim();
if (!trimmedCity) return;
setCity(trimmedCity);
setInputCity("");
}
return (
<>
<SearchInput
inputCity={inputCity}
onChange={(e) => setInputCity(e.target.value)}
handleSearch={handleSearch}
/>
<ContentContainer />
</>
);
};
export default App;
2. ContentContainer.jsx
Handles conditional rendering:
Loading...when API call is pending.Homewhen nothing is searched.Errorwhen API request fails.Weatherwhen data is successfully fetched.
const ContentContainer = () => {
const weatherData = useSelector(selectWeatherData);
const loading = useSelector(selectWeatherLoading);
const error = useSelector(selectWeatherError);
return (
<div className="card-details">
{loading && <p className="loading-text">Loading...</p>}
{!loading && !weatherData && !error && <Home />}
{!loading && error && <Error />}
{!loading && weatherData && <Weather />}
</div>
);
};
export default ContentContainer;
3. Weather.jsx
- Displays city name, temperature, weather icon, humidity, and wind speed.
- Uses helper functions for date formatting and weather status icons.
const weatherData = useSelector(selectWeatherData);
This line selects the weatherData from the Redux store, and based on this data, the UI updates dynamically to show the current weather details.
Key Takeaways
- Redux Toolkit + createAsyncThunk made state management clean and predictable.
- We avoided multiple
useStatehooks in each component by keeping data centralized in Redux. - Error handling and loading states were automatically managed inside the slice.
- Scaling the app (adding forecast, multiple cities, etc.) becomes easier since state logic is separated from UI.
2. CRUD Todo App with Redux Toolkit & RTK Query
This project is a full-featured Todo Application where users can:
- Add new tasks
- Update task completion status
- Delete tasks
- Fetch tasks from a backend
For the backend, we simulate APIs using json-server with a db.json file.
The main highlight of this project is how Redux Toolkit Query (RTK Query) simplifies:
- Data fetching (
getTasks) - Mutations (
addTask,updateTask,deleteTask) - Cache management (via
providesTags&invalidatesTags) - Optimistic updates (instant UI updates without waiting for server response)
Redux Toolkit Setup
This is how Redux Toolkit is setup in this project:
1. Store Configuration
We register the todosApi slice reducer and middleware inside the store:
// store/store.js
import { configureStore } from "@reduxjs/toolkit";
import { todosApi } from "./slices/todosApi";
export const store = configureStore({
reducer: {
[todosApi.reducerPath]: todosApi.reducer,
},
middleware: (getDefaultMiddleware) => [
...getDefaultMiddleware(),
todosApi.middleware,
],
});
This ensures RTK Query API slice (todosApi) is available throughout the app and handles auto-caching, re-fetching, and request lifecycle.
RTK Query API Slice
The heart of the application is the todosApi slice, created using createApi.
import { nanoid } from "@reduxjs/toolkit";
import { fetchBaseQuery } from "@reduxjs/toolkit/query";
import { createApi } from "@reduxjs/toolkit/query/react";
export const todosApi = createApi({
reducerPath: "todosApi",
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3000" }),
endpoints: (builder) => ({
getTasks: builder.query({
query: () => "/tasks",
transformResponse: (tasks) => tasks.reverse(),
providesTags: ["Tasks"],
}),
addTask: builder.mutation({
query: (task) => ({
url: "/tasks",
method: "POST",
body: task,
}),
invalidatesTags: ["Tasks"],
async onQueryStarted(task, { dispatch, queryFulfilled }) {
const patch = dispatch(
todosApi.util.updateQueryData("getTasks", undefined, (draft) => {
draft.unshift({ ...task, id: nanoid() });
})
);
try {
await queryFulfilled;
} catch {
patch.undo();
}
},
}),
updateTask: builder.mutation({
query: (task) => ({
url: `/tasks/${task.id}`,
method: "PATCH",
body: task,
}),
invalidatesTags: ["Tasks"],
async onQueryStarted(task, { dispatch, queryFulfilled }) {
const patch = dispatch(
todosApi.util.updateQueryData("getTasks", undefined, (draft) => {
const taskIndex = draft.findIndex((t) => t.id === task.id);
if (taskIndex !== -1) {
draft[taskIndex] = { ...draft[taskIndex], ...task };
}
})
);
try {
await queryFulfilled;
} catch {
patch.undo();
}
},
}),
deleteTask: builder.mutation({
query: (id) => ({
url: `/tasks/${id}`,
method: "DELETE",
}),
invalidatesTags: ["Tasks"],
async onQueryStarted(id, { dispatch, queryFulfilled }) {
const patch = dispatch(
todosApi.util.updateQueryData("getTasks", undefined, (draft) => {
const taskIndex = draft.findIndex((t) => t.id === id);
if (taskIndex !== -1) {
draft.splice(taskIndex, 1);
}
})
);
try {
await queryFulfilled;
} catch {
patch.undo();
}
},
}),
}),
});
export const {
useGetTasksQuery,
useAddTaskMutation,
useUpdateTaskMutation,
useDeleteTaskMutation,
} = todosApi;
Key Features of this API Slice:
getTasks→ Fetches tasks from backend (json-server).addTask→ Adds new task (optimistic update ensures instant UI update).updateTask→ Updates task’s completed status.deleteTask→ Deletes a task (optimistic update removes it immediately).- Optimistic Updates → If API fails,
patch.undo()rolls back the changes. - Tags (
providesTags,invalidatesTags) → Ensures auto-refetching when data changes.
State Management Flow
Here’s how the flow works with RTK Query:
- Component calls hook → e.g.,
useGetTasksQuery()oruseAddTaskMutation(). - RTK Query handles the request lifecycle:
isLoading,isError,errorstates- Caching & re-fetching
- On mutation (
addTask,updateTask,deleteTask) → invalidates cache & re-fetches latest tasks. - Optimistic updates ensure UI stays snappy & responsive.
UI Integration
Now let’s see how this data and mutations integrate with the UI:
1. App.jsx
- Manages input for new tasks.
- Fetches all tasks using
useGetTasksQuery. - Handles loading & error states.
- Uses mutation hooks (
useAddTaskMutation,useUpdateTaskMutation,useDeleteTaskMutation) to modify tasks.
const { data: allTasks, isError, isLoading, error } = useGetTasksQuery();
const [addTask] = useAddTaskMutation();
const [updateTask] = useUpdateTaskMutation();
const [deleteTask] = useDeleteTaskMutation();
<form
onSubmit={(e) => {
e.preventDefault();
const task = {
value: newTask,
completed: false,
};
addTask(task);
setNewTask("");
}}
This eliminates the need for manual reducers, useState hooks, or async thunks.
2. TaskItem.jsx
Each task item supports completion toggle and deletion.
<input
type="checkbox"
checked={completed}
onChange={() => {
updateTask({ id, value, completed: !completed });
}}
/>
<svg
onClick={(e) => {
e.preventDefault();
deleteTask(id);
}}
/>
updateTask→ Toggles completion status.deleteTask→ Removes task instantly via optimistic update.
Key Takeaways
- RTK Query drastically reduces boilerplate compared to manual async logic.
- Auto caching + invalidation → UI always shows latest data.
- Optimistic updates → Instant feedback, rollback on failure.
- Cleaner code → No manual
extraReducers, no action types, nouseEffectfor API calls. - Easy to scale → Adding more endpoints (e.g., search tasks, filter completed tasks) is straightforward.
3. What’s Next
I’m excited to keep growing and sharing along the way! Here’s what’s coming up:
- Posting new blog updates every 3 days to share what I’m learning and building.
- Diving deeper into Data Structures & Algorithms with Java — check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
- Sharing regular progress and insights on X (Twitter) — feel free to follow me there and join the conversation!
Thanks for being part of this journey!
4. Conclusion
Building these two projects — a Weather App with createAsyncThunk and a CRUD Todo App with RTK Query — shows the real power of Redux Toolkit in simplifying state management.
- In the Weather App, we learned how
createAsyncThunkhelps manage async API calls with minimal boilerplate, handlingloading,success, anderrorstates automatically. - In the Todo App, RTK Query took things even further by removing the need for manual thunks, reducers, and side effects, while also giving us caching, invalidation, and optimistic updates out of the box.
The key takeaway is:
👉 Redux Toolkit + RTK Query = predictable, scalable, and developer-friendly state management.
Whether you’re building a small app (like fetching weather data) or a more complex app (like full CRUD with backend integration), Redux Toolkit ensures your state logic is cleanly separated from UI, making your application more maintainable and easier to scale in the long run.
Thanks for reading — and if you’re also learning Redux Toolkit, I hope this blog helps you speed up your journey



