100 Redux practice problems with solutions

Here are 100 Redux practice problems with solutions, covering core Redux, React-Redux bindings, middleware, Redux Toolkit, and advanced patterns. Each problem includes a concise code solution.

Try it: 100 Javascript practice problems with solutions

Section 1: Core Redux (Store, Reducers, Actions)

1. Create a basic Redux store
Create a store with a reducer that returns an initial state { count: 0 }.

js

// store.js
import { createStore } from 'redux';
const reducer = (state = { count: 0 }, action) => state;
export const store = createStore(reducer);

2. Define an action type and action creator
Define a constant INCREMENT and an action creator increment() that returns { type: INCREMENT }.

js

// actionTypes.js
export const INCREMENT = 'INCREMENT';
// actions.js
import { INCREMENT } from './actionTypes';
export const increment = () => ({ type: INCREMENT });

3. Handle action in reducer
Modify reducer to handle INCREMENT by increasing count by 1.

js

const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    default:
      return state;
  }
};

4. Dispatch action from store
Dispatching increment() from store and log state before and after.

js

import { increment } from './actions';
console.log(store.getState()); // { count: 0 }
store.dispatch(increment());
console.log(store.getState()); // { count: 1 }

5. Subscribe to store changes
Subscribe to the store and log every new state.

js

const unsubscribe = store.subscribe(() => {
  console.log('State updated:', store.getState());
});
// later: unsubscribe();

6. Create a decrement action
Add action type DECREMENT and corresponding reducer logic.

js

// actions.js
export const decrement = () => ({ type: 'DECREMENT' });
// reducer
case 'DECREMENT':
  return { count: state.count - 1 };

7. Reset count to zero
Create RESET action that sets count to 0.

js

export const reset = () => ({ type: 'RESET' });
// reducer
case 'RESET':
  return { count: 0 };

8. Pass payload in increment
Modify increment to accept an amount and add it to count.

js

export const incrementBy = (amount) => ({ type: 'INCREMENT_BY', payload: amount });
// reducer
case 'INCREMENT_BY':
  return { count: state.count + action.payload };

9. Todo list: add item
State { items: [] } → ADD_TODO adds item to array.

js

const initial = { items: [] };
const reducer = (state = initial, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return { ...state, items: [...state.items, action.payload] };
    default: return state;
  }
};
const addTodo = (text) => ({ type: 'ADD_TODO', payload: { id: Date.now(), text, completed: false } });

10. Todo list: toggle todo
Add TOGGLE_TODO action that toggles completed by id.

js

case 'TOGGLE_TODO':
  return {
    ...state,
    items: state.items.map(item =>
      item.id === action.payload ? { ...item, completed: !item.completed } : item
    )
  };
const toggleTodo = (id) => ({ type: 'TOGGLE_TODO', payload: id });

11. Combine multiple reducers
Create counterReducer and todoReducer, then combine with combineReducers.

js

import { combineReducers } from 'redux';
const rootReducer = combineReducers({
  counter: counterReducer,
  todos: todoReducer,
});
export const store = createStore(rootReducer);

12. Use combineReducers with different state keys
State shape: { counter: { count:0 }, todos: { items:[] } }. Access state.counter.count.

13. Add a loading flag
In the reducer, handle SET_LOADING action to toggle isLoading boolean.

js

case 'SET_LOADING':
  return { ...state, isLoading: action.payload };

14. Use createStore with preloaded state
Preload state from session storage or defaults.

js

const preloadedState = { counter: { count: 5 } };
const store = createStore(rootReducer, preloadedState);

15. Replace reducer dynamically
Use store.replaceReducer(nextReducer) to update reducer logic without losing state.

Section 2: React-Redux Binding (connect & hooks)

16. Provide store to React app
Wrap <App> with <Provider store={store}> from react-redux.

jsx

import { Provider } from 'react-redux';
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

17. Access count with useSelector
Functional component reads count from Redux.

jsx

import { useSelector } from 'react-redux';
const CounterDisplay = () => {
  const count = useSelector(state => state.counter.count);
  return <h1>{count}</h1>;
};

18. Dispatch with useDispatch

jsx

import { useDispatch } from 'react-redux';
const IncrementButton = () => {
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(increment())}>+</button>;
};

