The best way to learn React state management is to build one small task tracker and refactor it three times: local useState, lifted state with props, then context plus useReducer. Each step should be adopted only when the previous one starts to hurt, and the rubric below tells you how to judge that. The exercise set fits inside a 14-day internship and gives you a portfolio-ready repository and a short write-up you can show a mentor.
Why one task tracker teaches state management better than many demos
Counter apps and todo snippets hide the real problem: state management only matters when several components need the same data and several actions change it. A task tracker has just enough moving parts. It has a list of tasks, a filter, an add form, an edit mode, a counter in the header, and a “clear completed” button. Those pieces force decisions about where state lives and who may change it.
Keeping the same app for the whole exercise set lets you compare approaches fairly. The features never change; only the wiring does. By the end you will be able to explain, with your own code as evidence, why you picked one approach over another. That explanation is what interviewers and internship mentors actually probe.
The task tracker specification you will build against
Fix the feature list before writing code so the refactors stay honest.
- Add a task with a title and a priority (low, medium, high).
- Toggle a task complete or incomplete.
- Edit a task title inline and cancel the edit.
- Delete a single task.
- Filter by all, active, or completed.
- Show a header summary such as “3 of 8 done”.
- Clear all completed tasks.
A suggested component tree is App, Header, TaskForm, FilterBar, TaskList, and TaskItem. Use Vite with the React template, plain CSS, and no state library. Persistence in localStorage is a stretch goal added in Stage 5.
Free 2-Week CareerSwami Internship
Online, 14 days, with a free offer letter and a free completion certificate once you finish the work. Check with your college that it fits your requirement before you enrol.
Five-stage exercise set from useState to context and a reducer
Stage 1: Everything in one component with useState
Put the whole app in App first. Keep tasks, filter, and the form input in separate useState calls. Write handlers inline. This is deliberately messy; it shows you what a working baseline feels like.
- Use the functional update form,
setTasks(prev => ...), for every change based on the previous list. - Never mutate the array. Use
map,filter, and spread. - Derive the visible list and the “done” count during render instead of storing them in state.
Checkpoint: all seven features work, and you can state which values are state and which are derived.
Stage 2: Split into components and lift state up
Break the UI into the component tree above. Keep tasks and filter in App and pass values and callbacks down as props. Keep the add-form input and the inline edit text as local state inside TaskForm and TaskItem, because nobody else needs them.
- Note every prop that a component receives only to pass it on. Count them.
- Write one sentence on why the edit text does not belong in
App.
Checkpoint: a short note listing which state is local, which is lifted, and how many props are merely passed through.
Stage 3: Introduce a reducer with useReducer
Replace tasks and filter in App with one useReducer. Define actions as plain objects with a type and a payload. Keep the reducer a pure function in its own file.
// Illustrative sketch, not a tested implementation
function tasksReducer(state, action) {
switch (action.type) {
case 'added':
return { ...state, tasks: [...state.tasks, action.task] };
case 'toggled':
return {
...state,
tasks: state.tasks.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
),
};
case 'filter_set':
return { ...state, filter: action.filter };
case 'completed_cleared':
return { ...state, tasks: state.tasks.filter(t => !t.done) };
default:
throw new Error('Unknown action: ' + action.type);
}
}
- Name actions after what happened (“toggled”), not what to do (“setDone”).
- Write the reducer so it never generates IDs or reads the clock; create those before dispatching.
- Test the reducer as a plain function with a few input and output pairs in a small script or test file.
Checkpoint: all feature logic lives in the reducer, and components only call dispatch.
Stage 4: Add context to remove prop drilling
Create a TasksContext for the state and a separate TasksDispatchContext for dispatch. Wrap the tree in a provider component and write two small custom hooks, useTasks and useTasksDispatch. Now TaskItem and Header read what they need directly.
- Delete every pass-through prop you counted in Stage 2.
- Open React Developer Tools and observe which components re-render when you toggle one task.
- Make the hooks throw a clear error if used outside the provider.
Checkpoint: the component tree has no pass-through props, and you can describe what re-renders on a toggle.
Stage 5: Persistence, a derived selector, and a decision write-up
Save state to localStorage inside an effect and load it as the reducer’s initial state through the lazy initializer. Move the “visible tasks” calculation into a helper. Then write a half-page note, using the rubric below, on which stages of the app were justified and which were over-engineered.
Checkpoint: a refresh keeps your tasks, and your note names at least one place where a simpler approach would have been enough.
Rubric: when useState, lifting, useReducer, or context is justified
| Approach | Justified when | Warning sign you chose it too early | Task tracker example |
|---|---|---|---|
Local useState |
Only one component uses the value, and updates are simple. | You keep sending the value upward through callbacks. | Add-form input, inline edit text |
| Lifted state with props | Two or three nearby components share a value. | The shared state lives far above the components using it. | filter shared by FilterBar and TaskList |
useReducer |
Many update types touch related state, or the next state depends on complex rules. | You have one boolean and three actions. | The tasks array with add, toggle, edit, delete, clear |
| Context | Many components at different depths need the same data and you are passing props only to relay them. | The tree is two levels deep, or the value changes constantly. | Tasks and dispatch needed by Header and TaskItem |
| External library | You need shared state across routes, caching, or devtools beyond what React offers. | You cannot name the concrete pain it solves. | Not justified for this app |
A quick scoring checklist for each piece of state
For every value, ask these questions in order and stop at the first “yes”.
- Can it be computed from other state or props? Then derive it; do not store it.
- Does only one component use it? Keep it local.
- Do only close siblings share it? Lift it to their parent.
- Do many updates share complex rules? Use a reducer.
- Is it passed through components that do not use it? Consider context.
Worked illustration: deciding where the “done count” lives
This example is illustrative, not a record of a real project. Suppose you have stored doneCount as its own useState in App. Toggling a task needs two updates: change the task, then adjust the count. In a quick edit you forget to decrement the count when a completed task is deleted, so the header shows “4 of 7 done” when only three are done.
The fix is not a reducer or context. The count is derived data, so remove the state and compute it during render:
// Illustrative
const doneCount = tasks.filter(t => t.done).length;
The bug becomes impossible because there is only one source of truth. Only after this, if the update logic across add, edit, delete, and clear still feels scattered, does a reducer earn its place. In your write-up, record this as “removed redundant state” rather than “added a pattern”. That distinction shows judgement.
Common context and reducer slips in a small React app
- One giant context. Putting state and dispatch in a single value makes every consumer re-render on any change. Splitting them, as in Stage 4, is a cheap improvement.
- Impure reducers. Calling
Date.now(),Math.random(), orlocalStorageinside a reducer breaks predictability. - Mutating state. Using
pushor assigning totask.donemay appear to work but defeats change detection and causes odd bugs in Strict Mode. - Storing derived values. Visible tasks, counts, and “all done” flags should be computed.
- Using index as key. Use stable task IDs, or inline edit text will jump between rows after deletes.
A 14-day plan for the exercise set
| Days | Focus | Output |
|---|---|---|
| 1-2 | Setup, spec, Stage 1 baseline | Working single-component app in a Git repository |
| 3-4 | Stage 2 component split and lifting | Note on local versus lifted state |
| 5-7 | Stage 3 reducer and reducer tests | Pure reducer file with test cases |
| 8-9 | Stage 4 context and custom hooks | No pass-through props; re-render observations |
| 10-11 | Stage 5 persistence, selector, edge cases | Refresh-safe app |
| 12 | Rubric write-up | Half-page decision note |
| 13 | README, screenshots, deploy to a free host | Public repository and live link |
| 14 | Review and polish | Final walkthrough you can explain in five minutes |
If you want a structured place to do this, CareerSwami’s free two-week online internship offers a 14-day format with 26 tracks, a free offer letter, and a free completion certificate after successful completion. The plan above works equally well on your own.
CUSAT B.Tech students: turning this exercise into internship evidence
The B.Tech regulations apply from 2023-24 to all programmes under the Faculty of Engineering except Marine Engineering, as set out in the CUSAT B.Tech Regulations 2023. They describe a programme structure that includes professional practice covering project, seminar, and industrial training and internship. That means a documented frontend project can feed into the written work your department expects, though the regulations as summarised here do not state duration, credits, or whether an online internship qualifies.
The same document says project and internship work is assessed 40% on continuous assessment by the guide or faculty mentor, 20% on the report, and 40% on the final review, which is held by a committee including the Project/Internship Coordinator, the guide, and a senior faculty member. In practice, this rewards evidence you can show over time. Commit regularly, keep your Stage notes dated, and write the Stage 5 decision note so it can double as a section of a report. A mentor can only assess continuously what they have seen.
Also note that the regulations say revaluation is not permitted for internships and project work, so clarify expectations before you start rather than after. Regulations from 2023 may have been amended, so check the current version.
Verification steps for CUSAT students before counting this work
- Ask your department’s Project/Internship Coordinator in writing whether a free online 14-day internship can count toward your professional practice requirement.
- Request the department’s required report format and any prescribed documents, such as an offer letter or certificate.
- Ask your faculty mentor to agree on the Stage checkpoints as the basis for continuous assessment.
- Confirm which semester your internship or project component falls in, and its credit value, from your scheme and syllabus.
- Check the academic calendar page for 2026-2027 for break dates, since the excerpt did not show them, and plan your 14 days around them.
What to put in your README and interview answer
- One paragraph describing the app and a screenshot.
- A table of the five stages with a link to the commit or branch for each.
- The rubric note: which state is local, which is lifted, what the reducer owns, and what context removes.
- One honest sentence on what you would not use context for.
- Setup instructions: install, run, and test commands.
In an interview, a good answer sounds like: “I started with useState, moved to a reducer when updates became many and related, and added context when I counted props being passed only to relay data.” Claim only what your repository shows, and do not describe the project as an industry placement or a paid role.
Frequently Asked Questions
Should I use useReducer or useState for a small app?
Use useState until updates become numerous and related. In the task tracker, a reducer starts paying off once add, toggle, edit, delete, and clear all change the same list. For a single text input, it is unnecessary.
Does context replace a state management library?
Context only passes values down the tree; it does not manage updates. Paired with useReducer it covers many small apps. Libraries become relevant when you need caching, cross-route state, or specialised devtools.
Why split state and dispatch into two contexts?
The dispatch function has a stable identity, so components that only dispatch need not re-render when state changes. Splitting keeps consumers limited to what they actually read.
Is it wrong to keep form input in local state?
No. Input text and edit mode are typical local state. Lift or globalise only data other components genuinely need, such as the tasks and the filter.
Can this project count as my CUSAT internship or project work?
That is a department decision. The B.Tech regulations list professional practice including internships, but the details you need, such as duration, credits, and acceptance of online internships, should be confirmed in writing with your Project/Internship Coordinator.
What should I test in the reducer?
Test each action type with a small starting state and check the returned state, including that the original state was not changed. Add a case for an unknown action and for clearing when nothing is completed.
How many tasks should I seed to check performance?
A few hundred generated tasks are enough to observe re-renders in React Developer Tools for a learning exercise. Treat any measurements as observations from your own machine, and do not optimise before you see an actual slowdown.
Does the CareerSwami internship give credit or a stipend?
No such claim is made. It is a free 14-day online internship with a free offer letter and a free completion certificate after successful completion. Whether it counts for academic purposes is for your department to decide.
Related Frontend Development guides
- Mizoram University Students: Frontend Internship Applications When Local Openings Are Few
- D.A.V. University: Responsive Layout Drills for a Frontend Internship Using Grid and Flexbox
- Nagaland University Students: Presenting Frontend Internship Work on LinkedIn
- All Frontend Development internship guides
Ready to put this into practice?
Join the free two-week CareerSwami online internship in Frontend Development. The internship, the offer letter and the completion certificate are free.














