useSelector and useDispatch Hooks Deep Dive

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
};
                

Additional Resources