We already know what redux is and what problems it solves. But redux has its own problems like lots of configurations, need to create reducers and action creators separately, etc...
Now with redux toolkit we don't have that problem anymore. It simplifies our code, provides a standard structure, and comes with powerful tools like createSlice and createAsyncThunk, that can help you create and manage slices of state.
A simple app to play around with redux toolkit
Before we start you can clone this repo:
git clone https://github.com/duniandewon/robofriends-pwa.git
I know it has pwa in it, but we are not making progressive web apps now. Maybe some other time.
This is a simple app where we fetch some users data from jsonplaceholder and filter them by their name.
Now first thing we need to do is download react-redux and @reduxjs/toolkit
npm install react-redux @reduxjs/toolkit
Remember how many packages we had to install before? Now we only need these two...
Create a file and name it store.ts which will contain:
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {}
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
To create a store we use configureStore function from redux toolkit and all future reducers will go into the reducer object.
The last two variables we export are typescript stuff for type safety.
Now we go to app.tsx and wrap our app with redux provider
import { Provider } from "react-redux";
import { store } from "@/redux/store";
// other imports...
function App() {
// other stuff may going on here...
return (
<Provider store={store}>
// other components...
</Provider>
);
}
export default App;
Now for our reducers create a file and name it robotsSlice.ts
We are not slicing robots here, we are creating a slice of our redux state that looks like this:
import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { RootState } from "./store";
interface InitialState {
robots: IRobot[];
filteredRobots: IRobot[];
status: "idle" | "loading" | "failed";
}
const initialState: InitialState = {
robots: [],
filteredRobots: [],
status: "idle",
};
export const fetchRobots = createAsyncThunk("robots/fetch", async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const robots = await res.json();
return robots;
});
const robotsSlice = createSlice({
name: "robots",
initialState,
reducers: {
filterRobots: (state, action: PayloadAction<string>) => {
// filtering robots...
},
extraReducers: (builder) => {
builder
.addCase(fetchRobots.pending, (state) => {
state.status = "loading";
})
.addCase(fetchRobots.fulfilled, (state, action) => {
state.status = "idle";
state.robots = action.payload;
})
.addCase(fetchRobots.rejected, (state) => {
state.status = "failed";
});
},
}
});
export const selectRobots = (state: RootState) => state.robots.robots;
export const selectFilteredRobots = (state: RootState) =>
state.robots.filteredRobots;
export const { filterRobots } = robotsSlice.actions;
export default robotsSlice.reducer;
This is our whole action types, action creators, reducers. Much cleaner right?
To create our state we mainly use createSlice function. It has three required properties: name, initialState, and reducers.
Name and initialState properties are self-explanatory. The reducers property is where our synchronous action creators go.
To create an asynchronous action we use createAsyncThunk that takes a string and a callback function where we can do all sort of async stuff. But to let redux know about our async action we need to configure it in extraReducers.
Now let's go back to out store.ts and put our newly created reducer in there
// other imports...
import robotsReducer from "@/redux/robotsSlice";
export const store = configureStore({
reducer: {
robots: robotsReducer,
},
});
// other stuff
By the way... I created custom hook for useDispatch and useSelector to have more type safety:
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "@/redux/store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Now we can use our actions in peace
RobotsList.tsx:
import { useCallback, useEffect } from "react";
import RobotItem from "./RobotItem";
import { useAppDispatch, useAppSelector } from "@/hooks/useDispatch";
import {
fetchRobots,
selectFilteredRobots,
selectRobots,
} from "@/redux/robotsSlice";
function RobotsList() {
const dispatch = useAppDispatch();
const robots = useAppSelector(selectRobots);
const filteredRobots = useAppSelector(selectFilteredRobots);
const renderRobots = useCallback(() => {
const data = filteredRobots.length ? filteredRobots : robots;
return data.map((robot) => <RobotItem robot={robot} key={robot.id} />);
}, [filteredRobots, robots]);
useEffect(() => {
dispatch(fetchRobots());
}, [dispatch]);
return (
<ul className="flex flex-wrap gap-4 px-3 justify-center">
{renderRobots()}
</ul>
);
}
SearchBox.tsx:
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
import { filterRobots } from "@/redux/robotsSlice";
import { useAppDispatch } from "@/hooks/useDispatch";
function SearchBox() {
const [search, setSearch] = useState("");
const ref = useRef<HTMLInputElement>(null);
const dispatch = useAppDispatch();
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
dispatch(filterRobots(e.target.value));
},
[dispatch]
);
useEffect(() => {
ref.current?.focus();
}, []);
return (
<div className="max-w-lg mx-auto mt-6 rounded-lg">
<label htmlFor="search" className="sr-only">
Search robots
</label>
<input
type="text"
id="search"
className="outline-slate-500 border-2 border-slate-400 py-4 pl-4 w-full rounded-lg"
placeholder="Search robots"
ref={ref}
value={search}
onChange={handleChange}
/>
</div>
);
}
export default SearchBox;
That's it. You can checkout the entire app in this repo
Top comments (0)