RTK Query vs Redux Saga: What to choose for your next project?
Tags: react, redux, webdev, tutorial
Published: 2022-11-03 20:14:52

Today we're going to discuss and compare **Redux Saga** and **RTK Query** approaches for API access and async tasks. We will go through both of them and in the end, you will have all the knowledge that will help you decide which one to use in your project!

So your application is getting bigger, and you decide to use Redux as your state management for the project.

Now, you want to connect your app with an external API! 

**In Redux** you can't dispatch asynchronous actions directly in the store, so you will typically need to install an async middleware such as _Redux Thunk_ or _Redux Saga_, that will help encapsulate asynchronous side effects.

**RTK** provides built-in tools that can be used for API interactions such as **createReduxThunk** and **RTK Query** which is indeed built on top of cAT and other RTK APIs internally. It is purpose built for data fetching so it simplifies the async logic that is usually handwritten.

In this tutorial, I assume you have basic knowledge of React and Redux.

I'm gonna show you how to integrate **Saga** into **plain Redux** and **RTK**, explore some of its powerful features, and then dive into **RTK Query** and how it cuts down a lot of data fetching and caching logic.

## Redux Saga

**Redux-Saga** is a powerful middleware library that allows Redux store to asynchronously interact with external APIs.

Redux-Saga uses an advanced ES6 feature called **generator**, it's simply a kind of function that can be paused in between the execution, similar to what happens when using _async/await_, which makes life easier while managing asynchronous calls.

_Now let's do some coding and start building our app!_

We will be setting up a simple API call that gets a list of random users - I will start with integrating Saga using **plain Redux** then try it out with **RTK** and see the difference between both integrations.


**Plain Redux:**

**Step 1** - Initialize a React application

We will be using CRA to bootstrap our React app.
Run this command in the terminal:

`npx create-react-app users-list && cd users-list`

**Step 2** - Setting up Redux

To set up Redux and integrate Saga we will need to install some packages:

`npm install redux react-redux redux-saga --save`

_Let's now create our store_

- Create a new directory in the src folder and name it **store**.
- Inside of it create **configureStore.js** 
- Copy the following code into the file we just created.

```
import { createStore, combineReducers } from 'redux';
import users from '../reducers/reducer';

export default function configureStore() {
    const reducers = combineReducers({ users });

    return {
        ...createStore(reducers)
    }
}
```

Now we set up the actions and root reducer

- Create an **actions** directory in **src**
- Create **action.js** file
- Copy following code into it

```
export const GET_USERS_FETCH = 'GET_USERS_FETCH';
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS';

export const getUsersFetch = () => ({
    type: GET_USERS_FETCH
})
```

- Create a **reducers** directory in src
- Create **reducer.js**
- Copy the following code into it.

```
import { GET_USERS_SUCCESS } from "../actions/action";

const users = (state = { users: [] }, action) => {
    switch (action.type){
        case GET_USERS_SUCCESS:
            return { ...state, users: action.users }
        default: 
            return state;
    }
}

export default users;
```

Now we update **index.js** file in the root directory


```
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import configureStore from './store/configureStore';

const store = configureStore();

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
```

**Step 3** - Integrate our first Saga

- Create **sagas** folder in the root directory
- Create **saga.js**

As we mentioned earlier sagas are implemented using **generators**.
Let's create the first **generator** function.

Copy the code below into the **saga** file we just created

```
import { takeEvery } from "redux-saga/effects";
import { GET_USERS_FETCH } from "../actions/action";


function* getUsersSaga() {
    yield takeEvery(GET_USERS_FETCH, getUsersRequest);
}

export default getUsersSaga;
```

Notice the _asterisk_ that follows the function keyword, this is how you define a **generator** function in ES6. 

**yield** pauses the function until what comes after it is fulfilled, you can think of it as _await_ in async function.

**takeLatest** is a special redux saga effect - _Effects are simply instructions to the middleware to perform some operations_

So here **takeLatest** fires the instance immediately and takes the latest version of data, terminating any previous active request.

**takeLatest** takes two parameters, the first is an action type string that triggers the function that we put in the second parameter.

There are much more helpful Saga effects - _There is **takeEvery** which allows multiple requests to be fired concurrently, so you can start a new instance without terminating the previous active request._

