
Sunscrapers Team
31 August 2022, 6 min read

What's inside
Intro
Some people believe that Redux is over and shouldn’t be used anymore in any kind of project.
We have React-Query, Mobx, XState, Zustand, Recoil, and other state management libraries.
Read our Complete Guide to state management in React - part 1 and part 2.
However, recently Redux Toolkit, like the main library for Redux now, introduced a few interesting APIs. We will look at it and decide if it makes more sense to use it over the Context API.
Many people believe Context is a state management tool.
Some people wonder if we should be teaching (or learning) Redux in 2022, and according to Redux maintainer Mark Eriksen,
"(Context), It's a Dependency Injection mechanism, whose only purpose is to make a single value accessible to a nested tree of React components. It's up to you to decide what that value is and how it's created. Typically, that's done using data from React component state, i.e., useState and useReducer. So, you're actually doing all the "state management" yourself - Context just gives you a way to pass it down the tree.
Redux is a library and a pattern for separating your state update logic from the rest of your app and making it easy to trace when/where/why/how your state has changed. It also gives your whole app the ability to access any piece of state in any component.
In addition, there are some distinct differences between how Context and (React-) Redux pass along updates. Context has some major limitations - in particular, any component that consumes a context will be forced to re-render, even if it only cares about part of the context value”.
The whole discussion and Mark Eriksen’s answer are available here.
When Redux should be used
According to Redux documentation, you might need Redux when:
- You have large amounts of application states needed in many places in the app.
- The app state is updated frequently.
- The logic to updating that state may be complex.
- The app has a medium or large-sized codebase and might be worked on by many people.
- You need to see how that state is being updated over time.
Slices in Redux Toolkit & inject reducer
Redux Toolkit now offers a slice API.
You can create actions and reducers in one file:
(example from Redux Toolkit documentation)
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState = { value: 0 } as CounterState
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
Also, there is an option to inject reducers:
import { createStore } from 'redux'
// Define the Reducers that will always be present in the application
const staticReducers = {
users: usersReducer,
posts: postsReducer
}
// Configure the store
export default function configureStore(initialState) {
const store = createStore(createReducer(), initialState)
// Add a dictionary to keep track of the registered async reducers
store.asyncReducers = {}
// Create an inject reducer function
// This function adds the async reducer, and creates a new combined reducer
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer
store.replaceReducer(createReducer(store.asyncReducers))
}
// Return the modified store
return store
}
function createReducer(asyncReducers) {
return combineReducers({
...staticReducers,
...asyncReducers
})
}
With this not only you can make your application more performance-oriented, and inject only part of the store you need for a particular view. Also, you have Redux developer tools available which are really helpful when working on bigger, scalable applications.
There are also a lot of packages that can help you with that.
RTK Query
On top of that if you use Redux Toolkit slice API you can also migrate to RTK Query which uses slices API.
RTK Query solves different kinds of problems than Redux:
- Tracking loading state in order to show UI spinners.
- Avoiding duplicate requests for the same data.
- Optimistic updates to make the UI feel faster.
- Managing cache lifetimes as the user interacts with the UI.
There are two ways of initializing it:
import { createApi } from '@reduxjs/toolkit/query'
/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react'
and then you can write base hook, that you will re-use later on:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'
// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi
You will also need to configure the store as it works with Redux:
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[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),
})
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)
And now you have cashing included, and the same goodies as React Query. You can use now use the hook we prepared before:
(example from RTK Query documentation)
import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'
export default function App() {
// Using a query hook automatically fetches data and returns query values
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
// Individual hooks are also accessible under the generated endpoints:
// const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')
// render UI based on data and loading state
}
As you can see the API is quite similar to React Query. If you are interested about the differences between those two, there is an interesting table in React Query documentation - check it here.
There is also some comparison in Redux Toolkit documentation which concentrates on different aspects of the comparison - here.
Summary
Redux Toolkit and Context are very different tools for different purposes.
The usage of each should depend on the project itself and business requirements, there is no “jack-of-all-trades” when it comes to state management.
Redux Toolkit offers many tools and API’s for experienced developers, who are comfortable with Redux. So if you are a seasoned professional - it’s worth considering it and RTK Query as well. If you are less experienced then probably Context and React Query might be an option to go.