Introduction
Managing state in complex applications can quickly become challenging. React’s local state is sufficient for small to medium applications, but when multiple components need to share state or when state logic becomes complex, a more structured solution is required. Redux provides a predictable state management solution for JavaScript applications.
Redux follows a strict unidirectional data flow that ensures application state is centralized, predictable, and easy to debug. Understanding Redux fundamentals is essential for developers building large-scale React applications.
In this post, we will cover:
- Introduction to Redux architecture
- Core concepts: Actions, Reducers, Store
- Dispatching actions and updating state
- A practical example: A simple counter with Redux
Introduction to Redux Architecture
Redux is built around three main principles:
- Single Source of Truth
- All application state is stored in a single store.
- This ensures consistency and easy debugging.
- State is Read-Only
- State can only be changed by dispatching actions.
- Actions are plain objects that describe what happened.
- Changes are Made with Pure Functions
- Reducers are pure functions that take the current state and an action, and return a new state.
- Reducers never mutate the existing state.
Unidirectional Data Flow
Redux enforces a predictable flow:
Component -> Dispatch Action -> Reducer -> New State -> Component
- Components trigger actions.
- Actions are processed by reducers.
- The store updates the state and notifies subscribed components.
Core Concepts of Redux
1. Store
The store is the central object that holds the entire state of the application.
- Methods provided by the store:
getState(): Returns the current state.dispatch(action): Sends an action to the reducer to update state.subscribe(listener): Registers a listener that runs whenever state changes.
Creating a Store
import { createStore } from 'redux';
// Initial state
const initialState = { count: 0 };
// Reducer function
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;
}
}
// Create store
const store = createStore(counterReducer);
console.log(store.getState()); // { count: 0 }
2. Actions
Actions are plain JavaScript objects that describe what happened in the application.
- Must have a
typeproperty. - Can include a
payloadfor additional data.
const incrementAction = { type: 'INCREMENT' };
const decrementAction = { type: 'DECREMENT' };
const addAmountAction = { type: 'ADD_AMOUNT', payload: 5 };
3. Reducers
Reducers are pure functions that take the current state and an action, then return a new state.
function counterReducer(state = { count: 0 }, action) {
switch(action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'ADD_AMOUNT':
return { count: state.count + action.payload };
default:
return state;
}
}
- Never mutate state directly.
- Always return a new object.
Dispatching Actions and Updating State
The only way to update state in Redux is to dispatch actions to the store.
Example: Dispatching Actions
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // { count: 1 }
store.dispatch({ type: 'DECREMENT' });
console.log(store.getState()); // { count: 0 }
store.dispatch({ type: 'ADD_AMOUNT', payload: 10 });
console.log(store.getState()); // { count: 10 }
- Each dispatch calls the reducer with the current state and action.
- The reducer returns a new state, which updates the store.
- Subscribed components will automatically be notified of the change.
Subscribing to Store Changes
store.subscribe(() => {
console.log("State updated:", store.getState());
});
store.dispatch({ type: 'INCREMENT' }); // Logs: State updated: { count: 1 }
- Components or functions can subscribe to changes.
- Useful for updating the UI when state changes.
Example: Simple Counter with Redux
Step 1: Install Redux
npm install redux react-redux
Step 2: Create the Reducer
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 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
Step 3: Create the Store
import { createStore } from 'redux';
const store = createStore(counterReducer);
Step 4: React Component
import React from 'react';
import { Provider, useDispatch, useSelector } from 'react-redux';
import { createStore } from 'redux';
// Reducer and store
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 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
const store = createStore(counterReducer);
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
export default App;
Key Points:
Providerwraps the app and supplies the store.useSelectorreads state from the store.useDispatchdispatches actions to update state.- UI updates automatically when state changes.
Advantages of Redux
- Predictable State: State changes are centralized and predictable.
- Single Source of Truth: All state is in one store, making debugging easier.
- Scalable: Suitable for large applications with complex state logic.
- Time Travel Debugging: Redux DevTools allow inspection of state changes.
- Separation of Concerns: Components focus on rendering, while state logic is handled by reducers.
Best Practices
- Keep actions descriptive: Use meaningful action types like
INCREMENT_COUNTERinstead ofADD. - Do not mutate state: Always return new objects.
- Use multiple reducers if needed: Combine reducers for modularity using
combineReducers. - Keep state normalized: Avoid nested structures; use IDs for references.
- Use Redux DevTools: Inspect state and dispatched actions.
- Prefer action creators: Encapsulate action objects in functions for consistency.
Leave a Reply