**throttle** is also very useful to prevent too many API requests.


For more info about Saga effects check the [official docs](https://redux-saga.js.org/docs/recipes)

Copy the code down below and update the **saga** file

```
import { call, put, takeEvery } from "redux-saga/effects";
import { GET_USERS_FETCH, GET_USERS_SUCCESS } from "../actions/action";

function* getUsersRequest() {
    const users = yield call(() => fetch('https://jsonplaceholder.typicode.com/users'));
    const formattedUsers = yield users.json(); 
    yield put({ type: GET_USERS_SUCCESS, formattedUsers });
}

function* getUsersSaga() {
    yield takeEvery(GET_USERS_FETCH, getUsersRequest);
}

export default getUsersSaga;
```

Here we add the function that contains the actual logic for the API call.
**put** obviously puts the data we got from the API response and updates the store with it.

Now let's wire things up and update the **store** with the code below

```
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import users from '../reducers/reducer';

export default function configureStore() {
    const sagaMiddleware = createSagaMiddleware();
    const reducers = combineReducers({ users });

    return {
        ...createStore(reducers, applyMiddleware(sagaMiddleware)),
        runSaga: sagaMiddleware.run,
    }
}
```

Above we simply create _a saga middleware_ instance and return the run function that we are gonna pass the saga we just created into. 

Now we update the **index** file

```
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import configureStore from './store/configureStore';
import getUsersSaga from './sagas/saga';

const store = configureStore();
store.runSaga(getUsersSaga);

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
```

Finally, let's update our JSX - copy the code below into **App.js** and run the app.


```
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getUsersFetch } from './actions/action';


function App(){
  const dispatch = useDispatch();
  const users = useSelector(state => state.users.users);
  useEffect(() => {
    dispatch(getUsersFetch())
  }, [dispatch])
  return (
    <div className="App">
      {users.map(user => (<h2 key={Math.random()}>{user.name}</h2>))}
    </div>
  );
}
export default App;
```

Now let's build the same saga functionality with **RTK**

We will first need to install the toolkit dependency

`npm install @reduxjs/toolkit --save`


**Step 1** - Create usersSlice

**RTK** introduces **slice** which essentially replaces _reducers_ and _actions_ with **a single feature** that auto generates **action types** and **action creators**,

- Let's create a new folder in the root directory and name it **features**.
- Inside of it create **usersSlice.js** and copy the code below into it.
- We initialized a _slice_ using **createSlice** which is a function that takes an object of slice _name_, _initialState_ and _reducer_ functions, then export the _actions_ that redux generated for us and the reducer function that manages our state.


```
import { createSlice } from "@reduxjs/toolkit";

const usersSlice = createSlice({
  name: "users",
  initialState: {
    users: []
  },
  reducers: {
    getUsersSuccess: (state, action) => {
      state.users = action.payload;
    },
  }
});

export const { getUsersSuccess } = usersSlice.actions;

export default usersSlice.reducer;
```

Notice how we seem to be "mutating" the state object, but indeed RTK takes care of _immutability_ behind the scene using [Immer](https://redux-toolkit.js.org/usage/immer-reducers).

**Step 2** - Update Saga

Now let's update our **saga**

```
import { call, put, takeEvery } from "redux-saga/effects";
import { getUsersSuccess } from "../features/userSlice";

function* getUsersRequest() {
    const users = yield call(() => fetch('https://jsonplaceholder.typicode.com/users'));
    const formattedUsers = yield users.json(); 
    yield put(getUsersSuccess(formattedUsers)) 
}

export function* getUsersSaga(action) {
    yield takeEvery('users/getUsersFetch', getUsersRequest)
}
```

Notice in the **getUsersSaga** function how we are using the action type which is generated based on the slice name.

**Step 3** - Update configureStore

Now we make a slight change in the **configureStore** file and we are good to go!


```
import {
    configureStore,
} from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import usersReducer from "../features/userSlice";


export default function configureStoreFunction() {
    const sagaMiddleware = createSagaMiddleware();

    return {
        ...configureStore({
            reducer: {
                users: usersReducer
            },
            middleware: [sagaMiddleware]
        }),
        runSaga: sagaMiddleware.run,
    }
}
```

Here we use the RTK configureStore instead of the _deprecated_ createStore that we used in **plain Redux**

**configureStore** takes an object of reducer and middleware - reducer is an object of the generated reducers , and we add the saga instance into the middleware array.

Another slight change in the imports of **App.js**

```
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getUsersFetch } from "./features/userSlice";

function App() {
  const dispatch = useDispatch();
  const users = useSelector((state) => state.users.users);
  useEffect(() => {
    dispatch(getUsersFetch())
  }, [dispatch])
  return (
    <div className="App">
      {users.map(user => (<h2 key={Math.random()}>{user.name}</h2>))}
    </div>
  );
}
export default App;
```

Now it's time to explore **RTK Query**


## RTK Query

_We don't need to install any additional packages to use RTK Query._


**Step 1** - createApi

Let's start by updating the **usersSlice.js** with the code below

```
import { createApi } from '@reduxjs/toolkit/query/react'

export const apiSlice = createApi({
    reducerPath: 'UsersAPI'
})
```


Here we are defining _usersApi_ using **createApi** which is basically the core of _RTK Query's functionality_, then we pass an object and set the _reducerPath_ to **UsersAPI** to identify it.

After that, we're going to set _baseQuery_, which will essentially be our client with the API base URL.


```
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const apiSlice = createApi({
    reducerPath: 'UsersAPI',
    baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
})
```

We then assign _endpoints_ which will contain the logic that specifies the actual paths we are getting (or modifying) the fetch data from, and then start defining the fields using **builder** that we get from the arguments of _endpoints_.

```
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const apiSlice = createApi({
    reducerPath: 'UsersAPI',
    baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
    endpoints: (builder) => ({
        getUsers: builder.query({
            query: () => 'users',
        }),
    })
})
```


Now we set the basic query param that we are hitting, which would be **users**.

Here we used the **query** method because we are just getting data from the endpoint. If we are updating it, we would use **mutation**.

That will auto-generate a hook based on the endpoint's name, so that would be **useGetUsersQuery**, and then we are exporting it for later use.

**Full code:**

```
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const apiSlice = createApi({
    reducerPath: 'UsersAPI',
    baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
    endpoints: (builder) => ({
        getUsers: builder.query({
            query: () => 'users',
        }),
    })
})

export const {
    useGetUsersQuery
} = apiSlice
```

**Step 2** - ApiProvider

**Redux** requires us to wrap our app with a _Provider_ to which we pass our store.

When we use **RTK Query** for API access, we need to specify _ApiProvider_ and pass the **apiSlice** into it.

Let's update **index.js** with the code below

```
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApiProvider } from "@reduxjs/toolkit/query/react";
import App from './App';
import { apiSlice } from "./features/userSlice";

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <ApiProvider api={apiSlice}>
      <App />
    </ApiProvider>
  </React.StrictMode>
);
```

_Let's now make use of what we have integrated_

Update **App.js** with the following code:

```
import { useGetTodosQuery } from "./features/userSlice";

function App() {
  const {
    data: users,
    isLoading,
    isSuccess,
  } = useGetTodosQuery()  

  return (
    <div className="App">
      {isLoading && <span>Loading...</span>}
      {isSuccess && users.map(user => (<h2 key={Math.random()}>{user.name}</h2>))}
    </div>
  );
}
export default App;
```

**RTK query** provides us with some cool stuff that we usually track and write its logic ourselves.

It encapsulates the entire data fetching process, so you can _catch errors_ and trace the request status if it's still _pending_ or _fulfilled_ using **isLoading** without any handwritten boilerplate code that we usually do.

**Let's now run the app and that's a wrap!**

Full code can be found in the github repo [here](https://github.com/thisisgazzar/rtk-query-redux-saga).

## Conclusion:

As we can see, **Redux Saga** is a powerful tool with **many cool effects** built in, which saves you a lot of boilerplate code if you want to achieve what those effects offer.
However, with **RTK Query**, you can just kick off with a few lines of code _without_ installing any additional dependencies. It's also the **recommended approach** by the Redux team for API interactions.
Finally, you should carefully pick which one will fit better for your project, as it could be a pain in the ass if you decide to change it later.