19. Component using both hooks

jsx

const Counter = () => {
  const count = useSelector(state => state.counter.count);
  const dispatch = useDispatch();
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
};

20. Connect with mapStateToProps (class component)

jsx

import { connect } from 'react-redux';
const mapStateToProps = (state) => ({ count: state.counter.count });
export default connect(mapStateToProps)(CounterClass);

21. Connect with mapDispatchToProps as object

jsx

import { increment } from './actions';
const mapDispatchToProps = { increment };
export default connect(null, mapDispatchToProps)(Button);

Try it: 100 React Native practice problems with solutions

22. Connect with mapDispatchToProps as function

jsx

const mapDispatchToProps = (dispatch) => ({
  onIncrement: () => dispatch(increment()),
});

23. Combine mapStateToProps and mapDispatchToProps

jsx

const CounterContainer = connect(
  (state) => ({ count: state.counter.count }),
  { increment, decrement }
)(Counter);

24. Use useSelector with equality check

jsx

const selectItems = (state) => state.todos.items;
const items = useSelector(selectItems, shallowEqual);

25. Use useSelector with memoized selector
Pass a selector created by reselect.

jsx

import { createSelector } from 'reselect';
const selectCompleted = createSelector(
  [(state) => state.todos.items],
  (items) => items.filter(item => item.completed)
);
// in component:
const completed = useSelector(selectCompleted);

26. Using useDispatch with action creators

jsx

const dispatch = useDispatch();
const add = useCallback((text) => dispatch(addTodo(text)), [dispatch]);

27. Show loading spinner based on Redux state

jsx

const isLoading = useSelector(state => state.ui.isLoading);
return isLoading ? <Spinner /> : <Content />;

28. Using useSelector to get array length

jsx

const itemCount = useSelector(state => state.todos.items.length);

29. Conditional rendering from Redux state

jsx

const error = useSelector(state => state.ui.error);
if (error) return <ErrorMessage message={error} />;

30. Re-render performance: use useSelector per field

jsx

const count = useSelector(state => state.counter.count);
const todos = useSelector(state => state.todos.items);

31. Using useDispatch in a custom hook

js

export const useCounterActions = () => {
  const dispatch = useDispatch();
  return {
    increment: () => dispatch(increment()),
    decrement: () => dispatch(decrement()),
  };
};

32. Handling input changes in Redux form

jsx

const TextInput = () => {
  const value = useSelector(state => state.form.text);
  const dispatch = useDispatch();
  return (
    <input value={value} onChange={e => dispatch(setText(e.target.value))} />
  );
};

33. Using useSelector with filter by id

jsx

const todo = useSelector(state => state.todos.items.find(t => t.id === todoId));

34. Avoiding unneeded re-renders by extracting child

jsx

const CountDisplay = () => { const count = useSelector(state => state.counter.count); return <span>{count}</span>; };

35. Use connect with forwardRef

jsx

const ConnectedComponent = connect(mapState)(React.forwardRef(MyComponent));

Section 3: Middleware & Async (Thunks)

36. Apply thunk middleware

js

import thunk from 'redux-thunk';
import { applyMiddleware, createStore } from 'redux';
const store = createStore(rootReducer, applyMiddleware(thunk));

37. Simple thunk action creator

js

export const fetchUsers = () => async (dispatch) => {
  dispatch({ type: 'FETCH_USERS_REQUEST' });
  try {
    const response = await fetch('/api/users');
    const users = await response.json();
    dispatch({ type: 'FETCH_USERS_SUCCESS', payload: users });
  } catch (error) {
    dispatch({ type: 'FETCH_USERS_FAILURE', error });
  }
};

38. Dispatch thunk from component

jsx

useEffect(() => {
  dispatch(fetchUsers());
}, [dispatch]);

39. Async request with loading and error states in reducer

js

case 'FETCH_USERS_REQUEST':
  return { ...state, loading: true, error: null };
case 'FETCH_USERS_SUCCESS':
  return { ...state, loading: false, users: action.payload };
case 'FETCH_USERS_FAILURE':
  return { ...state, loading: false, error: action.error };

40. Thunk that conditionally avoids fetch

js

export const fetchIfNeeded = () => (dispatch, getState) => {
  const { lastUpdated } = getState().users;
  if (Date.now() - lastUpdated < 5 * 60000) return;
  dispatch(fetchUsers());
};

