DEV Community

Mohammad Baragbah
Mohammad Baragbah

Posted on

Simplify Your Redux States with Redux Toolkit

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
Enter fullscreen mode Exit fullscreen mode

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.

Image description

Now first thing we need to do is download react-redux and @reduxjs/toolkit

npm install react-redux @reduxjs/toolkit
Enter fullscreen mode Exit fullscreen mode

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>;

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

That's it. You can checkout the entire app in this repo

Top comments (0)