Seamlessly integrate Redux with React using modern hooks and optimization techniques.
Seamlessly integrate Redux with React using modern hooks and optimization techniques.
React Redux provides hooks that make it easy to connect React components to the Redux store without the complexity of connect() HOC.
The useSelector hook allows you to extract data from the Redux store state.
1import { useSelector } from 'react-redux';
2
3function UserProfile() {
4 // Basic selector
5 const user = useSelector(state => state.user);
6
7 // Nested property selection
8 const userName = useSelector(state => state.user.profile.name);
9
10 // Multiple values
11 const { isLoading, error } = useSelector(state => ({
12 isLoading: state.user.loading,
13 error: state.user.error
14 }));
15
16 return (
17 <div>
18 <h1>{userName}</h1>
19 {isLoading && <p>Loading...</p>}
20 {error && <p>Error: {error}</p>}
21 </div>
22 );
23}The useDispatch hook returns a reference to the dispatch function.
1import { useDispatch } from 'react-redux';
2import { fetchUser, updateUser } from './userSlice';
3
4function UserActions({ userId }) {
5 const dispatch = useDispatch();
6
7 const handleFetchUser = () => {
8 dispatch(fetchUser(userId));
9 };
10
11 const handleUpdateUser = (userData) => {
12 dispatch(updateUser({ id: userId, ...userData }));
13 };
14
15 return (
16 <div>
17 <button onClick={handleFetchUser}>Fetch User</button>
18 <button onClick={() => handleUpdateUser({ name: 'New Name' })}>
19 Update User
20 </button>
21 </div>
22 );
23}1// store/hooks.ts
2import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
3import type { RootState, AppDispatch } from './store';
4
5export const useAppDispatch = () => useDispatch<AppDispatch>();
6export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
7
8// Usage in components
9function TypedComponent() {
10 const dispatch = useAppDispatch(); // Fully typed dispatch
11 const user = useAppSelector(state => state.user); // Fully typed state
12
13 const handleAction = () => {
14 dispatch(someAction()); // TypeScript knows available actions
15 };
16
17 return <div>{user.name}</div>;
18}1// store/store.ts
2export const store = configureStore({
3 reducer: {
4 user: userReducer,
5 posts: postsReducer,
6 ui: uiReducer
7 }
8});
9
10export type RootState = ReturnType<typeof store.getState>;
11export type AppDispatch = typeof store.dispatch;
12
13// Inferred types in components
14const userState = useAppSelector(state => state.user); // UserState type
15const allPosts = useAppSelector(state => state.posts.items); // Post[] type1import { createSelector } from '@reduxjs/toolkit';
2
3// Basic selectors
4const selectUsers = state => state.users.entities;
5const selectFilter = state => state.users.filter;
6
7// Memoized derived selector
8const selectFilteredUsers = createSelector(
9 [selectUsers, selectFilter],
10 (users, filter) => {
11 console.log('Recomputing filtered users'); // Only logs when inputs change
12
13 if (!filter) return Object.values(users);
14
15 return Object.values(users).filter(user =>
16 user.name.toLowerCase().includes(filter.toLowerCase())
17 );
18 }
19);
20
21// Component usage
22function UserList() {
23 const filteredUsers = useAppSelector(selectFilteredUsers);
24
25 return (
26 <ul>
27 {filteredUsers.map(user => (
28 <li key={user.id}>{user.name}</li>
29 ))}
30 </ul>
31 );
32}1// ❌ Creates new object on every render
2const badSelector = state => ({
3 users: state.users,
4 loading: state.loading
5});
6
7// ✅ Use separate selectors
8const users = useAppSelector(state => state.users);
9const loading = useAppSelector(state => state.loading);
10
11// ✅ Or use memoized selector
12const selectUsersAndLoading = createSelector(
13 [state => state.users, state => state.loading],
14 (users, loading) => ({ users, loading })
15);
16
17// ✅ Shallow equality for objects
18const { users, loading } = useAppSelector(selectUsersAndLoading, shallowEqual);1// Factory function for parametric selectors
2const makeSelectUserById = () => createSelector(
3 [state => state.users.entities, (state, id) => id],
4 (users, id) => users[id]
5);
6
7// Component with parametric selector
8function UserCard({ userId }) {
9 const selectUserById = useMemo(makeSelectUserById, []);
10 const user = useAppSelector(state => selectUserById(state, userId));
11
12 return <div>{user?.name}</div>;
13}
14
15// Alternative: Direct parameterized selection
16function UserCardAlt({ userId }) {
17 const user = useAppSelector(state => state.users.entities[userId]);
18 return <div>{user?.name}</div>;
19}1function useUserData(userId) {
2 const dispatch = useAppDispatch();
3 const { user, loading, error } = useAppSelector(state => ({
4 user: state.users.entities[userId],
5 loading: state.users.loading,
6 error: state.users.error
7 }));
8
9 useEffect(() => {
10 if (userId && !user && !loading) {
11 dispatch(fetchUser(userId));
12 }
13 }, [dispatch, userId, user, loading]);
14
15 const refetch = useCallback(() => {
16 dispatch(fetchUser(userId));
17 }, [dispatch, userId]);
18
19 return {
20 user,
21 loading,
22 error,
23 refetch
24 };
25}
26
27// Usage
28function UserProfile({ userId }) {
29 const { user, loading, error, refetch } = useUserData(userId);
30
31 if (loading) return <div>Loading...</div>;
32 if (error) return <div>Error: {error}</div>;
33 if (!user) return <div>User not found</div>;
34
35 return (
36 <div>
37 <h1>{user.name}</h1>
38 <button onClick={refetch}>Refresh</button>
39 </div>
40 );
41}1function useFormWithRedux(initialValues, validationSchema) {
2 const dispatch = useAppDispatch();
3 const [values, setValues] = useState(initialValues);
4 const [errors, setErrors] = useState({});
5 const [touched, setTouched] = useState({});
6
7 const isSubmitting = useAppSelector(state => state.forms.isSubmitting);
8 const serverErrors = useAppSelector(state => state.forms.errors);
9
10 const handleChange = useCallback((name, value) => {
11 setValues(prev => ({ ...prev, [name]: value }));
12
13 // Clear field error on change
14 if (errors[name]) {
15 setErrors(prev => ({ ...prev, [name]: null }));
16 }
17 }, [errors]);
18
19 const handleBlur = useCallback((name) => {
20 setTouched(prev => ({ ...prev, [name]: true }));
21
22 // Validate field on blur
23 if (validationSchema[name]) {
24 const error = validationSchema[name](values[name]);
25 setErrors(prev => ({ ...prev, [name]: error }));
26 }
27 }, [values, validationSchema]);
28
29 const handleSubmit = useCallback(async (onSubmit) => {
30 // Mark all fields as touched
31 setTouched(
32 Object.keys(initialValues).reduce(
33 (acc, key) => ({ ...acc, [key]: true }), {}
34 )
35 );
36
37 // Validate all fields
38 const validationErrors = Object.keys(validationSchema).reduce(
39 (acc, key) => {
40 const error = validationSchema[key](values[key]);
41 if (error) acc[key] = error;
42 return acc;
43 }, {}
44 );
45
46 setErrors(validationErrors);
47
48 if (Object.keys(validationErrors).length === 0) {
49 try {
50 await dispatch(onSubmit(values)).unwrap();
51 setValues(initialValues); // Reset on success
52 setTouched({});
53 } catch (error) {
54 // Server errors are handled in Redux
55 }
56 }
57 }, [values, initialValues, validationSchema, dispatch]);
58
59 return {
60 values,
61 errors: { ...errors, ...serverErrors },
62 touched,
63 isSubmitting,
64 handleChange,
65 handleBlur,
66 handleSubmit
67 };
68}1function withReduxState(mapStateToProps, mapDispatchToProps) {
2 return function (WrappedComponent) {
3 return function WithReduxStateComponent(props) {
4 const stateProps = useAppSelector(mapStateToProps);
5 const dispatch = useAppDispatch();
6
7 const dispatchProps = useMemo(() => {
8 return typeof mapDispatchToProps === 'function'
9 ? mapDispatchToProps(dispatch)
10 : bindActionCreators(mapDispatchToProps, dispatch);
11 }, [dispatch]);
12
13 return (
14 <WrappedComponent
15 {...props}
16 {...stateProps}
17 {...dispatchProps}
18 />
19 );
20 };
21 };
22}
23
24// Usage
25const mapStateToProps = state => ({
26 user: state.user,
27 isLoading: state.user.loading
28});
29
30const mapDispatchToProps = {
31 fetchUser,
32 updateUser
33};
34
35const EnhancedUserProfile = withReduxState(
36 mapStateToProps,
37 mapDispatchToProps
38)(UserProfile);1function ReduxStateProvider({ selector, children }) {
2 const state = useAppSelector(selector);
3 const dispatch = useAppDispatch();
4
5 return children({ state, dispatch });
6}
7
8// Usage
9<ReduxStateProvider selector={state => state.user}>
10 {({ state, dispatch }) => (
11 <div>
12 <h1>{state.name}</h1>
13 <button onClick={() => dispatch(fetchUser())}>
14 Refresh
15 </button>
16 </div>
17 )}
18</ReduxStateProvider>1const UserItem = React.memo(({ userId }) => {
2 const user = useAppSelector(state => state.users.entities[userId]);
3 const dispatch = useAppDispatch();
4
5 const handleUpdate = useCallback((data) => {
6 dispatch(updateUser({ id: userId, ...data }));
7 }, [dispatch, userId]);
8
9 return (
10 <div>
11 <h3>{user.name}</h3>
12 <button onClick={() => handleUpdate({ status: 'updated' })}>
13 Update
14 </button>
15 </div>
16 );
17});
18
19// List component that doesn't re-render items unnecessarily
20function UserList() {
21 const userIds = useAppSelector(state => state.users.ids);
22
23 return (
24 <div>
25 {userIds.map(userId => (
26 <UserItem key={userId} userId={userId} />
27 ))}
28 </div>
29 );
30}1import { batch } from 'react-redux';
2
3function useBatchedActions() {
4 const dispatch = useAppDispatch();
5
6 const batchedUpdate = useCallback((updates) => {
7 batch(() => {
8 updates.forEach(update => {
9 dispatch(update.action(update.payload));
10 });
11 });
12 }, [dispatch]);
13
14 return { batchedUpdate };
15}
16
17// Usage
18function BulkUpdateComponent() {
19 const { batchedUpdate } = useBatchedActions();
20
21 const handleBulkUpdate = () => {
22 batchedUpdate([
23 { action: updateUser, payload: { id: 1, name: 'John' } },
24 { action: updateUser, payload: { id: 2, name: 'Jane' } },
25 { action: setLoading, payload: false }
26 ]);
27 };
28
29 return <button onClick={handleBulkUpdate}>Bulk Update</button>;
30}1class ReduxErrorBoundary extends React.Component {
2 constructor(props) {
3 super(props);
4 this.state = { hasError: false };
5 }
6
7 static getDerivedStateFromError(error) {
8 return { hasError: true };
9 }
10
11 componentDidCatch(error, errorInfo) {
12 // Dispatch error to Redux store
13 this.props.dispatch(logError({
14 error: error.message,
15 errorInfo,
16 timestamp: Date.now()
17 }));
18 }
19
20 render() {
21 if (this.state.hasError) {
22 return (
23 <div className="error-boundary">
24 <h2>Something went wrong.</h2>
25 <button onClick={() => this.setState({ hasError: false })}>
26 Try again
27 </button>
28 </div>
29 );
30 }
31
32 return this.props.children;
33 }
34}
35
36// HOC version
37function withReduxErrorBoundary(Component) {
38 return function WithErrorBoundary(props) {
39 const dispatch = useAppDispatch();
40
41 return (
42 <ReduxErrorBoundary dispatch={dispatch}>
43 <Component {...props} />
44 </ReduxErrorBoundary>
45 );
46 };
47}1import { render, screen } from '@testing-library/react';
2import { Provider } from 'react-redux';
3import { configureStore } from '@reduxjs/toolkit';
4import userReducer from '../userSlice';
5import UserProfile from '../UserProfile';
6
7function renderWithRedux(component, initialState = {}) {
8 const store = configureStore({
9 reducer: {
10 user: userReducer
11 },
12 preloadedState: initialState
13 });
14
15 const Wrapper = ({ children }) => (
16 <Provider store={store}>{children}</Provider>
17 );
18
19 return {
20 ...render(component, { wrapper: Wrapper }),
21 store
22 };
23}
24
25describe('UserProfile', () => {
26 test('displays user name when user exists', () => {
27 const initialState = {
28 user: {
29 entities: {
30 1: { id: 1, name: 'John Doe', email: '[email protected]' }
31 },
32 loading: false,
33 error: null
34 }
35 };
36
37 renderWithRedux(<UserProfile userId={1} />, initialState);
38
39 expect(screen.getByText('John Doe')).toBeInTheDocument();
40 });
41
42 test('dispatches fetchUser when user not found', () => {
43 const { store } = renderWithRedux(<UserProfile userId={1} />, {
44 user: { entities: {}, loading: false, error: null }
45 });
46
47 // Check that fetchUser action was dispatched
48 const actions = store.getState();
49 // ... assertions
50 });
51});1import { renderHook, act } from '@testing-library/react';
2import { Provider } from 'react-redux';
3import { useUserData } from '../hooks/useUserData';
4
5function createWrapper(initialState = {}) {
6 const store = configureStore({
7 reducer: { user: userReducer },
8 preloadedState: initialState
9 });
10
11 return ({ children }) => (
12 <Provider store={store}>{children}</Provider>
13 );
14}
15
16describe('useUserData hook', () => {
17 test('fetches user data on mount', async () => {
18 const wrapper = createWrapper();
19
20 const { result, waitForNextUpdate } = renderHook(
21 () => useUserData(1),
22 { wrapper }
23 );
24
25 expect(result.current.loading).toBe(true);
26 expect(result.current.user).toBe(null);
27
28 await waitForNextUpdate();
29
30 expect(result.current.loading).toBe(false);
31 expect(result.current.user).toBeDefined();
32 });
33
34 test('refetch function works correctly', async () => {
35 const wrapper = createWrapper();
36
37 const { result } = renderHook(
38 () => useUserData(1),
39 { wrapper }
40 );
41
42 act(() => {
43 result.current.refetch();
44 });
45
46 expect(result.current.loading).toBe(true);
47 });
48});1import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2import { rest } from 'msw';
3import { setupServer } from 'msw/node';
4import { Provider } from 'react-redux';
5import { store } from '../store';
6import UserDashboard from '../UserDashboard';
7
8// Mock API server
9const server = setupServer(
10 rest.get('/api/users/:id', (req, res, ctx) => {
11 return res(
12 ctx.json({
13 id: parseInt(req.params.id),
14 name: 'Test User',
15 email: '[email protected]'
16 })
17 );
18 })
19);
20
21beforeAll(() => server.listen());
22afterEach(() => server.resetHandlers());
23afterAll(() => server.close());
24
25test('full user dashboard workflow', async () => {
26 render(
27 <Provider store={store}>
28 <UserDashboard userId={1} />
29 </Provider>
30 );
31
32 // Initially shows loading
33 expect(screen.getByText('Loading...')).toBeInTheDocument();
34
35 // Wait for user data to load
36 await waitFor(() => {
37 expect(screen.getByText('Test User')).toBeInTheDocument();
38 });
39
40 // Test user interaction
41 fireEvent.click(screen.getByText('Edit Profile'));
42
43 expect(screen.getByRole('textbox')).toBeInTheDocument();
44
45 // Test form submission
46 fireEvent.change(screen.getByRole('textbox'), {
47 target: { value: 'Updated Name' }
48 });
49
50 fireEvent.click(screen.getByText('Save'));
51
52 await waitFor(() => {
53 expect(screen.getByText('Updated Name')).toBeInTheDocument();
54 });
55});Your progress is saved in this browser, on this device only.