Redux is a popular state management library for JavaScript applications, particularly for React. It provides a centralized store for managing application state and makes it easier to understand and debug the state changes in your app.
React is a great library for building UI components, but it doesn’t provide a built-in way to manage the application state. That’s where Redux comes in. By using Redux, you can centralize your application state and keep it in sync with your UI components. This makes it easier to understand and debug your application, as well as improve performance.
Understanding the Redux Store
State Management in React
In React, you manage your application state by passing data from parent components to child components via props. While this works well for small applications, it can become difficult to manage the state as your application grows. This is where Redux can help.
Redux Store
Redux Store is an object that stores the app’s state and makes it accessible to all components. The store holds the state tree and allows access through selectors and dispatched actions. The store is the single source of truth and ensures predictable state updates.
Actions and Reducers
Redux actions are objects that represent an intention to change the state. They are dispatched to the Redux store to trigger a state update. Actions contain information about the intended change, such as the type of change and any relevant data.
Redux reducers are pure functions that receive the current state and an action and return a new state. The reducer takes the action’s information and uses it to update the state predictably. Reducers should not modify the existing state, instead, they should create a new state object based on the existing state.
In short, actions describe changes and reducers implement those changes in the state.
Redux-Toolkit
Redux-Toolkit is a set of utilities for Redux that simplify the setup and management of a Redux store. It provides a simpler and more efficient way to manage your application state, making it easier to build complex applications with Redux and React.
Redux Toolkit uses a concept of slices.
A slice is a piece of state and a set of related actions and reducers that can be grouped. This allows you to modularize and organize your state management in a more manageable way.
Each slice has its state, actions, and reducers, and these pieces can be managed independently from other slices. This leads to better separation of concerns and makes it easier to maintain your state management code.
In Redux-Toolkit, slices are created using the createSlice
function. This function takes in an initial state object and a set of reducers and returns a slice object that can be combined with other slices to create a complete state management solution for your application.
Building a React-Redux App
Setting up the project
I recommend starting with the creation of simple React applications using create-react-app. Let’s create a new project:
npx create-react-app counter-app
cd counter-app
The next step is installing all necessary dependencies. In our case it’s Redux, React-Redux, and Redux Toolkit:
npm install redux react-redux @reduxjs/toolkit
Now we have all dependencies we need.
Setting up the Store
First of all, you need to create a new directory to store all stuff related to the store. Let’s create a store
directory with an index.js
file under src
:
src/
|-- store/
| |-- index.js
|-- App.js
|-- index.js
Setting up a Redux store with Redux Toolkit is quick and simple. You can use the configureStore
function to set up a store with sensible defaults, and easily add middleware and enhancers as needed.
configureStore
returns a store instance, which we then export for use in our application. The store instance has the standard Redux methods, such as getState
, dispatch
, and subscribe
, as well as other useful methods provided by Redux Toolkit.
src/store/index.js:
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {}
});
export default store;
The next step is providing a store
for the whole application. You can do it using the Provider
component from the React-Redux library.
src/index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from "react-redux";
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Now every component in the application has access to the created store.
Creating Actions and Reducers (Slice)
One of the key features of Redux Toolkit is the createSlice
function, which allows you to define a reducer and its associated actions in a single place. This function generates action creators and action types for you, and can also handle creating a thunk for you.
Let’s create a counterSlice
:
src/store/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0
},
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
}
}
});
export default counterSlice.reducer;
export const { increment, decrement } = counterSlice.actions;
In this example, createSlice
is used to create a new slice for a counter with an initial value of 0. The reducers property defines two actions, increment and decrement, which are used to update the state.
Actions are used to send data from your application to the Redux store. In Redux-Toolkit, actions are defined as functions in your slice. For example:
const { increment, decrement } = counterSlice.actions;
Reducers are used to update the state in the Redux store based on the actions received. In Redux-Toolkit, the reducers
property of a slice is used to define how the state should be updated.
The next step is updating the store with just created slice.
src/store/index.js:
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
const rootReducer = combineReducers({
counter: counterReducer
});
const store = configureStore({
reducer: rootReducer
});
export default store;
Function combineReducers
The combineReducers
function is a utility function in Redux that allows you to combine multiple reducers into a single root reducer. The root reducer is the function that is passed to the createStore
function to create the Redux store.
Each reducer in the combineReducers
function is responsible for managing a specific slice of the state. The combineReducers
function takes in an object where each property maps to a reducer function, and the key of each property is used to determine the corresponding slice of the state.
For example, if you have a counter and a user profile in your state, you might use combineReducers
like this:const rootReducer = combineReducers({
counter: counterReducer,
profile: profileReducer,
});
const store = configureStore({
reducer: rootReducer
});
In this example, the counterReducer
is responsible for managing the counter
slice of state, and the profileReducer
is responsible for managing the profile
slice of the state. These two reducers are combined into the rootReducer
using the combineReducers
function.
Now you have everything to start using this state inside the application.
Accessing Store from Components
Let’s create a component that will use the store.
The react-redux
library provides a set of hooks that make it easier to connect components to the Redux store and dispatch actions.
The most important hook is the useSelector
hook, which allows you to extract the state from the store and map it to component props.
src/Components/Counter.js
import React from 'react';
import { useSelector } from "react-redux";
const Counter = () => {
const counterValue = useSelector(state => state.counter.value);
return (
<div>{counterValue}</div>
);
};
export default Counter;
The Counter
component uses the useSelector
hook to extract the counter value from the store and render it on a page.
Another important hook is the useDispatch
hook, which allows you to access the dispatch
function from the store and dispatch actions.
src/Components/Counter.js
import React from 'react';
import { useDispatch, useSelector } from "react-redux";
import { increment, decrement } from "../store/counterSlice";
const Counter = () => {
const counterValue = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
{counterValue}
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
};
export default Counter;
The Counter
component now uses the useDispatch
hook to access the dispatch
function from the store and dispatch actions to increment and decrement the store value by clicking buttons.
src/App.js
import './App.css';
import Counter from "./components/Counter";
function App() {
return (
<div className="App">
<Counter />
</div>
);
}
export default App;
Now the counter application is ready so you can try it out!
Best Practices for Using Redux
When using Redux with React, it’s important to keep the store simple. Having a large and complex store can make it difficult to understand what’s going on and can make it harder to debug issues. To keep the store simple, consider the following tips:
- Break down complex state slices into smaller, simpler slices.
- Avoid keeping non-reusable data in the store.
- Use selectors to extract data from the store and reduce the complexity of state management.
Conclusion
Recap of the Benefits of using Redux
Redux provides several benefits when used in combination with React:
- Centralized state management: Redux provides a centralized store for your application’s state, making it easier to manage and track changes in your state over time.
- Predictable state updates: Redux follows strict rules for updating state, making it easier to understand and predict how your application’s state will change over time.
- Reusable state logic: Redux encourages you to write modular and reusable state logic, making it easier to manage and maintain your application as it grows.
- Improved performance: Redux provides efficient updates to your application’s state, reducing the number of unnecessary re-renders in your React components.
- Easy debugging: Redux provides a detailed log of all state changes, making it easier to debug and understand how your application’s state is changing.
- Better code structure: Redux helps you write cleaner and more structured code by encouraging you to write modular, reusable, and well-organized state management logic.
- Improved collaboration: Redux makes it easier for multiple developers to work on a project together, as it provides a clear and well-defined way to manage the state in a React application.
Drawbacks of using Redux
While using Redux with React has several benefits, there are also some potential drawbacks to consider:
- Steep learning curve: Redux can be complex and difficult to learn, especially for developers who are new to the concept of state management.
- Overhead: Using Redux can introduce additional overhead and complexity to your application, especially if you are managing a large and complex state.
- Boilerplate code: Redux requires a significant amount of boilerplate code, which can be repetitive and time-consuming to write.
- Performance issues: In some cases, using Redux can negatively impact the performance of your application, especially if you are not careful with how you manage your state updates.
- Testing difficulties: Testing code that uses Redux can be more difficult than testing regular React components, as you need to test both the state updates and the components that rely on that state.
- Verbose code: Redux often requires writing more verbose and complex code compared to other state management alternatives, making it less readable and maintainable.
Overall, using Redux with React can be a powerful tool for managing the state of your application, but it is important to carefully weigh the benefits and drawbacks before deciding if it is the right choice for your specific use case.