Back

/ 10 min read

Redux—A State Management Library

Last Updated:

What is Redux?

Redux is a state management library for JavaScript applications. It helps you manage the state of your application in a predictable way. Redux is like a librarian for your app—organizing state, making it easy to find, and keeping chaos at bay. It works well with React, making state management fun!

What is state actually? Before answering this question, let’s understand why we use libraries like React, Angular, or Vue over vanilla JavaScript. Yes, they provide a way to create reusable UI components, but they also help us manage the state of our application. State is the data that drives your application. It can be anything from a simple boolean value to a complex object.

React provides a way to manage state using component state. But as your application grows in complexity, managing state in React can become challenging. This is where Redux comes in. Redux provides a way to manage the state of your application in a centralized store. This makes it easier to manage state across your application and keep it in sync.

How Redux Works

Redux works by following three core principles:

  1. Single Source of Truth: Redux stores the state of your application in a single object called the store. This makes it easier to manage state across your application and keep it in sync.

  2. State is Read-Only: In Redux, the state is read-only. This means that you cannot directly modify the state. Instead, you need to dispatch actions to update the state.

  3. Changes are Made with Pure Functions: In Redux, changes to the state are made by pure functions called reducers. Reducers take the current state and an action as input and return a new state.

Core Concepts

Redux has a few core concepts that you need to understand to use it effectively:

  1. Store: The store is the object that holds the state of your application. You can think of it as a database for your application.
  2. Actions: Actions are payloads of information that send data from your application to your Redux store. They are the only source of information for the store.
  3. Reducers: Reducers specify how the application’s state changes in response to actions sent to the store. Remember that actions only describe what happened, but don’t describe how the application’s state changes.
  4. Dispatch: This is the only way to trigger a state change. Dispatching an action is the process of sending an action to the store.
  5. Selectors: Selectors are functions that take the Redux store state as an argument and return some data to pass to your components.
  6. Middleware: Middleware provides a way to interact with actions that have been dispatched to the store before they reach the reducers.

Example

Here’s a simple example of how you can use Redux in a React application:

  1. Install Redux:
Terminal window
npm install redux react-redux
  1. Define actions:
actions.ts
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const increment = () => ({
type: INCREMENT,
});
export const decrement = () => ({
type: DECREMENT,
});
  1. Define a reducer:
reducer.ts
import { INCREMENT, DECREMENT } from './actions';
const initialState = {
count: 0,
};
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + 1,
};
case DECREMENT:
return {
...state,
count: state.count - 1,
};
default:
return state;
}
};
  1. Create a store:
store.ts
import { createStore } from 'redux';
import counterReducer from './reducer';
const store = createStore(counterReducer);
export default store;
  1. Dispatch actions in your components:
Counter.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';
const Counter = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
);
};
export default Counter;

Summary

From this example, we can summarize that:

  • Store: Holds the state.
  • Actions: Describe changes.
  • Reducers: Handle state changes.
  • Dispatch: Sends actions.
  • Selectors: Extract state.
  • Middleware: Extend functionality.

Redux Toolkit (RTK)

While Redux is a powerful library, it can be verbose and require a lot of boilerplate code. To address this, Redux Toolkit (RTK) was introduced. RTK is the official, recommended way to write Redux logic. It provides a set of tools and best practices that help you write Redux code faster and with less boilerplate.

Example with RTK

Here’s how you can rewrite the previous example using RTK:

  1. Install Redux Toolkit:
Terminal window
npm install @reduxjs/toolkit react-redux
  1. Define a slice:

    A slice is a collection of Redux reducer logic and actions for a single feature of your application.

counterSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 0,
},
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
},
});
  1. Create a store:

    Use configureStore to create the Redux store and combine slices if you have multiple.

store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterSlice from './counterSlice';
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
export default store;
  1. Provide the store to your app:

    Use the Provider component from react-redux to make the Redux store available to your React components.

App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';
const App = () => (
<Provider store={store}>
<Counter />
</Provider>
);
export default App;
  1. Dispatch actions in your components:

    Use the useSelector hook to read state from the Redux store and the useDispatch hook to dispatch actions.

Counter.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
const Counter = () => {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
);
};
export default Counter;

Summary

From this example, we can summarize that:

  • Redux Toolkit: Official way to write Redux logic.
  • Slice: Collection of reducer logic and actions.
  • configureStore: Create the Redux store.
  • Provider: Make the store available to components.
  • useSelector: Read state from the store.
  • useDispatch: Dispatch actions.

Persisting State

What happens if the user refreshes the page or closes the browser? The state of your application will be lost. To persist the state, you can use redux-persist. This library allows you to save the state of your Redux store to localStorage, sessionStorage, or any other storage engine.