41. Using redux-thunk with async/await
As shown, standard pattern.

42. Logging middleware (custom)

js

const logger = store => next => action => {
  console.log('dispatching', action);
  const result = next(action);
  console.log('next state', store.getState());
  return result;
};
applyMiddleware(logger);

43. Crash reporter middleware

js

const crashReporter = store => next => action => {
  try {
    return next(action);
  } catch (err) {
    console.error('Caught an exception!', err);
    throw err;
  }
};

44. Thunk with custom argument

js

const store = createStore(rootReducer, applyMiddleware(thunk.withExtraArgument(api)));
// action creator:
const fetchData = () => (dispatch, getState, api) => { ... };

45. Polling with setInterval in thunk

js

export const startPolling = () => (dispatch) => {
  const interval = setInterval(() => dispatch(fetchData()), 10000);
  dispatch({ type: 'POLLING_START', interval });
};

46. Debounce action with custom middleware
Middleware that debounces actions of a type.

Try it: 100 ReactJS practice problems with solutions

47. Redux Saga: simplest saga
Requires redux-saga. Example saga watching for action.

js

import { takeEvery, put } from 'redux-saga/effects';
function* handleIncrementAsync() {
  yield delay(1000);
  yield put({ type: 'INCREMENT' });
}
function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', handleIncrementAsync);
}

48. Apply redux-saga middleware

js

import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);

49. Multiple async requests with Promise.all in thunk

js

export const fetchAll = () => async (dispatch) => {
  const [users, posts] = await Promise.all([
    fetch('/users').then(r => r.json()),
    fetch('/posts').then(r => r.json()),
  ]);
  dispatch({ type: 'SET_USERS', payload: users });
  dispatch({ type: 'SET_POSTS', payload: posts });
};

50. Cancel async request with AbortController in thunk

js

export const fetchUser = (id) => async (dispatch) => {
  const abortController = new AbortController();
  dispatch({ type: 'FETCH_START', signal: abortController.signal });
  try {
    const res = await fetch(`/users/${id}`, { signal: abortController.signal });
    // ...
  } catch (err) { if (err.name !== 'AbortError') dispatch({ type: 'FETCH_ERROR', error: err }); }
  return () => abortController.abort();
};

51. Normalize data after fetch

js

import { normalize, schema } from 'normalizr';
const userSchema = new schema.Entity('users');
// inside thunk:
const normalized = normalize(response, [userSchema]);
dispatch({ type: 'SET_USERS', payload: normalized.entities });

52. Real-time with WebSockets – middleware
Middleware that listens to socket messages and dispatches actions.

53. Thunk with retry logic

js

export const fetchWithRetry = (retries = 3) => async (dispatch) => {
  for (let i = 0; i < retries; i++) {
    try {
      return await dispatch(fetchData());
    } catch (err) {
      if (i === retries - 1) dispatch({ type: 'FETCH_FAILED', error: err });
    }
  }
};

54. Undo/Redo middleware
Custom middleware that stores past states in a stack and allows undo/redo actions.

55. Combining multiple middleware (thunk, logger)

js

applyMiddleware(thunk, logger)

Section 4: Redux Toolkit (RTK)

56. Create a slice with createSlice

js

import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => { state.count += 1; },
    decrement: (state) => { state.count -= 1; },
    incrementBy: (state, action) => { state.count += action.payload; },
  },
});
export const { increment, decrement, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;

57. Configure store with configureStore

js

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
  reducer: { counter: counterReducer },
});

58. Use createAsyncThunk for API call

js

import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId, thunkAPI) => {
    const response = await fetch(`/users/${userId}`);
    return response.json();
  }
);

59. Extra reducers for async thunk

js

const userSlice = createSlice({
  name: 'users',
  initialState: { data: null, status: 'idle' },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchUserById.fulfilled, (state, action) => { state.status = 'succeeded'; state.data = action.payload; })
      .addCase(fetchUserById.rejected, (state) => { state.status = 'failed'; });
  },
});

60. Use createEntityAdapter for normalized CRUD

js

