Today we will talk about a new and in my opinion a phenomenal functionality for working with Redux, namely - Redux Toolkit (I really hope you understand the concept of how Redux works before you start reading this article as otherwise, some terminology may not be clear for you).
Redux Toolkit is an updated vision of Redux developers on how to manipulate data globally and do it easily and effortlessly.
Let's take a step back and recall what problems the classical approach has 🤔
If don't go into detail and statistical comparison, Redux isn't liked by developers (especially beginners) because of its complexity.
First, you need to spend a lot of time understanding the ideology of Redux, then spend time creating basic things (actions, action creators, reducers and etc.). 🤯
But we are developers - we want to write code, not waste time on settings. Right? 👩💻
Below I will describe my vision of how you can work with Redux Toolkit and use it with TypeScript (since I adore TypeScript and sincerely recommend using it in your projects).
Installation ⌛
I want to start by using the standard create-react-app structure with TypeScript.
This can be done with the following commands:
# npm
npx create-react-app my-app --template typescript
# yarn
yarn create react-app my-app --template typescript
After that, let's add the toolkit module directly to our project:
# npm
npm install @reduxjs/toolkit
# yarn
yarn add @reduxjs/toolkit
And directly the react-redux module:
# npm
npm install react-redux
# yarn
yarn add react-redux
Deep dive into Redux Toolkit 🤿
The new and most important concept that we will immediately encounter will be - slice.
To begin within the src folder I will create one more folder with the name - slices.
Inside, I'll create a profile.ts file that will contain functionality that pertains to the user's profile (it's a simple division by type of work, nothing special).
(React is flexible in terms of architecture, so you can choose a different files location. My choice is based on the ability to conveniently use the absolute paths provided by default when using react with TypeScript. You just need to find tsconfig.json file, find "compilerOptions" object and add another field - "baseUrl": "src")
We go to the profile.ts profile.ts file. In order to create a slice directly, you need to import the function that is responsible for it, namely - createSlice. Together with it, we import the module - PayloadAction (about which I will tell later).
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
The createSlice function accepts an object with main fields:
- name - describe the type of actions inside (for example, actions on user data or actions on authentication data);
- the initial value for our redux state (any data type, mainly array, or object);
- reducers (an object that will contain methods that will implement changes in our redux state).
const profileSlice = createSlice({
name: 'profile',
initialState: initialState,
reducers: {},
});
Everything is clear with the name.
The next field is initialState. Let's create it above the implementation of the createSlice function (initialState can be made into a separate file depending on your wishes and the size of this very initialState).
It will look like a simple object with one field - email:
const initialState = {
email: '',
};
The last field - reducers. Inside we describe the methods that will implement the change of our redux state.
Let's implement the changeEmail method (which represents just action creator).
const profileSlice = createSlice({
name: 'profile',
initialState: initialState,
reducers: {
changeEmail: (state, { payload }: PayloadAction<TPayload>) => {
state.email = payload.email;
},
},
});
Now let's deal with everything that is described above and what is used here.
The first thing I want to note is that the changeEmail method takes two parameters (identity how a reducer does in a classic Redux structure).
The first parameter - state, which will contain the initial value and will change after applying some of our methods.
The second parameter is the object that represents the action (in the classic use of this object contains the type field and the payload field), but in our, we use only the payload, which is obtained by destructuring (ES6).
We add type for our payload using PayloadAction (which we imported).
PayloadAction takes the type created above implementation of function createSlice:
type TPayload = {
email: string;
};
Now let's look directly inside in our changeEmail method:
changeEmail: (state, { payload }: PayloadAction<TPayload>) => {
state.email = payload.email;
}
The first thing we can see is a somewhat unconventional approach to change the Redux state. We simply assign new values to state.email with the values we get with the payload.
And what about the rule of immutability, which we must always remember when we change the Redux state? (If you don't fully understand what I'm talking about, then I'll make a very quick explanation of what I mean).
The main concept of immutability when changing the Redux state is to always make a copy of the current Redux state (mostly using the spread operator with ES6 for objects and arrays) and then change the copy and return it as a new state of our Redux state. This is so that React can recognize the changes (since the references have changed) and redraw our component that uses data from the Redux state.
In this regard, the Redux Toolkit "under the hood" uses an additional module called immer. This module takes on the job of copying and granting just these rules of immutability. Therefore, we can not worry about it and change the fields directly.
Let's summarize what happened. We just took the old value from the Redux store and replaced it with a new one, which we passed to one of our components (will see later).
That's it, our first slice is ready, with which I congratulate you! 🎉.
After implementing createSlice, let's do the following:
export const profileReducer = profileSlice.reducer;
export const { changeEmail } = profileSlice.actions;
The first export is our reducer, which we will call profileReducer (we will need it soon).
The second export is just our created changeEmail method (our action creator).
All this is available to us by referring to our created slice - profileSlice.
Redux store configuration 🔧
Let's use our profileReducer (the first export from the profile.ts file) and work on a full-fledged redux connection.
In the src folder, create another folder - reducer. Inside create a file reducer.ts:
reducer.ts will look like this:
import { profileReducer } from "slices/profile";
const reducer = {
profileReducer,
// another reducers (if we have)
};
export default reducer;
We import our created reducer so that in the future we can add another and combine them together.
I will note one more interesting thing - the reducer object in which we can add other reducers is analogous to the use of the combineReducers function but without the additional import of this combineReducers function and the generally compact appearance of the whole combination structure.
Let's create another file - store.ts in the src folder:
import { configureStore } from "@reduxjs/toolkit";
import reducer from "./reducer/reducer";
export const store = configureStore({ reducer });
export type TStore = ReturnType<typeof store.getState>;
If we used the standard Redux approach, it would be an ideal place to add middleware (for example thunk) and connect redux-dev-tool. But we use Redux Toolkit, and here everything is much more interesting 🤩.
This is where the magic is hidden, which we don't see 🔮. In fact, the Redux-Toolkit already connected thunk and redux-dev-tool "under the hood" when we just used the configureStore function. Only 3 lines of code and what is great the result.
Additionally, on the last line, you can see the creating of the TStore type. It allows us to use this type in the case when we want to grab data from the Redux store (for example using selector hook - useSelector).
We go further and go to the last step of the redux connection - the connection in the index.tsx file:
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { store } from "store";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
This simply uses the Provider component, which should by default wrap our main internal component (App in our case) and use the store property with the value which has name store too (which we created and in the previous step and imported here).
We finished with the setup. Now let's use what we've been working on.
Using 🏄♂️
Go to the component where we want to use our previously created action creator (in my case it is the App.tsx file).
We import the useDispatch function (which we will need) and directly our action creator - changeEmail.
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { changeEmail } from "slices/profile";
const App = () => {
const dispatch = useDispatch();
const { email } = useSelector((state: TStore) => state.profileReducer);
const handleEmailChange = () => {
dispatch(changeEmail({ email: "newEmail@gmail.com" }));
};
return (
<div>
<button onClick={handleEmailChange}>
Change email
</button>
<h2>
Email: {email}
</h2>
</div>
);
}
export default App;
We added a button with an event - onClick which provides as add handleEmailChange function. Inside we call function dispatch with our action creator - changeEmail as a parameter. Inside changeEmail we see an object - it's actually our payload object.📦
Below the button, we have a header. Inside we use the variable - email. You've probably noticed that we got this variable from the Redux store using the previously mentioned hook - useSelector. useSelector accepts the function with the - state (to which we add the TStore type) parameter and returns the value we want to get.
After clicking the button we see a new result.
If we check Redux Dev-Tool (hope you have this useful extension) we can notice what everything is works and our Redux store is changed and now we have a new value for the field - email.
That's it, we have full functionality when working with Redux Toolkit.
We can create other methods, create other slices, perform asynchronous actions and use it all in our application.
Final code example you can check here.
Thank you very much for taking the time to read my article. I hope she helped you figure out the Redux Toolkit.
As always open to feedback ❤️
Top comments (4)
There is no need to type the store in every component. You can skip it like that:
If you think the toolkit is cool, you should give it a go with TS. You can start typing your slices dynamically!
this is also my prefered style of redux-store. scalable, clean, Nice!
Awesome article. Thanks!