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.