import { createEntityAdapter } from '@reduxjs/toolkit';
const usersAdapter = createEntityAdapter();
const initialState = usersAdapter.getInitialState();
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    addUser: usersAdapter.addOne,
    removeUser: usersAdapter.removeOne,
    updateUser: usersAdapter.updateOne,
  },
});
export const selectors = usersAdapter.getSelectors(state => state.users);

61. Create multiple slices and combine with combineReducers (RTK style)

js

const store = configureStore({
  reducer: {
    counter: counterReducer,
    todos: todosReducer,
    users: usersReducer,
  },
});

62. Use configureStore with middleware (default includes thunk)

js

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(loggerMiddleware),
});

63. RTK Query: createApi

js

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUsers: builder.query({ query: () => 'users' }),
  }),
});
export const { useGetUsersQuery } = api;

64. Add RTK Query reducer and middleware

js

const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (gdm) => gdm().concat(api.middleware),
});

65. Using auto-generated hooks

jsx

const { data, error, isLoading } = useGetUsersQuery();

66. Memoized selector with createSelector (from RTK’s reselect)

js

import { createSelector } from '@reduxjs/toolkit';
const selectAllTodos = state => state.todos.entities;
const selectCompleted = createSelector(selectAllTodos, todos => todos.filter(t => t.completed));

67. Reusing action from another slice

js

import { increment } from '../counter/counterSlice';
// inside another slice's reducers: use case on that action
extraReducers: (builder) => {
  builder.addCase(increment, (state) => { state.someProp = ...; });
}

68. Using createAction with prepare callback

js

import { createAction } from '@reduxjs/toolkit';
const addTodo = createAction('todos/add', function prepare(text) {
  return { payload: { id: nanoid(), text, completed: false } };
});

69. Normalized state with createEntityAdapter selectors

js

const selectTotal = usersAdapter.getSelectors(state => state.users).selectTotal;
const count = useSelector(selectTotal);

70. Using builder.addDefaultCase for unhandled actions

js

extraReducers: (builder) => {
  builder.addDefaultCase((state) => state);
}

71. Write synchronous thunk using createAsyncThunk? Not recommended, but can do async () => value

72. Use createReducer standalone

js

const counterReducer = createReducer({ count: 0 }, {
  increment: (state) => { state.count++ },
});

73. Configure store with devtools (enabled by default in RTK)

74. Use prepareHeaders in fetchBaseQuery for auth

js

const baseQuery = fetchBaseQuery({
  baseUrl: '/api',
  prepareHeaders: (headers, { getState }) => {
    headers.set('authorization', `Bearer ${getState().auth.token}`);
    return headers;
  },
});

75. Provide store to App with RTK
Same as usual Provider.

Try it: 100 Angular practice problems with solutions

Section 5: Advanced Patterns & Testing

76. Write a selector that returns count value squared

js

const selectCountSquared = createSelector(
  [(state) => state.counter.count],
  (count) => count * count
);

77. Using reselect with dynamic argument

js

const selectTodoById = createSelector(
  [(state) => state.todos.entities, (_, id) => id],
  (entities, id) => entities[id]
);

78. Memoize all selectors in a file

79. Persist state with redux-persist

js

import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
const persistConfig = { key: 'root', storage };
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(persistedReducer);
const persistor = persistStore(store);

80. Write a reducer test

js

test('counter increment', () => {
  const state = counterReducer(undefined, { type: 'counter/increment' });
  expect(state.count).toBe(1);
});

81. Test an action creator

js

test('createTodo action', () => {
  const action = addTodo('Buy milk');
  expect(action.payload.text).toBe('Buy milk');
});

82. Test a thunk (fetchUsers) with mock fetch

js

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const mockStore = configureMockStore([thunk]);
test('fetchUsers success', async () => {
  global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([{id:1}]) }));
  const store = mockStore({});
  await store.dispatch(fetchUsers());
  const actions = store.getActions();
  expect(actions[0].type).toBe('FETCH_USERS_REQUEST');
});

83. Use createReducer with builder pattern for complex logic

js

const reducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state) => { ... })
    .addMatcher((action) => action.type.endsWith('/fulfilled'), (state) => { ... });
});

84. useSelector with shallowEqual from react-redux

js

import { useSelector, shallowEqual } from 'react-redux';
const props = useSelector((state) => ({ a: state.a, b: state.b }), shallowEqual);

