From Vanilla Redux to TanStack react-query
Tags: redux, reactnative, reactquery, performance
Published: 2023-07-17 16:08:25

When dealing with a legacy React/React Native codebase, it is not uncommon to come across the usage of vanilla Redux in various aspects, such as managing client-state, handling side effects with thunks/sagas, or even managing server-state. 

You can easily find tons of boilerplate for _actions, reducers_, and maintaining a central store, which can be time-consuming and error-prone. Additionally, vanilla Redux does not offer built-in solutions for handling asynchronous actions, leading developers to rely on _middleware_ libraries like _redux-thunk_ or _redux-saga_. 

As technology evolves and newer approaches and libraries emerge, it becomes essential to consider migrating from vanilla Redux to a more modern approach.

Separation of client-state vs async server-state management is highly recommended in modern approach. There're many solutions for client-state management like Redux/Redux-Toolkit, Zustand, Jotai or Recoil from Meta/FB. On the server side, options are also affluent: RTK-Query, SWR, Apollo Client, and TanStack Query.

Recently, I had the opportunity to revamp our legacy code base, which heavily relies on vanilla Redux for managing both client and server state. While there was a desire to work with a newer tech stack, it became apparent that, for the time being, leveraging the current code base was the most practical approach. Therefore, we will be sticking with the legacy vanilla Redux for the time being. However, this presents a valuable opportunity to implement a 'separation of concerns' approach, utilizing Redux for client-state management while finding a suitable solution for asynchronous state management.

Since we are already using Redux, opting for Redux Toolkit (RTK) seems like a logical choice. This [blog](https://shopify.engineering/react-redux-toolkit-migration) further strengthens our confidence in this decision.

**But...**


![react-query](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sznglnoq62qy103zc4km.jpg)



TanStack Query was selected because I wanted to minimize the setup of additional boilerplate code, even though it has been significantly reduced with RTK. I also desired a separate handling of server state. After researching and exploring various options, I found TanStack Query to be simpler, with well-documented [resources](https://tanstack.com/query/latest). Another factor influencing my decision was the [complexity](https://redux.js.org/tutorials/essentials/part-8-rtk-query-advanced#injecting-endpoints) involved in setting up the endpoints when working with APIs from multiple sources. TanStack Query offered a more straightforward and streamlined approach. It is happy as long as it gets a [Promise.](https://tanstack.com/query/latest/docs/react/guides/queries)

**Enough talk, show me some code already!**

Here is simplified code which will do HTTP request to get _Feed_, and also _Cards_ in certain condition. The result will be either error or success, along with the corresponding data, and appropriate actions will be dispatched.


``` js
// A typical thunk to fetch feed from a HTTP Api

const getFeed = (filter: FilterKey) => (dispatch) => {
    dispatch({ type: 'FEED_WILL_REFRESH' })

    Api.getFeed(filter).then((res) =>
        res.either(
            (error) =>
                dispatch({
                    type: 'FEED_DID_REFRESH_ERROR',
                    error,
                }),
            (feed) => {
                if (filter === 'recent') {
                    // also fetch cards if filter is 'recent'
                    dispatch(getCards(feed))
                } else {
                    dispatch({
                        type: 'FEED_DID_REFRESH',
                        feed,
                    })
                }
            }
        )
    )
}

// Fetch cards

const getCards = (feed: Feed) => (dispatch) => {
    dispatch({ type: 'FEED_WILL_GET_CARDS' })

    Api.getCards().then((res) =>
        res.either(
            (error) =>
                dispatch({
                    type: 'FEED_DID_REFRESH_ERROR',
                    error,
                }),
            (cards) =>
                dispatch({
                    type: 'FEED_DID_REFRESH',
                    feed: { ...feed, cards },
                })
        )
    )
}

// ...

// other code for reducers or API request are removed for simplicity purpose.

```

Equivalent code with RTK :)

``` js
// Async thunk for fetching the Feed
export const fetchFeedAsync = createAsyncThunk('feed/fetchFeed', async () => {
  const response = await Api.getFeed(...);
  return response; 
});

// Async thunk for fetching Cards
export const fetchCardsAsync = createAsyncThunk('cards/fetchCards', async () => {
  const response = await Api.getFeed();
  return response; 
});

// Slice for managing the Feed state
const feedSlice = createSlice({
  name: 'feed',
  initialState: {
    data: null,
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchFeedAsync.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchFeedAsync.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchFeedAsync.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

// Slice for managing the Cards state
const cardsSlice = createSlice({
  name: 'cards',
  initialState: {
    data: null,
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchCardsAsync.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchCardsAsync.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchCardsAsync.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

```

Equivalent code with TanStack Query:

``` js

...

// Fetch Feed with useQuery
const useFeedQuery = (filter: FilterKey) =>
    useQuery({
        queryKey: ['feed', filter],
        queryFn: () => Api.getFeed(filter),
    })

// and fetching Cards - only "enabled" if filter is "recent"
const useCardsQuery = (filter: FilterKey) =>
    useQuery({
        queryKey: ['cards', filter],
        queryFn: Api.getCards,
        enabled: filter === 'recent',
    })

```

As you can see, the code is much clearer and contains less boilerplate when using TanStack Query. Additionally, with TanStack Query, if the filter is set to 'recent', both queries will run in parallel instead of in a cascade, as was the case with the legacy code. This enhances performance and allows for more efficient data retrieval.

_loading_ and _error_ states are given by TanStack Query out of the box. Additionally, it serves as a robust _global state manager_. If you call the query with the same _queryKey_ in multiple places, you will consistently receive the same data. TanStack Query also _deduplicates_ requests that occur simultaneously, resulting in fewer network requests and faster response times. This helps optimize network and improves overall performance.

The end result of this simple migration is highly satisfying, with **~ 1000** line of legacy Redux code is removed. The majority of these code consisted of vanilla Redux boilerplate, which is no longer necessary with the adoption of TanStack Query. This reduction in code not only simplifies the codebase but also improves maintainability and readability, making it easier to work with.

- ~~Action Creators~~
- ~~Middlewares~~
- ~~Reducers~~
- ~~Loading/Error/Result states~~

All of them are provided by React Query with just a few lines of code: 

``` js
const { isLoading, isError, error, data } = useFeedQuery(...)
```

Another advantage of adopting TanStack Query is its ability to coexist with our legacy vanilla Redux codebase. This allows for a gradual migration process, enabling us to make improvements over time without the need for a complete overhaul. Having TanStack Query alongside our legacy Redux provides a clear path forward and a proper solution for enhancing our state management. It brings us confidence that we can incrementally improve our codebase, making the transition from legacy Redux to a more efficient and streamlined solution less daunting and more manageable.

That's it for today. In the next post, I will focus on comparing performance _before and after_, examining how this simple migration has made a positive impact. Let me know if you have any specific areas you would like me to focus on for further polishing.