Overview
Redux is a predictable state container for JavaScript applications. It helps manage application state in a centralized store, making state changes traceable and debuggable.
Redux Store Single source of truth for application state
Actions Describe what happened in the app
Reducers Specify how state changes
Redux Toolkit Modern, efficient Redux development
Why Redux?
Prop Drilling : No need to pass state through many components
State Management : Centralized state easier to manage
Predictability : State changes follow strict patterns
Debugging : Time-travel debugging with dev tools
Scalability : Works well in large applications
Consider Redux when:
App has complex state logic
State is needed in many places
State updates follow complex patterns
You need powerful debugging tools
Team prefers explicit state management
You might NOT need Redux for:
Simple apps with minimal state
State mostly local to components
Quick prototypes
Core Concepts
The single source of truth holding application state: import { configureStore } from '@reduxjs/toolkit' ;
const store = configureStore ({
reducer: {
counter: counterReducer ,
auth: authReducer ,
},
});
The store:
Holds entire application state
Allows state access via getState()
Updates state via dispatch(action)
Registers listeners via subscribe()
Plain objects describing what happened: // Action object
{
type : 'counter/increment' ,
payload : 10
}
Actions have:
type : String describing the action
payload : (Optional) Additional data
Pure functions specifying state changes: function counterReducer ( state = initialState , action ) {
switch ( action . type ) {
case 'counter/increment' :
return { ... state , value: state . value + 1 };
default :
return state ;
}
}
Reducers must:
Be pure functions (no side effects)
Return new state (never mutate)
Handle unknown actions by returning state
Redux Toolkit is the official, recommended way to write Redux logic. It simplifies common patterns and includes best practices.
Install Redux Toolkit
npm install @reduxjs/toolkit react-redux
Create a Slice
Slices contain reducer logic and actions for a feature
Configure Store
Combine slices into a single store
Provide Store
Make store available to React components
Use in Components
Read state with useSelector and dispatch with useDispatch
Creating a Slice
Slices are the core of Redux Toolkit, combining actions and reducers.
import { createSlice } from '@reduxjs/toolkit' ;
const initialCounterState = { counter: 0 , showCounter: true };
const counterSlice = createSlice ({
name: 'counter' ,
initialState: initialCounterState ,
reducers: {
increment ( state ) {
// Looks like mutation, but Immer handles it!
state . counter ++ ;
},
decrement ( state ) {
state . counter -- ;
},
increase ( state , action ) {
state . counter = state . counter + action . payload ;
},
toggleCounter ( state ) {
state . showCounter = ! state . showCounter ;
},
},
});
export const counterActions = counterSlice . actions ;
export default counterSlice . reducer ;
createSlice Benefits:
Automatically generates action creators
Uses Immer for “mutable” updates
Reduces boilerplate code
Type-safe with TypeScript
Configuring the Store
Combine all slices into a single store:
import { configureStore } from '@reduxjs/toolkit' ;
import counterReducer from './counter' ;
import authReducer from './auth' ;
const store = configureStore ({
reducer: {
counter: counterReducer ,
auth: authReducer ,
},
});
export default store ;
With this configuration, state looks like: {
counter : {
counter : 0 ,
showCounter : true
},
auth : {
isAuthenticated : false
}
}
Providing the Store
Make the store available to your React app:
import React from 'react' ;
import ReactDOM from 'react-dom/client' ;
import { Provider } from 'react-redux' ;
import store from './store/index' ;
import App from './App' ;
const root = ReactDOM . createRoot ( document . getElementById ( 'root' ));
root . render (
< Provider store = { store } >
< App />
</ Provider >
);
The Provider component must wrap your entire app to give all components access to the store.
Using Redux in Components
Access state and dispatch actions using React-Redux hooks.
Read data from the store: import { useSelector } from 'react-redux' ;
const counter = useSelector (( state ) => state . counter . counter );
const show = useSelector (( state ) => state . counter . showCounter );
const isAuth = useSelector (( state ) => state . auth . isAuthenticated );
useSelector automatically subscribes to store updates. Component re-renders when selected state changes.
Dispatch actions to update state: import { useDispatch } from 'react-redux' ;
import { counterActions } from '../store/counter' ;
const dispatch = useDispatch ();
// Dispatch actions
dispatch ( counterActions . increment ());
dispatch ( counterActions . increase ( 10 ));
dispatch ( counterActions . toggleCounter ());
Complete Component Example
import { useSelector , useDispatch } from 'react-redux' ;
import { counterActions } from '../store/counter' ;
import classes from './Counter.module.css' ;
const Counter = () => {
const dispatch = useDispatch ();
const counter = useSelector (( state ) => state . counter . counter );
const show = useSelector (( state ) => state . counter . showCounter );
const incrementHandler = () => {
dispatch ( counterActions . increment ());
};
const increaseHandler = () => {
dispatch ( counterActions . increase ( 10 ));
};
const decrementHandler = () => {
dispatch ( counterActions . decrement ());
};
const toggleCounterHandler = () => {
dispatch ( counterActions . toggleCounter ());
};
return (
< main className = { classes . counter } >
< h1 > Redux Counter </ h1 >
{ show && < div className = { classes . value } > { counter } </ div > }
< div >
< button onClick = { incrementHandler } > Increment </ button >
< button onClick = { increaseHandler } > Increase by 10 </ button >
< button onClick = { decrementHandler } > Decrement </ button >
</ div >
< button onClick = { toggleCounterHandler } > Toggle Counter </ button >
</ main >
);
};
export default Counter ;
Action Payloads
Pass data to actions via payloads:
Slice Definition
Component Usage
const counterSlice = createSlice ({
name: 'counter' ,
initialState: { counter: 0 },
reducers: {
increase ( state , action ) {
// action.payload contains the data
state . counter = state . counter + action . payload ;
},
},
});
Redux Toolkit Payload Convention: When you dispatch counterActions.increase(10), RTK creates: {
type : 'counter/increase' ,
payload : 10
}
For multiple values, pass an object: dispatch ( counterActions . updateUser ({ id: 1 , name: 'John' }));
Working with Multiple Slices
Organize related state into separate slices:
import { useSelector } from 'react-redux' ;
import Counter from './components/Counter' ;
import Auth from './components/Auth' ;
import Header from './components/Header' ;
import UserProfile from './components/UserProfile' ;
function App () {
const isAuthenticated = useSelector (( state ) => state . auth . isAuthenticated );
return (
<>
< Header />
{ ! isAuthenticated && < Auth /> }
{ isAuthenticated && < UserProfile /> }
< Counter />
</>
);
}
export default App ;
Slice Organization: Create separate slices for different features:
userSlice - User profile and preferences
cartSlice - Shopping cart
authSlice - Authentication state
uiSlice - UI state (modals, notifications)
Each slice is independent and focused on one concern.
Redux Immutability
Traditional Redux
Redux Toolkit
Manually create new state objects: function counterReducer ( state , action ) {
if ( action . type === 'increment' ) {
// ✅ Create new object
return {
... state ,
counter: state . counter + 1
};
}
// ❌ NEVER mutate state
// state.counter++;
// return state;
}
RTK uses Immer - write “mutable” code that’s converted to immutable: const counterSlice = createSlice ({
name: 'counter' ,
initialState: { counter: 0 },
reducers: {
increment ( state ) {
// Looks like mutation, but it's not!
// Immer converts to immutable update
state . counter ++ ;
},
},
});
Behind the scenes, Immer:
Creates a draft state
Tracks your “mutations”
Produces new immutable state
You get clean code AND immutability!
Class-Based Components
While the course focuses on hooks, Redux also works with class components:
import { Component } from 'react' ;
import { connect } from 'react-redux' ;
class Counter extends Component {
incrementHandler () {
this . props . increment ();
}
render () {
return (
< div >
< div > { this . props . counter } </ div >
< button onClick = { this . incrementHandler . bind ( this ) } >
Increment
</ button >
</ div >
);
}
}
const mapStateToProps = ( state ) => {
return {
counter: state . counter . counter ,
};
};
const mapDispatchToProps = ( dispatch ) => {
return {
increment : () => dispatch ({ type: 'counter/increment' }),
};
};
export default connect ( mapStateToProps , mapDispatchToProps )( Counter ) ;
Modern React apps should use hooks (useSelector and useDispatch) instead of connect. The connect HOC is legacy but still supported.
Redux Toolkit automatically enables Redux DevTools:
Install Extension
Install Redux DevTools browser extension
View State
Inspect current state structure and values
Track Actions
See all dispatched actions in order
Time Travel
Jump to any previous state
Debug Issues
Understand state changes and find bugs
Best Practices
Organize by Feature Group related slices, components, and logic together
Normalize State Store data in flat structure, not deeply nested
Use Redux Toolkit Avoid hand-written Redux - use RTK always
Selective Selectors Select only the state you need to minimize re-renders
Action Naming Use clear, descriptive action names
Keep Logic in Reducers Reducers contain business logic, components stay simple
Common Patterns
const dataSlice = createSlice ({
name: 'data' ,
initialState: {
items: [],
loading: false ,
error: null ,
},
reducers: {
fetchStart ( state ) {
state . loading = true ;
state . error = null ;
},
fetchSuccess ( state , action ) {
state . loading = false ;
state . items = action . payload ;
},
fetchError ( state , action ) {
state . loading = false ;
state . error = action . payload ;
},
},
});
reducers : {
toggleSidebar ( state ) {
state . sidebarOpen = ! state . sidebarOpen ;
},
openModal ( state ) {
state . modalOpen = true ;
},
closeModal ( state ) {
state . modalOpen = false ;
},
}
reducers : {
addItem ( state , action ) {
state . items . push ( action . payload );
},
removeItem ( state , action ) {
state . items = state . items . filter (
( item ) => item . id !== action . payload
);
},
updateItem ( state , action ) {
const index = state . items . findIndex (
( item ) => item . id === action . payload . id
);
if ( index !== - 1 ) {
state . items [ index ] = action . payload ;
}
},
}
Resources
Redux Toolkit Docs Official Redux Toolkit documentation
React-Redux Hooks useSelector and useDispatch API reference