Example with redux-persist

  1. Install redux-persist:
Terminal window
npm install redux-persist
  1. Configure the store with redux-persist:
store.ts
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import counterSlice from './counterSlice';
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, counterSlice.reducer);
const store = configureStore({
reducer: {
counter: persistedReducer,
},
});
export const persistor = persistStore(store);
export default store;
  1. Wrap your app with PersistGate:
App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import store, { persistor } from './store';
import Counter from './Counter';
const App = () => (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Counter />
</PersistGate>
</Provider>
);
export default App;

Summary

From this example, we can summarize that:

  • redux-persist: Persist the state of your Redux store.
  • persistStore: Persist the store.
  • PersistGate: Wrap your app to persist the state.

RTK Query

RTK Query is a powerful data fetching and caching tool built on top of Redux Toolkit. It simplifies the process of fetching data from an API and managing the cache. RTK Query provides a set of hooks that make it easy to fetch data in your React components.

Can we combine RTK Query with redux-persist? Yes, however, it’s important to understand that RTK Query is designed to manage caching, fetching, and synchronization of server data, and it has its own mechanisms for caching data. Persisting the entire RTK Query cache might not always be necessary or recommended, but you can persist specific parts of your state if needed.

Example with RTK Query

  1. Set up an API slice:
apiSlice.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
postPost: builder.mutation({
query: (post) => ({
url: 'posts',
method: 'POST',
body: post,
}),
}),
}),
});
export const { useGetPostsQuery, usePostPostMutation } = api;
  1. Create a store with RTK Query:
store.ts
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { api } from './apiSlice';
import counterSlice from './counterSlice';
const persistConfig = {
key: 'root',
storage,
whitelist: ['counter'], // Specify which reducers you want to persist
};
const persistedReducer = persistReducer(persistConfig, counterSlice.reducer);
const store = configureStore({
reducer: {
counter: persistedReducer,
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
export const persistor = persistStore(store);
export default store;
  1. Use RTK Query hooks in your components:
Posts.tsx
import React from 'react';
import { useGetPostsQuery } from './apiSlice';
const Posts = () => {
const { data, error, isLoading } = useGetPostsQuery();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
};
export default Posts;

Example Handling Authentication

RTK Query provides a way to handle authentication using hooks and endpoints. You can use the useQuery and useMutation hooks to fetch data and send mutations with authentication headers. You can also use the baseQuery option to add authentication headers to all requests.

  1. Create API service for authentication:
authService.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const authApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: 'auth/login',
method: 'POST',
body: credentials,
}),
}),
}),
});
export const { useLoginMutation } = authApi;
  1. Update the protected endpoint:
apiSlice.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
},
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
postPost: builder.mutation({
query: (post) => ({
url: 'posts',
method: 'POST',
body: post,
}),
}),
}),
});
export const { useGetPostsQuery, usePostPostMutation } = api;
  1. Create a slice for authentication:
authSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const authSlice = createSlice({
name: 'auth',
initialState: {
token: null,
},
reducers: {
setToken: (state, action) => {
state.user = action.payload.user;
state.token = action.payload.token;
},
clearToken: (state) => {
state.user = null;
state.token = null;
},
},
});
export const { setToken, clearToken } = authSlice.actions;
export default authSlice.reducer;
  1. Update the store configuration:
store.ts
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { api } from './apiSlice';
import { authApi } from './authService';
import counterSlice from './counterSlice';
import authSlice from './authSlice';
const persistConfig = {
key: 'root',
storage,
whitelist: ['counter', 'auth'],
};
const persistedReducer = persistReducer(persistConfig, counterSlice.reducer);
const store = configureStore({
reducer: {
counter: persistedReducer,
auth: authSlice.reducer,
[api.reducerPath]: api.reducer,
[authApi.reducerPath]: authApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware, authApi.middleware),
});
export const persistor = persistStore(store);
export default store;
  1. Use the useLoginMutation hook in your components:
Login.tsx
import React, { useState } from 'react';
import { useLoginMutation } from './authService';
const Login = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [login, { isLoading, error }] = useLoginMutation();
const handleSubmit = (e) => {
e.preventDefault();
login({ username, password });
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" disabled={isLoading}>
Login
</button>
</form>
{error && <div>Error: {error.message}</div>}
</div>
);
};
export default Login;

Summary

From this example, we can summarize that:

  • RTK Query: Data fetching and caching tool.
  • API Slice: Define endpoints for fetching data.
  • Caching: RTK Query has its own caching mechanisms.
  • Middleware: Use api.middleware in the store configuration.
  • Whitelist: Specify which reducers you want to persist.