Understanding React-Redux Hooks
React-Redux hooks provide a more direct and elegant way to connect your React components to Redux, eliminating the need for connect() and HOCs (Higher-Order Components).
🎣 The Fishing Analogy
Think of Redux hooks as fishing tools:
- useSelector: Your fishing rod to catch data from the Redux ocean
- useDispatch: Your casting mechanism to send actions into the water
- Selector Functions: Your fishing lure - determines what you catch
- Memoization: Your fishing net - prevents catching the same fish twice
Just as different fishing techniques catch different fish, different selector patterns retrieve different data shapes.
useSelector Hook In-Depth
graph TD
A[useSelector] --> B[Selector Function]
B --> C[Redux Store State]
C --> D[Selected Data]
D --> E[Component Re-render]
E --> F{Data Changed?}
F -->|Yes| G[Update UI]
F -->|No| H[Skip Update]
style A fill:#f96
style B fill:#9cf
style F fill:#ff9
Basic Usage
import React from 'react';
import { useSelector } from 'react-redux';
// Simple selector
const UserProfile = () => {
const user = useSelector(state => state.user);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
// Multiple selectors
const Dashboard = () => {
const user = useSelector(state => state.user);
const notifications = useSelector(state => state.notifications);
const isLoading = useSelector(state => state.ui.isLoading);
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Welcome, {user.name}</h1>
<NotificationList items={notifications} />
</div>
);
};
Advanced Selector Patterns
// Computed values
const CartSummary = () => {
const totalItems = useSelector(state =>
state.cart.items.reduce((sum, item) => sum + item.quantity, 0)
);
const totalPrice = useSelector(state =>
state.cart.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
);
const discount = useSelector(state => state.cart.discount);
const finalPrice = totalPrice - discount;
return (
<div>
<p>Items: {totalItems}</p>
<p>Total: ${finalPrice}</p>
</div>
);
};
// Selector with props
const TodoItem = ({ todoId }) => {
const todo = useSelector(state =>
state.todos.find(todo => todo.id === todoId)
);
if (!todo) return null;
return (
<div>
<input type="checkbox" checked={todo.completed} readOnly />
<span>{todo.text}</span>
</div>
);
};
// Selector factory pattern
const makeSelectTodoById = (todoId) => (state) =>
state.todos.find(todo => todo.id === todoId);
const TodoItemOptimized = ({ todoId }) => {
// Create selector once for this component instance
const selectTodoById = useMemo(
() => makeSelectTodoById(todoId),
[todoId]
);
const todo = useSelector(selectTodoById);
return todo ? <div>{todo.text}</div> : null;
};
// Multiple related values (be careful!)
const UserDashboard = () => {
// ⚠️ This creates a new object every render
const data = useSelector(state => ({
user: state.user,
posts: state.posts,
comments: state.comments
}));
// ✅ Better: use shallowEqual
const data = useSelector(state => ({
user: state.user,
posts: state.posts,
comments: state.comments
}), shallowEqual);
// ✅ Best: separate selectors
const user = useSelector(state => state.user);
const posts = useSelector(state => state.posts);
const comments = useSelector(state => state.comments);
};
Selector Performance Optimization
Using Reselect
import { createSelector } from 'reselect';
// Input selectors
const selectCart = state => state.cart;
const selectItems = state => state.cart.items;
const selectTaxRate = state => state.config.taxRate;
// Memoized selector
const selectCartSummary = createSelector(
[selectItems, selectTaxRate],
(items, taxRate) => {
const subtotal = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const tax = subtotal * taxRate;
const total = subtotal + tax;
return { subtotal, tax, total };
}
);
// Component using memoized selector
const CartSummary = () => {
const { subtotal, tax, total } = useSelector(selectCartSummary);
return (
<div>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>
</div>
);
};
// Parameterized selectors
const selectFilteredTodos = createSelector(
[
state => state.todos,
(state, filter) => filter
],
(todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
return todos;
}
}
);
// Using parameterized selector
const TodoList = ({ filter }) => {
const filteredTodos = useSelector(state =>
selectFilteredTodos(state, filter)
);
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
};
useDispatch Hook In-Depth
graph LR
A[Component] --> B[useDispatch]
B --> C[dispatch function]
C --> D[Action]
D --> E[Redux Store]
E --> F[Reducers]
F --> G[New State]
style B fill:#f96
style C fill:#9cf
style E fill:#9f9
Basic Usage
import React from 'react';
import { useDispatch } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo } from './todoActions';
const TodoControls = () => {
const dispatch = useDispatch();
// Direct dispatch
const handleClick = () => {
dispatch({ type: 'INCREMENT' });
};
// Dispatch action creators
const handleAddTodo = (text) => {
dispatch(addTodo(text));
};
// Dispatch with payload
const handleToggleTodo = (id) => {
dispatch(toggleTodo(id));
};
return (
<div>
<button onClick={() => handleAddTodo('New Todo')}>
Add Todo
</button>
</div>
);
};
Advanced Dispatch Patterns
// Async actions with Thunk
const DataLoader = () => {
const dispatch = useDispatch();
const data = useSelector(state => state.data);
const isLoading = useSelector(state => state.isLoading);
const error = useSelector(state => state.error);
const fetchData = async () => {
dispatch({ type: 'FETCH_DATA_START' });
try {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_DATA_FAILURE', payload: error.message });
}
};
// With thunk action creator
const fetchDataThunk = () => async (dispatch) => {
dispatch({ type: 'FETCH_DATA_START' });
try {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_DATA_FAILURE', payload: error.message });
}
};
const loadData = () => {
dispatch(fetchDataThunk());
};
return (
<div>
<button onClick={loadData}>Load Data</button>
{isLoading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
{data && <DataDisplay data={data} />}
</div>
);
};
// Memoized callbacks
const TodoForm = () => {
const dispatch = useDispatch();
const [text, setText] = useState('');
// Memoize dispatch callback
const handleSubmit = useCallback((e) => {
e.preventDefault();
if (text.trim()) {
dispatch(addTodo(text));
setText('');
}
}, [dispatch, text]);
// Stable reference (doesn't depend on changing values)
const handleClear = useCallback(() => {
dispatch({ type: 'CLEAR_TODOS' });
}, [dispatch]);
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add todo"
/>
<button type="submit">Add</button>
<button type="button" onClick={handleClear}>
Clear All
</button>
</form>
);
};
Custom Hooks with Redux
// Custom hook for authentication
const useAuth = () => {
const user = useSelector(state => state.auth.user);
const isAuthenticated = useSelector(state => state.auth.isAuthenticated);
const isLoading = useSelector(state => state.auth.isLoading);
const error = useSelector(state => state.auth.error);
const dispatch = useDispatch();
const login = useCallback((credentials) => {
return dispatch(authActions.login(credentials));
}, [dispatch]);
const logout = useCallback(() => {
dispatch(authActions.logout());
}, [dispatch]);
const register = useCallback((userData) => {
return dispatch(authActions.register(userData));
}, [dispatch]);
return {
user,
isAuthenticated,
isLoading,
error,
login,
logout,
register
};
};
// Usage
const LoginForm = () => {
const { login, isLoading, error } = useAuth();
const [credentials, setCredentials] = useState({
email: '',
password: ''
});
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login(credentials);
// Navigate to dashboard on success
} catch (err) {
// Error handling
}
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
</form>
);
};
// Custom hook for data fetching
const useReduxData = (endpoint, selector) => {
const data = useSelector(selector);
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(endpoint);
const result = await response.json();
dispatch({ type: 'SET_DATA', payload: result });
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [endpoint, dispatch]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
};
// Custom hook for form management
const useReduxForm = (formName, initialValues = {}) => {
const formState = useSelector(state => state.forms[formName]);
const dispatch = useDispatch();
const updateField = useCallback((field, value) => {
dispatch({
type: 'UPDATE_FORM_FIELD',
payload: { formName, field, value }
});
}, [dispatch, formName]);
const submitForm = useCallback((onSubmit) => {
dispatch({ type: 'SUBMIT_FORM_START', payload: formName });
return onSubmit(formState.values)
.then(result => {
dispatch({
type: 'SUBMIT_FORM_SUCCESS',
payload: { formName, result }
});
return result;
})
.catch(error => {
dispatch({
type: 'SUBMIT_FORM_FAILURE',
payload: { formName, error }
});
throw error;
});
}, [dispatch, formName, formState]);
const resetForm = useCallback(() => {
dispatch({
type: 'RESET_FORM',
payload: { formName, initialValues }
});
}, [dispatch, formName, initialValues]);
return {
values: formState?.values || initialValues,
errors: formState?.errors || {},
isSubmitting: formState?.isSubmitting || false,
updateField,
submitForm,
resetForm
};
};
Real-World Example: Post Management System
// Selectors
const selectPosts = state => state.posts.items;
const selectPostsLoading = state => state.posts.loading;
const selectPostsError = state => state.posts.error;
const selectCurrentFilter = state => state.posts.filter;
const selectFilteredPosts = createSelector(
[selectPosts, selectCurrentFilter],
(posts, filter) => {
if (!filter) return posts;
return posts.filter(post => {
switch (filter.type) {
case 'CATEGORY':
return post.category === filter.value;
case 'AUTHOR':
return post.authorId === filter.value;
case 'DATE_RANGE':
const postDate = new Date(post.createdAt);
return postDate >= filter.startDate && postDate <= filter.endDate;
default:
return true;
}
});
}
);
// Custom hook for posts
const usePosts = () => {
const posts = useSelector(selectFilteredPosts);
const loading = useSelector(selectPostsLoading);
const error = useSelector(selectPostsError);
const dispatch = useDispatch();
const fetchPosts = useCallback((params = {}) => {
dispatch(postsActions.fetchPosts(params));
}, [dispatch]);
const createPost = useCallback((postData) => {
return dispatch(postsActions.createPost(postData));
}, [dispatch]);
const updatePost = useCallback((id, updates) => {
return dispatch(postsActions.updatePost(id, updates));
}, [dispatch]);
const deletePost = useCallback((id) => {
return dispatch(postsActions.deletePost(id));
}, [dispatch]);
const setFilter = useCallback((filterType, value) => {
dispatch(postsActions.setFilter({ type: filterType, value }));
}, [dispatch]);
return {
posts,
loading,
error,
fetchPosts,
createPost,
updatePost,
deletePost,
setFilter
};
};
// Post list component
const PostList = () => {
const {
posts,
loading,
error,
fetchPosts,
deletePost,
setFilter
} = usePosts();
useEffect(() => {
fetchPosts();
}, [fetchPosts]);
const handleDelete = async (postId) => {
if (window.confirm('Are you sure?')) {
try {
await deletePost(postId);
} catch (err) {
console.error('Failed to delete post:', err);
}
}
};
const handleFilterChange = (e) => {
const [type, value] = e.target.value.split(':');
setFilter(type, value);
};
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return (
<div>
<div className="filters">
<select onChange={handleFilterChange}>
<option value="">All Posts</option>
<option value="CATEGORY:tech">Tech</option>
<option value="CATEGORY:news">News</option>
<option value="AUTHOR:1">John's Posts</option>
</select>
</div>
<div className="post-list">
{posts.map(post => (
<PostCard
key={post.id}
post={post}
onDelete={handleDelete}
/>
))}
</div>
</div>
);
};
// Post editor with form management
const PostEditor = ({ postId }) => {
const dispatch = useDispatch();
const post = useSelector(state =>
state.posts.items.find(p => p.id === postId)
);
const [formData, setFormData] = useState(post || {
title: '',
content: '',
category: '',
tags: []
});
const [isSaving, setIsSaving] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSaving(true);
try {
if (postId) {
await dispatch(postsActions.updatePost(postId, formData));
} else {
await dispatch(postsActions.createPost(formData));
}
// Navigate to post list or show success message
} catch (error) {
console.error('Failed to save post:', error);
} finally {
setIsSaving(false);
}
};
const handleChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.title}
onChange={handleChange('title')}
placeholder="Post Title"
/>
<textarea
value={formData.content}
onChange={handleChange('content')}
placeholder="Post Content"
/>
<select
value={formData.category}
onChange={handleChange('category')}
>
<option value="">Select Category</option>
<option value="tech">Tech</option>
<option value="news">News</option>
<option value="tutorial">Tutorial</option>
</select>
<button type="submit" disabled={isSaving}>
{isSaving ? 'Saving...' : (postId ? 'Update' : 'Create')} Post
</button>
</form>
);
};
Common Pitfalls and Solutions
Pitfall 1: Selector Recreation
// ❌ Problem: Creates new function every render
const BadComponent = () => {
const data = useSelector(state =>
state.items.filter(item => item.active)
);
};
// ✅ Solution: Memoize selector
const selectActiveItems = createSelector(
state => state.items,
items => items.filter(item => item.active)
);
const GoodComponent = () => {
const data = useSelector(selectActiveItems);
};
Pitfall 2: Unnecessary Re-renders
// ❌ Problem: New object reference every time
const BadComponent = () => {
const data = useSelector(state => ({
user: state.user,
posts: state.posts
}));
};
// ✅ Solution 1: Multiple selectors
const GoodComponent1 = () => {
const user = useSelector(state => state.user);
const posts = useSelector(state => state.posts);
};
// ✅ Solution 2: Use shallowEqual
import { shallowEqual } from 'react-redux';
const GoodComponent2 = () => {
const data = useSelector(state => ({
user: state.user,
posts: state.posts
}), shallowEqual);
};
Pitfall 3: Dispatch Function Dependencies
// ❌ Problem: Dispatch in useEffect dependency
const BadComponent = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchData());
}, [dispatch]); // dispatch is stable, unnecessary dependency
};
// ✅ Solution: Remove from dependencies
const GoodComponent = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchData());
}, []); // dispatch is stable, no need to include
};
Practice Exercise
Task: Create a Comment System
Build a comment system with Redux hooks that includes:
- Display comments for a post
- Add new comments
- Edit existing comments
- Delete comments
- Like/dislike comments
- Thread nested replies
// Create these custom hooks:
// 1. useComments hook
const useComments = (postId) => {
// TODO: Select comments for post
// TODO: Implement CRUD operations
// TODO: Handle loading/error states
};
// 2. useCommentActions hook
const useCommentActions = () => {
// TODO: Create action dispatchers
// TODO: Handle optimistic updates
// TODO: Error recovery
};
// 3. useCommentThread hook
const useCommentThread = (parentCommentId) => {
// TODO: Select nested replies
// TODO: Manage thread expansion state
// TODO: Handle reply submission
};
// Component structure to implement:
const CommentSection = ({ postId }) => {
// TODO: Use custom hooks
// TODO: Render comment tree
// TODO: Handle user interactions
};