React Hooks vs. Redux in 2024
TL;DR: This blog discusses how React Hooks and Redux differ in managing the state of React apps. React Hooks excels in simpler projects and component-level states, while Redux is better for complex, large-scale applications. Get insights into their core concepts and best practices for combining both approaches.
Overview of React Hooks and Redux
In this article, we will look at state management in React.
Redux and React are two of the most popular libraries for building modern web applications.
There are many different Hooks in React.
This article explores the difference between React Hooks and Redux in 2024.
Why this comparison is relevant in 2024
The front-end ecosystem is ever-evolving. As React evolves, developers face choices about managing the state of their applications.
Redux and React Hooks are two of the most popular programming languages for building web and mobile applications.
Understanding levels of state management
There are three types of state management in any modern web application:
-
Component-level: State used by only a single component. The
useState()
hook is typically used for this purpose. -
Module-level: State shared across multiple components within a module. The
useContext()
hook is ideal here, as it only binds the state to the module’s context. Combining it withuseState()
allows updating the state from any module part. -
Central-level: State shared among components throughout the application. In such cases, Redux is used for centralized state management.
In React, you can use the module-level state, the component-level state, and the component-level state.
Let’s see an example of different React Hooks and Redux and then compare them.
What are React Hooks?
In this tutorial, we will learn how to create React Hooks.
There are many built-in hooks available in React, and you can also create different custom hooks. However, we will explore the four hooks that are important for state management.
useState()
We define the useState Hook with a default value, and it returns an array of values, where the first value is the state, and the second value is a function to update the state.
Variables cannot be used to store the values as they get overridden once the component re-renders. React provides the Hook to persist the state.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return (
<main>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</main>
);
}
Once the value of the useState()
Hook changes, the component re-renders, and the value is rendered or displayed in the browser.
useEffect()
The useEffect()
is a lifecycle Hook which is invoked at three different stages in a component:
-
When the component mounts.
-
When anything in the dependency array changes.
-
When the component is about to unmount.
This Hook is crucial, as it allows tracking the component lifecycle, such as making network calls to fetch data from a server when the component mounts.
This record then can be stored using the useState()
Hook.
import React, { useState, useEffect } from "react";
function FetchData() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos")
// convert the raw data to JSON
.then((response) => response.json())
// then set the data
.then((data) => setData(data));
}, []); // Empty array means this effect runs once on mount
return (
<div>
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : "Loading..."}
</div>
);
}
useContext()
The useContext()
creates a boundary that provides all the components within the context that can access the state directly rather than having to be passed down in the component tree.
This helps optimize and remove the middleman’s access, who does not even consume the props but rather just passes them down further.
//FeatureFlag.js
import React, { useState } from "react";
// Split test context
export const SplitTestFlag = React.createContext({});
// split text provider
export const SplitTestFlagProvider = ({ children }) => {
const [features, setFeatures] = useState({
darkMode: true,
chatEnabled: false
});
return (
<SplitTestFlag.Provider value={{ features, setFeatures }}>
{children}
</SplitTestFlag.Provider>
);
};
// Component to conditionally render feature
const Feature = ({ feature, children, value }) => {
const { features } = React.useContext(SplitTestFlag);
return features[feature] === value ? children : null;
};
// Example
const Example = () => {
const { features, setFeatures } = React.useContext(SplitTestFlag);
return (
<>
<Feature feature="darkMode" value={true}>
in Dark Mode
</Feature>
<Feature feature="chatEnabled" value={true}>
Chat
</Feature>
<button onClick={() => setFeatures({ ...features, chatEnabled: true })}>
Enable Chat
</button>
</>
);
};
export default function App() {
return (
<SplitTestFlagProvider>
<Example />
</SplitTestFlagProvider>
);
}
useReducer()
The useReducer is a library for managing nested objects in React.
import React, { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment_count":
return { count: state.count + 1 };
case "decrement_count":
return { count: state.count - 1 };
default:
throw new Error("Invalid action");
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment_count" })}>+</button>
<button onClick={() => dispatch({ type: "decrement_count" })}>-</button>
</div>
);
}
export default function App() {
return <Counter />;
}
Advantages of using Hooks
-
Simplicity: Hooks simplifies state management by allowing stateful logic to be reused across components without changing the component hierarchy.
-
Readability: Using Hooks with the functional components makes them more readable and concise than class components.
-
Composition: Hooks promote the composition of stateful logic, making it easier to split and reuse logic across components.
-
Performance: Functional components with Hooks can offer better performance due to reduced overhead.
Common use cases for Hooks
-
Managing local component state.
-
Sharing stateful logic between components through custom hooks.
-
Simplifying complex state logic with
useReducer()
. -
Handling network calls and side-effects like subscription and unsubscription.
Understanding Redux
What is Redux?
Redux is a declarative state container for managing JavaScript apps.
Core Concepts: Actions, Reducers, Store
Actions
Actions describe what has happened. They are pure JavaScript functions that return a plain JavaScript object containing an identifier and the payload.
Identifiers help trace what state has to be changed, and the payload is the value that needs to be set to that state, which can be processed further. Refer to the following code example.
const increment = () => ({ type: 'INCREMENT', payload: null });
const decrement = () => ({ type: 'DECREMENT', payload: null });
Reducers
The BBC's Get Inspired programme looks at the role of the Reducer.
A reducer is pure JavaScript with a switch case or if-else block to determine the actions and their course. Refer to the following code example.
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
Store
This is the centralized data manager that stores the state. To better manage the state, we can define and manage one or more states in Redux.
import { createStore } from 'redux';
const store = createStore(counterReducer);
Multiple reducers can be combined. Refer to the following code example.
import { combineReducers } from '@reduxjs/toolkit'
import todos from './todos'
import counter from './counter'
export default combineReducers({
todos,
counter
});
Advantages of using Redux
-
Predictability: Redux’s unidirectional data flow makes state changes predictable and easier to debug.
-
Centralized State Management: Redux maintains a single source of truth, ensuring that the state is consistent across the application.
-
Middleware: Redux supports middleware for handling side effects, such as asynchronous actions with redux-thunk or redux-saga.
-
DevTools: Redux DevTools provide powerful tools for debugging and visualizing state changes.
Common use cases for Redux
-
Managing global state across large applications.
-
Handling complex state logic and side effects.
-
Ensuring predictability and consistency in state management.
-
Debugging state changes with powerful tools like Redux DevTools.
React Hooks vs. Redux: Comparative Analysis
State management
React Hooks: Ideal for managing local component state and side effects. If state management complexity is relatively low in smaller apps, Hooks works well. Refer to the following code example.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
Redux: Best suited for large-scale applications where global state management, predictability, and robust debugging tools are essential. Refer to the following code example.
import { createStore } from "redux";
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
const store = createStore(counterReducer);
Side effects handling
React Hooks: Handles side effects using useEffect()
.
useEffect(() => {
// Perform side effects here
return () => {
// Cleanup if necessary
};
}, [dependencies]);
Redux: Uses middleware like redux-thunk or redux-saga to manage side effects.
const thunkMiddleware = (store) => (next) => (action) => {
if (typeof action === "function") {
return action(store.dispatch, store.getState);
}
return next(action);
};
Code complexity and boilerplate
In this article, you will learn how to use React Hooks in a large application.
const [state, setState] = useState(initialState);
useEffect(() => {
// Side effect logic
}, []);
Redux: Introduces more boilerplate code due to actions, reducers, and the store setup. This can lead to verbose and complex code, especially in large applications. Refer to the following code example.
const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });
function counterReducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
Scalability
React Hooks: Managing the state with Hooks in large applications can become challenging without proper organization and conventions. Custom Hooks can help, but they require careful design.
function useCustomHook() {
const [state, setState] = useState(initialState);
// Custom hook logic
return [state, setState];
}
Redux is an open-source distributed state management platform.
const rootReducer = combineReducers({
// Combine multiple reducers
});
const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
Performance
React Hooks: Functional components with Hooks can offer better performance due to reduced overhead. Hooks also enable fine-grained control over when and how components re-render.
const [state, setState] = useState(initialState);
useEffect(() => {
// Side effect logic
}, [dependencies]);
Redux and Middleware are two approaches to managing actions, reducers, and the store.
const store = createStore(counterReducer, applyMiddleware(thunkMiddleware));
When to use React Hooks
-
Small to Medium-Sized Applications: React Hooks are ideal for applications where the state management requirements are relatively simple and localized.
-
Team Experience with Functional Components: Teams familiar with functional components and the concept of Hooks will find it easier to implement state management using Hooks.
-
Component-Level State: Managing state that is specific to individual components or closely related components.
function Toggle() {
const [isOn, setIsOn] = useState(false);
return <button onClick={() => setIsOn(!isOn)}>{isOn ? "On" : "Off"}</button>;
}
- Rapid Development: When you need to develop features quickly without the overhead of setting up a complex state management system.
When to use Redux
-
Large-Scale Applications: Redux is best suited for managing global states across large applications with many moving parts.
-
Team Experience with Redux: Teams familiar with Redux’s concepts and patterns will benefit from its predictability and debugging tools.
-
Complex State Logic: When the application requires complex state logic and side effect management that Redux middleware can handle effectively.
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const initialState = { data: null };
function dataReducer(state = initialState, action) {
switch (action.type) {
case "SET_DATA":
return { ...state, data: action.payload };
default:
return state;
}
}
const store = createStore(dataReducer, applyMiddleware(thunk));
Integration: Combining React Hooks and Redux
-
Use React Hooks to manage local state and component-level side effects.
-
Use Redux to manage global and complex state logic spanning multiple components.
Refer to the following code example.
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchData } from "./actions";
function DataComponent() {
const [localState, setLocalState] = useState(null);
const globalState = useSelector((state) => state.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchData());
}, [dispatch]);
return (
<div>
<button onClick={() => setLocalState("Updated Local State")}>
Update Local State
</button>
<p>Local State: {localState}</p>
<p>Global State: {JSON.stringify(globalState)}</p>
</div>
);
}
Performance considerations
-
Ensure that state updates are optimized to avoid unnecessary re-renders.
-
Use memoization techniques to optimize component performance.
Conclusion
Redux and React Hooks will be phased out by the end of the decade.
Redux and React Hooks are two of the most popular programming languages for building web and mobile applications.