85. React.lazy with Redux store: store is available in lazy components via normal hooks.

86. Use batch from react-redux for multiple dispatches

js

import { batch } from 'react-redux';
const save = () => batch(() => {
  dispatch(updateA());
  dispatch(updateB());
});

87. Custom hook: useAppDispatch and useAppSelector typed (TypeScript example)

ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

88. Handling form state in Redux: each keystroke dispatches action

89. Using redux-thunk with AbortController to cancel requests (demonstrated earlier)

90. Redux middleware that injects dependencies

js

const apiMiddleware = (api) => store => next => action => {
  if (typeof action === 'function') return action({ ...store, api });
  return next(action);
};

91. Implementing undo/redo with redux-undo library (conceptual)

js

import undoable from 'redux-undo';
const counterReducer = (state = 0, action) => ...;
const undoableReducer = undoable(counterReducer);

92. Use redux-saga with takeLatest

js

function* fetchOnChange(action) {
  const data = yield call(api.fetch, action.payload);
  yield put(fetchSuccess(data));
}
function* watchChange() {
  yield takeLatest('CHANGE', fetchOnChange);
}

93. Testing saga with redux-saga-test-plan

js

import { expectSaga } from 'redux-saga-test-plan';
it('works', () => {
  return expectSaga(watchChange)
    .dispatch({ type: 'CHANGE', payload: 1 })
    .put(fetchSuccess(mockData))
    .run();
});

94. Selector factory: createSelector that returns a memoized selector per id

js

const makeSelectTodoById = () => createSelector(
  [(state) => state.todos.entities, (_, id) => id],
  (entities, id) => entities[id]
);

95. Using useSelector with a computed value and useMemo outside
No, just use createSelector to avoid re‑computing.

96. Reset state on logout

js

const rootReducer = (state, action) => {
  if (action.type === 'LOGOUT') return appReducer(undefined, action);
  return appReducer(state, action);
};

97. Register dynamic reducers (code-split)

js

const store = createStore(createReducer({}));
store.asyncReducers = {};
store.injectReducer = (key, reducer) => {
  store.asyncReducers[key] = reducer;
  store.replaceReducer(createReducer(store.asyncReducers));
};

98. Using areStatesEqual in connect options for performance?

js

connect(mapState, mapDispatch, null, { areStatesEqual: (next, prev) => next === prev })(Component);

99. Custom middleware that prevents certain actions from being dispatched

js

const blockMiddleware = store => next => action => {
  if (action.type === 'FORBIDDEN') return;
  return next(action);
};

100. Full Redux-Toolkit setup with async thunk, entity adapter, and React component
Show a concise example integrating all concepts: slice, async thunk, selectors, component.

js

// slice.js
import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
const postsAdapter = createEntityAdapter();
export const fetchPosts = createAsyncThunk('posts/fetch', async () => fetch('/posts').then(r => r.json()));
const postsSlice = createSlice({
  name: 'posts',
  initialState: postsAdapter.getInitialState({ status: 'idle' }),
  reducers: {},
  extraReducers: builder => {
    builder.addCase(fetchPosts.fulfilled, (state, action) => {
      state.status = 'succeeded';
      postsAdapter.setAll(state, action.payload);
    });
  },
});
export const { selectAll } = postsAdapter.getSelectors(state => state.posts);
// Component
function PostsList() {
  const dispatch = useAppDispatch();
  const posts = useAppSelector(selectAll);
  useEffect(() => { dispatch(fetchPosts()); }, []);
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

Final Thought

You’ve reached the final problem — and breathe.

It’s okay if Redux still feels a little heavy some days. State management isn’t a simple topic, and the fact that you showed up for 100 problems proves something really important: you’re patient with yourself, and you’re growing at exactly the right pace.

Each action you dispatched, each reducer you shaped, every slice of store you touched — they quietly built a calm understanding that tutorials alone can’t give. And even on days when nothing clicked, you kept going. That quiet determination is worth more than any perfect score.

Be gentle with your learning journey. Bookmark this page like a cup of warm tea — ready whenever confusion creeps in. Redux doesn’t need to be mastered in a day; it needs to be understood little by little, with a kind and steady heart. You’re doing just that. I’m proud of you. Now take a rest, you’ve earned it.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top