React JS Interview Questions for 2 Years Experience
40 questions with answers for candidates with 2 years of experience — 6 easy, 31 medium, 3 hard.
What interviewers expect: 2 years experience
At two years, expect deeper questions on re-renders, memoisation, state management choices and TypeScript. Interviewers ask how you structured features, handled caching with TanStack Query or RTK Query, wrote tests and fixed production bugs. Machine coding tasks become larger, such as an infinite scroll feed or a multi-step form with validation and state persistence.
This set is weighted towards Routing and Forms (10), Next.js and Advanced React (8), Testing and Debugging (6), React Hooks (5).
Re-render behaviour and memoisation Redux Toolkit and TanStack Query Testing with React Testing Library TypeScript with React
Solved at this levelStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
✓ 3 questions free • Unlock the rest in seconds
01
When should you choose useReducer over useState? Show a small example.
Use useReducer when state has several related fields or many update types, so logic lives in one pure, testable function.
function reducer(state, action) {
switch (action.type) {
case 'added': return [...state, action.item];
case 'removed': return state.filter(i => i.id !== action.id);
default: return state;
}
}
const [cart, dispatch] = useReducer(reducer, []);
dispatch({ type: 'added', item });
02
Show a Server Function used to handle a form in Next.js.
A function marked 'use server' runs on the server and can be passed to a form action.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function addTodo(formData: FormData) {
const title = String(formData.get('title'));
await db.todo.create({ data: { title } });
revalidatePath('/todos');
}
// app/todos/page.tsx
<form action={addTodo}>
<input name="title" required />
<button>Add</button>
</form>
Always validate input and check authorisation inside the function.
'use client' at the top of a file marks a boundary: that module and everything it imports become part of the client bundle and can use state, effects, event handlers and browser APIs. It does not mean the component is only rendered on the client; it is still pre-rendered to HTML on the server and then hydrated. Keep 'use client' as low in the tree as possible to send less JavaScript.
04
The linter shows a react-hooks/exhaustive-deps warning and a teammate wants to disable the rule. What do you advise?
Disabling it usually hides a stale closure bug. The rule tells you the effect reads a value that can change. Instead, include the dependency, move a function inside the effect, use the updater form of setState so state is not needed, or move non-reactive logic into useEffectEvent. If an object dependency changes every render, create it inside the effect or memoise it. Suppress only with a clear comment.
Server Components run only on the server or at build time. They can be async, read databases or files directly and send rendered output to the client without shipping their JavaScript, which reduces bundle size. They cannot use state, effects or browser APIs. Client Components, marked with 'use client', handle interactivity. Server Components can render Client Components and pass them serialisable props.
06
You are building a 4-step onboarding wizard. How do you keep data when users move back and forth between steps?
Store the wizard data in a parent component, a reducer, or a small store, not inside each step, because step components unmount when hidden. Each step receives its slice and an update callback. Validate each step before moving forward, and optionally save drafts to sessionStorage or the server so a refresh does not lose progress. The current step can also live in the URL for back-button support.
The server looks for a real file at /dashboard, which does not exist because routing happens in the browser. Configure the server to return index.html for all unknown paths: a rewrite rule in Nginx (try_files $uri /index.html), a _redirects or rewrites setting on static hosts, or a fallback in Express. Alternatively, use a framework with server routing. HashRouter also avoids it but gives ugly URLs.
09
What changed with createRoot in React 18 and later compared to ReactDOM.render?
createRoot from react-dom/client replaces the legacy ReactDOM.render, which was removed in React 19. createRoot enables the concurrent renderer, automatic batching of all updates, and features like useTransition and Suspense for data. You call const root = createRoot(document.getElementById('root')) and then root.render(<App />). For server-rendered HTML you use hydrateRoot instead.
Hydration is the process where React, running in the browser, attaches event handlers and state to HTML that was already rendered on the server, making it interactive. React expects the first client render to match the server HTML exactly; if not, it reports a hydration mismatch. Until hydration finishes, the page is visible but buttons may not respond, which is why reducing client JavaScript matters.
11
Users lose their typed data when they accidentally click another menu link. How would you warn them about unsaved changes?
Track a dirty flag, for example formState.isDirty from React Hook Form. For in-app navigation, use React Router's useBlocker to block the transition and show a confirm dialog. For tab close or refresh, add a beforeunload listener while the form is dirty, and remove it in the effect cleanup. Also consider autosaving drafts, which is often a better user experience than blocking.
12
A developer says the app is too slow, based on testing it with the development server. What do you tell them?
The development build includes extra warnings, StrictMode double rendering, checks and unminified code, so it is much slower and can mislead you. Production builds created with npm run build are minified, tree-shaken and optimised. Test performance on the production build, ideally on a mid-range Android phone with throttled network, because many Indian users browse on such devices and slower connections.
13
Tell me about working with a backend team when an API did not match what the frontend needed.
Situation: the orders API returned deeply nested data and no pagination, making our list screen slow. Task: get a better contract without blocking either team. Action: I shared a proposed response shape with the backend developer, explained the performance impact with numbers and built against a mocked API using MSW meanwhile. Result: they added pagination and flatter fields, and we integrated in two days with no rework.
fireEvent dispatches a single DOM event, such as one change event. user-event simulates full user interactions: typing fires keydown, keypress, input and keyup for every character, clicking includes pointer and focus events, and it respects disabled elements and visibility. This finds bugs that fireEvent misses. In current versions, call userEvent.setup() and await each interaction because they are asynchronous.
Data in React flows down from parent to child through props. A child cannot directly change its parent's data; instead the parent passes a callback function, and the child calls it to request a change. This makes the app predictable because you can trace where every value comes from. Two-way binding, as in some other frameworks, is done manually with value and onChange.
Use an i18n library such as react-i18next or, in Next.js, next-intl. Store translations in JSON files per locale, reference keys like t('cart.title') instead of hard-coded text, and handle plurals, dates, numbers and currency with Intl APIs. Put the locale in the URL, like /hi/products, for SEO and shareable links. For Indian audiences, check fonts for Devanagari and other scripts and test longer translated text.
17
Tell me about a time you had to learn a new tool or library quickly.
Situation: our project needed to switch from Jest to Vitest with a week's notice. Task: migrate around 300 tests. Action: I read the official migration guide, migrated one folder first to discover patterns, wrote a codemod for jest.fn to vi.fn replacements and documented gotchas for the team. Result: we finished in four days, test runs became much faster, and the guide helped others with later work.
18
How did you bring better testing practices to a team that had very few tests?
Situation: our React app had almost no tests and regressions reached production every release. Task: improve quality without slowing delivery. Action: I set up Vitest, React Testing Library and MSW, wrote example tests for two key screens, ran a short session for the team and added a rule that bug fixes include a test. Result: within three months, critical flows were covered and regressions dropped noticeably.
19
A search box fetches results on every keystroke and sometimes shows results for an older query. How do you fix this race condition?
Responses can arrive out of order, so a slow old request overwrites newer results. Cancel or ignore stale requests in the effect cleanup.
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/search?q=${q}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setResults)
.catch(() => {});
return () => ctrl.abort();
}, [q]);
Also debounce the input, or use TanStack Query which handles this for you.
20
How do you handle a tight deadline when you know the code is not perfect?
Situation: a festive sale banner feature had to go live in two days. Task: ship safely without gold-plating. Action: I listed must-haves with the product owner, wrote tests for the critical checkout path, kept a simple implementation and logged clear tech-debt tickets for refactoring. Result: we launched on time with no incidents, and I cleaned up the code in the next sprint as planned.
21
How do you read and update query parameters like ?page=2&sort=price?
Use useSearchParams, which works like useState for the query string.
import { useSearchParams } from 'react-router';
function ProductList() {
const [params, setParams] = useSearchParams();
const page = Number(params.get('page') ?? 1);
const sort = params.get('sort') ?? 'popular';
const next = () => setParams({ page: String(page + 1), sort });
return <Pagination page={page} onNext={next} />;
}
Keeping filters in the URL makes them shareable and bookmarkable.
In React 19 you can pass a function to a form's action prop: <form action={saveAddress}>. React calls it with the FormData when the form submits, runs it inside a transition, tracks pending state and resets uncontrolled fields after success. Combined with useActionState for results and useFormStatus for button state, this removes manual onSubmit, preventDefault and loading flags. In Next.js the action can be a Server Function.
23
What are the common styling options in React, and how do Server Components affect the choice?
Options include plain CSS or CSS Modules (scoped class names), Tailwind CSS utility classes, Sass, and CSS-in-JS libraries like styled-components or Emotion. Runtime CSS-in-JS needs client-side JavaScript and context, so it fits poorly with Server Components. That is why Tailwind and CSS Modules, or zero-runtime libraries, are the most common choice in modern Next.js projects. Component libraries like shadcn/ui build on Tailwind.
24
What is the difference between the App Router and the Pages Router in Next.js?
The Pages Router uses the pages directory with getServerSideProps and getStaticProps for data. The App Router, the recommended default, uses the app directory with nested layouts, React Server Components by default, streaming with loading.js, error.js boundaries and Server Functions. Data is fetched directly in async server components. Many older projects still use Pages Router, and both can coexist during migration.
React wraps native browser events in a SyntheticEvent object that has the same interface (preventDefault, stopPropagation, target) across browsers. Since React 17, listeners are attached to the root container instead of document, which makes it safer to mix multiple React versions. Event pooling was removed, so you can read event properties asynchronously. Handlers are passed as functions, like onClick={handleClick}.
26
What is the difference between useMemo and useCallback?
useMemo caches the result of a calculation between renders, like a filtered list: useMemo(() => filter(items, q), [items, q]). useCallback caches a function definition itself: useCallback(fn, deps), which equals useMemo(() => fn, deps). Both are performance tools, useful when passing values to React.memo children or effect dependencies. With the React Compiler enabled, much of this manual memoisation becomes unnecessary.
Use vi.mock with a factory, then control return values per test.
import { vi } from 'vitest';
import { getProfile } from './api';
vi.mock('./api', () => ({
getProfile: vi.fn()
}));
test('shows name', async () => {
getProfile.mockResolvedValue({ name: 'Ravi' });
render(<Profile />);
expect(await screen.findByText('Ravi')).toBeInTheDocument();
});
In Jest, the same pattern uses jest.mock and jest.fn.
28
How do you update objects and arrays in state without mutating them?
useActionState wraps an action function and returns [state, formAction, isPending]; you pass formAction to a form's action prop and the returned state holds the result, like a validation error. useFormStatus, from react-dom, lets a child component such as a submit button read the pending status of the parent form without prop drilling. Together they remove a lot of manual loading and error state code.
30
How do you handle async API calls with createAsyncThunk?
Link every input to a visible label using htmlFor and id (useId helps), use correct input types, and mark required fields. Show errors as text, connect them with aria-describedby and set aria-invalid on failing fields. Move focus to the first error on submit, keep everything keyboard operable and do not rely on colour alone. Use native button and form elements instead of clickable divs.
32
Show a basic React Router setup with a home page, a product page and a 404 page.
Client state is owned by the browser, like whether a modal is open, form input or theme. Server state lives on a backend and is only cached on the client, like products or orders; it can become stale, be changed by other users and needs refetching. Tools like TanStack Query or RTK Query manage server state, while useState, Context or Zustand handle client state.
Use renderHook from React Testing Library and wrap state changes in act.
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('increments', () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.increment());
expect(result.current.count).toBe(6);
});
If the hook needs context, pass a wrapper option with the provider.
35
A cart page keeps the selected item object in state and also the items array. After editing the item's quantity, the details panel shows the old quantity. Why?
The selected item is a copy duplicated from the array, so updating the array does not update the copy. This breaks the single source of truth. Store only selectedId in state and derive the item during render: const selected = items.find(i => i.id === selectedId). Now there is one place where each item lives, and every view stays in sync automatically.
In data or framework mode, a route can define a loader that fetches data before the route renders, and an action that handles form submissions or mutations for that route. Components read data with useLoaderData. After an action runs, the router automatically revalidates loaders so the UI shows fresh data. This moves data fetching out of effects and avoids request waterfalls between parent and child components.
Nested routes let a parent route render a shared layout, like a dashboard sidebar, while child routes render inside it. The parent component places <Outlet /> where the matching child should appear. For example, /dashboard renders DashboardLayout, and /dashboard/orders renders Orders inside its Outlet. This keeps layouts consistent, avoids repeating headers and lets each section load its own data.
An error boundary catches errors thrown while rendering its children and shows a fallback UI. It must be a class component (or use the react-error-boundary package).
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logError(error, info.componentStack);
}
render() {
return this.state.hasError ? <p>Something went wrong</p> : this.props.children;
}
}
It does not catch errors in event handlers or async code.
Frequently asked questions
What is asked in React JS interviews for 2 years of experience?
At two years, expect deeper questions on re-renders, memoisation, state management choices and TypeScript. Interviewers ask how you structured features, handled caching with TanStack Query or RTK Query, wrote tests and fixed production bugs. Machine coding tasks become larger, such as an infinite scroll feed or a multi-step form with validation and state persistence.
How many questions should I practise before a React JS interview?
Work through all 40 questions here, then the company-wise sets for the employers you are targeting. Quality matters more than count: be able to explain why each answer is right.
Which topics matter most for 2 years of experience?
Focus first on Re-render behaviour and memoisation, Redux Toolkit and TanStack Query, Testing with React Testing Library, TypeScript with React.