Hey everyone! I think I have found an interesting topic to write about today.
P.S.: I am not an expert on redux so if you feel like there is something that needs improvement, please feel free to provide your feedback...
I have recently used RTK Query and I was really amazed by it, that how cool redux has become now (I used redux for the first time in 2021, and then came redux-toolkit that I tried in 2022 and now 2023, nice consistency :P). So, in this blog, I will try to explain the journey of encapsulation, abstraction, and the increase in power of redux with time.
What is Redux?
Redux is a simple state management tool that is independent of any other library or framework. It doesn't matter whether you use react, vue, or angular. It can work with anything.
To use redux with different libraries or frameworks we use something called Redux UI binding libraries, they help in making redux flexible enough to get merged with your library/framework, and the one that is used for react is react-redux.
You can read more about redux on the official docs.
How Redux works?
Redux working can be thought of as a centralized system where there is only one manager and it handles the needs of every other component in the app.
Let's say there is no manager control over the apps, then what will happen? The components can ask for value from any other component. And as there is always a hierarchy being followed by the apps, so if you want to get something from the topmost component, that value must go through each and every component that's in the way of both the components. And Hence the code becomes messy and impossible to manage.
To prevent all this redux uses the centralized system, where there is one box that stores all the values in it and that box is independent of any kind of hierarchy and any component can call any value. And that value can be provided directly without being passed to any component.
So this was a very basic logical idea of how redux makes state management very easy.
Now let's see how it technically works:
There are 4 basic components in Redux:
State - The current state of components (the actual values).
Reducer - A function that updates the state (updates the values).
Action - The event that tells the dispatcher what to update in the state (when the user interacts with the state).
Dispatcher - The function that sends the new value to the reducer so it can update the state.
View - Receives the updated state.
Suppose there is a bouquet of roses, and you want that bouquet but you want that instead of all roses, there should be some lilies too. So you go to the shop owner and ask him to exchange some roses for lilies. He accepts your request and gets lilies and adds them to the bouquet and replaces some roses and you buy that and you are happy, the owner is happy, and everyone is happy.
Here, the bouquet is the State, and the current state of the bouquet is roses.
You are the Viewer, who wants to buy the bouquet
Your command to change some roses to lilies is the Action.
The shop owner listening to your command to perform the required operation of changing roses to lilies is the Dispatch function.
And the Reducer is the decision-making process of the shop owner on where to keep roses and where to put lilies.
The state gets updated on whatever action is performed by the dispatch function. That action is then taken into consideration by the reducer function and it looks for the value to update. And then the value is updated and saved in the state.
So this is how the reducer actually works... To read more about it you can refer to the docs.
React-Redux
Ahhh! Finally, the main content that I wanted to talk about. What has changed in going from react-redux to RTK Query?
You see, react-redux is just a UI binding of redux to react so it's just the same as redux, nothing different.
We create a store that saves all our values.
We create a reducer function that updates the state according to the condition specified.
We link that store to our app.
Whenever we want a value, we call the useSelector hook, which calls the value from the store.
Whenever we want to update the value, we call the useDispatch hook, which sends a message to the reducer and it updates the state.
That's it, that's what react-redux does.
Creating a Reducer
// Define an initial state value for the app
const initialState = {
value: 0,
//...other values
}
// Create a "reducer" function that determines what the new state
// should be when something happens in the app
function rootReducer(state = initialState, action) {
// Reducers usually look at the type of action that happened
// to decide how to update the state
switch (action.type) {
case 'root/increment':
return { ...state, value: state.value + 1 }
case 'root/decrement':
return { ...state, value: state.value - 1 }
default:
// If the reducer doesn't care about this action type,
// return the existing state unchanged
return state
}
}
Creating and adding a Store
const store = createStore(rootReducer)
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
Getting the value from Store and Setting it using dispatch
import { useSelector, useDispatch } from "react-redux";
function App() {
//Get value from state
const counter = useSelector((state) => state.value);
//Set value in state
const dispatch = useDispatch();
return (
<div className="App">
<h3>Counter</h3>
<h3>{counter}</h3>
<button onClick={() => dispatch({type: "root/increment"})}>Increase</button>
<button onClick={() => dispatch({type: "root/decrement"})}>Decrease</button>
</div>
);
}
export default App;
To see how redux did data fetching, you can refer to this link
But the issue with react-redux was, its time consuming to implement the code. Managing state, creating, and managing reducers and actions in Redux applications was a little tougher. And moreover, it only provided the state management functionality, if you wanted to do caching and data fetching, it would be a lot of code to write and manage again.
To overcome all these things situations, some abstractions were made. The code was made shorter and easier to use and that's how Redux Toolkit was born.
Redux toolkit
Redux toolkit made the redux code really shorter, I myself felt so at ease when I tried redux toolkit for the first time. But there was one thing that I found somewhat tricky.
Due to this abstraction, understanding the logic behind the code of redux toolkit became tougher. Why?
One example is the below code:
You don't need to understand this code for now, so chill.
Initially, I never understood where actually these values are coming from. It took me days to figure out that the function createSlice actually returns an object or we can say it's returning the data in a different format to make it cleaner and easier to use.
Redux Toolkit provides a simplified approach to using Redux, which can make it easier for developers to get started with the library. But still, I feel that having knowledge of redux is also good, so if you ever get time, try to read about react-redux too.
In Redux Toolkit, the process is almost the same as React-Redux but the only difference is, it's neat and easier to write the Redux code now.
RTK (Redux Toolkit) uses the createReducer function, which makes it easier to create and manage reducers and actions in Redux. These functions provide a simplified approach to creating reducers and actions, which can help reduce the amount of boilerplate code that developers need to write.
Creating a Slice ( a slice is a reducer only but short and easier to manage )
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
value: 0,
}
// Creating the Slice (reducer) using createSlice function
// Note that this time we didnt called switch case or checked for any conditions,
// instead we are directly creating the functions to update the value.
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => { // reducer functions that will update the state
state.value += 1
},
decrement: (state) => { // reducer functions that will update the state
state.value -= 1
}
},
})
// Action creators are generated for each case reducer function
export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer // Default export is used here that means it can be imported as any name you want in other files
Creating a Store and adding our Slice to it.
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export const store = configureStore({ //configureStore creates the store
reducer: { // reducer is the object that collects all the reducers/slices at one place
counter: counterReducer, // Our counter Slice that was default exported and hence called here with any name you want in this case counterReducer
},
})
Integrating store to our app.
import { store } from './app/store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Using the values and updating them using useSelector and useDispatch hooks
import { useSelector, useDispatch } from 'react-redux'
// calling our reducer functions that updates the values
import { decrement, increment } from './counterSlice'
export function Counter() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<button onClick={() => dispatch(increment())}>
Increment
</button>
<span>{count}</span>
<button onClick={() => dispatch(decrement())}>
Decrement
</button>
</div>
)
}
And that's it! Our reducer is all setup! The best part about RTK is it provides really good code splitting.
You can create a new reducer for every new activity in a new file and then combine it in the store. There is no switch case so writing redux types everywhere is also gone. You don't need to make a separate file for types too.
RTK also includes support for middleware, including the Redux Thunk middleware, which allows developers to write asynchronous actions like promises.
// CreateAsyncThunk is the function that is useful in doing async calls.
// This slice shows you all the post fetched from the database, to the user.
export const getSinglePost = createAsyncThunk(
"post/getSinglePost",
async (pid, { rejectWithValue }) => {
try {
const { data, status } = await getSinglePostService(pid);
return data.post;
} catch {
return rejectWithValue([], "Error occured. Try again later.");
}
}
);
export const postSlice = createSlice({
name: "post",
initialState,
extraReducers: {
//setting what action to take on the particular States of Promise:
[getPosts.pending]: (state) => { // Pending State
state.isLoading = true;
},
[getPosts.fulfilled]: (state, { payload }) => { // Success State
state.isLoading = false;
state.posts = payload;
},
[getPosts.rejected]: (state, { payload }) => { // Rejected State
state.isLoading = false;
state.error = payload;
}
}
})
export default postSlice.reducer;
So now, RTK was perfect, it provided shorter and more manageable code with great code-splitting abilities, but still, there was something missing: The caching part, and somewhere the data fetching part is still a problem, it's too large.
Just think of the above code and you have to add 6 more features like this in this same slice, see this extraReducers object will go crazy.
So now finally, it's time for RTK Queryyyyy!
Developers still find it difficult to manage data fetching and caching with Redux Toolkit due to the lack of inbuilt support.
What is RTK Query?
RTK Query is a powerful server data caching solution explicitly built for Redux Toolkit. It's built on top of RTK and follows the same architecture as Redux.
The inspiration for RTK Query came from other data-fetching libraries like React Query, Apollo, etc.
I don't really need to explain much about RTK Query here, as it's all well-written in the docs itself. Read the Motivation section in the docs where they explain exactly why RTK Query was needed.
In short, RTK Query was introduced to eliminate the need to hand-write data fetching & caching logic yourself. Redux, as I wrote above, does very minimal work itself and if you want to write some code like caching, you have to build that from scratch, but RTK Query gives you a predefined function. And it can be used with redux and RTK.
So, how do you use RTK Query?
There are just 4 things that you need to know to use RTK Query:
createApi(): It setups the base of the code, and defines a set of endpoints. You can alter the data, you can fetch the data, you can cache it, re-cache new data.
fetchBaseQuery(): A small wrapper around the fetch function that aims to simplify requests.
Provider: The container tag that takes in the complete app
Query/Mutation: These are the two keywords that does the HTTP methods work for you. In simple terms, if you want to get the data you use query, and if you want to alter the data you use mutation. You can read about them in detail from the docs.
Create an API service.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const pokemonApi = createApi({
reducerPath: 'pokemonApi', //name of the reducer
// baseQuery sets the base url, you just need to specify the paths and baseQuery will attach this path before your path.
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({ // builder function does the thing, it comes from RTK WueryQand helps you create your apiCall
// Below here we have specified the name of our api calling function.
// it will be called whenever you need some data from the api specified
// in the query section
getPokemonByName: builder.query({ // the keyword query defines fetching and caching data from the server
query: (name) => `pokemon/${name}`, // the api you want to hit
}),
}),
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints,
// if it is a query then we use Query at the end and if its mutation we use Mutation like useGetPokemonByNameMutation
export const { useGetPokemonByNameQuery } = pokemonApi
Add this to your store
import { configureStore } from '@reduxjs/toolkit'
import { pokemonApi } from './services/pokemon'
export const store = configureStore({
reducer: {
// reducerPath is the name we added in the createApi
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
})
Integrate it into your app
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'
const rootElement = document.getElementById('root')
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
Using the values in your App
import { useGetPokemonByNameQuery } from './services/pokemon'
export default function App() {
// Our Query hook provided everything to us: data, error, state all that we had to manage through asyncThunk in RTK
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
return (
<div className="App">
{error ? (
<>Oh no, there was an error</>
) : isLoading ? (
<>Loading...</>
) : data ? (
<>
<h3>{data.species.name}</h3>
</>
) : null}
</div>
)
}
And done! No async/await no try/catch, you just had to import a hook, and that's it, it works! That's how simple RTK Query is.
We have come so far in abstraction, and encapsulation and managed to make it work effortlessly with so few lines of code and with such neatness.
Thank you for reading this blog thus far! I hope I was able to tell you how cool and great Redux has become.
References:
https://redux-toolkit.js.org/tutorials/rtk-query/
https://redux-toolkit.js.org/rtk-query/overview
https://www.freecodecamp.org/news/learn-redux-by-making-a-counter-application/