We have already explored Redux in the "Redux is easier than you think!" article and we learned have to use it, still there are some problems when it comes to using redux. those problems are: 1. redux configuration is too complicated 2. redux requires too much boilerplate code 3. it doesn't do anything helpful by itself and we have to install packages like redux-thunk, redux-saga,redux-persist, etc...
Now redux toolkit is here to solve most of our problems by giving us all we need from redux to manage our states and data.
redux-toolkit is a suger around redux that does the same thing redux does under the hood, but we don't need to go through all of those complicated configurations.
Note: if it is the first time that you want to learn redux and you choose redux-toolkit, I encourage you to check out the redux article first, so you would have some idea about what redux is and what it solves because we are not going to talk about those things here.
I wrote this article supposing you know nothing about redux but meanwhile, I put some "redux notes" for the people who are familiar with redux or have read the redux article before. So if you are not one of those who already knows about redux, you'd better not to read "redux notes" if you don't want to get confused.
npm install @reduxjs/toolkit react-redux
@reduxjs/toolkit is the all-in-one package that includes everything we need and we don't have to install redux-thunk or any other redux packages except react-redux. we use redux to create store and react-redux is just the way for react to communicate with redux, e.g: updating global state, reading global state,...
After creating the features folder, we create a folder for each one of our actions, in the above image which is the content of the features folder, we created a folder named allProduct that is related to products actions and a folder named cart which is related to actions like adding or removing the product from the cart or even getting added items in the cart...
Each folder contains two files: 1. action.js 2. xxxSlice.js ( you can put anything instead of xxx, but in our example it is cartSlice because it is in the cart folder).
> Redux note: imagine slice file as a reducer, just like reducer file that we used to name it xxxReducer, we name slice file like xxxSlice and we are not going to create reducer files anymore.
action.js contains all the actions that we need like adding product to cart, removing product from cart , etc... :
CreateAsyncThunk is a function from redux-toolkit that accepts a Redux action type string and a callback function to fetch the required data and return it. if our requests need anything like id or data,... we just pass them as parameters of our async callback function. as you can see in the above image when we need one parameter we can just pass it to our callback easily (like deleteFromCart action), but if we need more than one parameter, then we have to pass them as an object(like updateCart,addToCart actions) because the second argument of our async callback is not for parameters.
after fetching the data with Axios or fetch or anything else) we return the data and this async callback is going to dispatch an object with the property named payload and the value of this payload is the data that we received from the server.
> Redux note: you might have noticed with the createAsyncThunk method we don't need to dispatch an object with type and payload anymore and by passing a unique string as the first argument of this function we are actually sending the type and the async callback is going to handle payload itself.
Now, we need a slice to take care of these actions and after dispatching one of these actions, the slice is going to receive the type and payload and send the payload to the global state of our application.
So, we create cartSlice for above actions:
createSlice comes from redux-toolkit and it is a function that accepts an object of options:
name option for naming the slice,
initialSatate option for defining the initial state,
extraReducer option for defining different cases for our actions and state updates, extraReducer is a function that receives a builder and inside this function, we need to define our cases like above, addCase method of builder accepts two parameters: 1. the action(imported from action.js) with the status of the action like fulfilled, rejected and pending, 2. the callback that updates the state and this callback accepts two parameters first the initial state that we already declared and the action which has a payload property that is equal to what the action returns.
Note: you might have noticed the name that we gave to createSlice is the name that we put in the first part of the type string in the first parameter of createAsyncThunk, this way, we are actually saying that the slice with the name of 'cart' will look over this action that has the type of 'cart/addToCart' or 'cart/deleteFromCart',...
> Redux note: just like reducer that we had an initialState in it, in the createSlice we have the same thing.
In the above example if the getCart action has been dispatched and received the data successfully, the first case that we add inside extraReducers is going to get invoked and update the value property of our state with the data that comes from the action.
This is another slice example with more cases:
As you can see, we have rejected and pending status too, so we can add a case for those statuses and tell redux whenever the action was dispatched and it was still in pending status or rejected status, invoke this case and update the error or loading property of our state so we can use it in our page to show a loading spinner or an error alert,...
Note: in redux, we usually declare a specific state for loading and set its value to true before making a request and set it to false after the request. but with redux-toolkit, we don't need to do that anymore.
This way could be a little bit annoying for some people, so we don't have to do it this way with redux-toolkit and as you can see in the above examples inside cases, we are actually changing the property of our state object so easily.
redux uses immer.js under the hood to turn our cases just like the way before(creates a new object and copies the old one into it and updates the property that we want) so we don't have to deal with that annoying syntax anymore.
We have another option for createSlice which is reducers: this option is used for synchronous actions and we don't even need to define actions in a separate file, reducers is an object of our sync actions these actions accept two parameters, first the initial state and the second is an action that is the first parameter that we gave to our action when we dispatch it:
After declaring an action inside reducers, createSlice gives us the object of an actions that contains all the actions we defined inside the reducers option and just like the example above, we can destructure it and export it.
You also might have noticed in all slice examples we have the last line that exports xxxSlice.reducer. createSlice gives us a reducer to use in our global store to be able to access the states we declared in slice files anywhere we want.
configureStore accepts an object that has an option named reducer and the reducer option is an object that contains all the reducers that we have exported from our slices and whenever we dispatch an action the data is going to be saved here at the end.
> Redux note: by using configureStore we don't need to use combineReducers or even config reduxDevTool anymore, because configureStore does all of that for us.
The provider comes from react-redux and we use it by wrapping it around the App component and we pass the store that we created with configureStore to the provider to make our states available inside all the components
Well, if I want to be brief, we either want to dispatch an action to update the data or read the updated data from the global state.
- Dispatch an action to update a data:
In above example, we are dispatching the addToCart action and because it needs two parameters, we have to put them inside an object. we dispatch the data by using the useDispatch hook that comes from react-redux.
- Read the updated data inside global state:
In the above example, we are using useSelector hook that comes from react-redux and accepts a callback that has one parameter which is our global state (reducer option inside the condifgureStore object). whenever we dispatch an action like addToCart or getCart or deleteFromCart the cartSlice is going to watch out and if the case that we add inside extraReducer was available so it is going to update the initial state and pass it to the cart property inside the reducer option of the configureStore method.
If you are looking for a different redux-toolkit structure that helps you with data fetching and data caching,... you can forget about all the structure above and use redux-toolkit in the way that I am going to tell you now:
This folder contains one file which is xxxApi (you can put anything instead of xxx, in our case we are going to call it onlineShopApi.js)
This file looks like this:
createApi is a function that accepts an object of options:
reducerPath: The reducerPath is a unique key that your service will be mounted to in your store.
baseQuery: it can be equal to the fetchBaseQUery which is a very small wrapper around fetch that aims to simplify requests. it accepts an object and we can put properties like baseUrl to simplify our requests.
endpoints: it is equal to a function that receives the build parameter and returns an object of properties and each property is equal to builder.query or builder.mutation which is a function that receives an object:
If the endpoint we have is for getting some data with the method of GET, we need to use builder.query which receives an object that contains a function named query and returns an string of our endpoit.
If the endpoint we have is for updating some data with methods like POST, DELETE, PATCH, or PUT, we need to use builder.mutation which receives a function named query and returns an object with url(endpoint string), method(PUT,...), and body property (if we need it).
Any data or id or whatever we need for our request, we can receive it as a parameter of query function and use it (like getProductDetails query or addToCart mutation in the above example), and don't forget if it is more than one parameter you need to pass it as an object(like addToCart mutation).
Now, createApi gives us a hook for each query or mutation we declared inside endpoints and we can destructure these hooks and export them to use them in our components (just like the last line in the above example).
these hooks are named by createApi with this format:
for builder.query endpoints:
use + your given name + Query
like : useGetCartQuery , useGetProductDetailsQuery hooks in above example.
for builder.mutation endpoints:
use + your given name + Mutation
like : useDeleteFromCartMutation, useAddToCartMutation hooks in above example.
createApi also gives us a reducer to use inside the reducer option of configureStore object that we are going to use in the next step.
The configureStore options is a little bit different from what you saw in previous structure:
We use the reducerPath option of our api to name the reducer it returns inside global state.
configureStore sets some middlewares to our store by default(like thunk middleware to make redux able to use async actions) but when we use the rtk query structure we want to also add the middlewares our api returns to our store. So, just like in the above image, we declare a middleware option that is equal to a function that accepts a function named getDefaultMiddleware and we call this function to get an array of all the default middlewares and also concat the middleware that comes from our api to this array.
> Redux node: each middleware that we put inside middleware array in configreStore is going to be used with applyMiddleware that we had inside redux.
Note: the middlewares that come from our api are going to help us in caching, invalidation,... processes.
This step is the same step that we had in the previous structure.
We either want to request data and get something by query hooks or update data by deleting, editing, or creating with mutation hooks.
- Using query hooks to get data:
As you see in the above example, query hooks give us an object of fetch results like data, loading, error, etc...
and we don't have to deal with declaring loading or error in our states. as soon as the component mounts, the request is going to be made and next time the data that has been returned before, is going to be cached(it means we just have loading for the first time that the data is empty).
- Using mutation hooks to update data:
As you see in the above example, mutation hooks give us a tuple. The first item in the tuple is the "trigger" function and the second element contains a results object with data, isLoading, isError,... properties.
This was the second way and structure to use redux-toolkit, the main thing that you should consider is that either in the first structure or in the second one, each method has lots of options and configurations that I am not able to talk about all of them in this article.
I tried to give you all the important and main concepts that you should know about redux-toolkit and be able to start using it, but if you want to learn more about it you can check out redux toolkit documentation.
Goodbye and Good luck🤞