Here are 28 React interview questions with model answers, written for React 19 and the way teams build with it today: hooks, rendering behavior, the React Compiler, Suspense, Server Components, Actions and state management. Browser-level topics such as accessibility, CSS and Core Web Vitals are on our front-end page. At Ryz, senior React engineers are sourced by recruiters, assessed in structured NTRVSTA AI interviews on rendering and component design, and reviewed by recruiters before and after. The AI scores are one input. People make the decisions.
React knowledge goes stale quickly. A candidate who learned React in 2019 may still write useEffect for everything and reach for forwardRef, which is fine to see but should not be the ceiling. Match questions to the work: a team on Next.js App Router needs the Server Components questions, while a team on a Vite single-page app needs more on state and rendering.
Ask candidates to explain why a component renders, not just how to stop it. Then have them build or debug a component live, because React fluency shows in how someone structures state while typing, not in definitions.
Its own state changing, its parent re-rendering, or a context it reads changing. Props changing is not a separate trigger: a child re-renders when its parent does, whether or not props changed, unless it is memoized. Rendering means calling the component function. React then commits only the DOM changes that differ.
What a strong answer shows: They separate rendering from DOM updates and do not treat every re-render as a problem.
key cause bugs?Keys tell React which item is which between renders. With index keys, inserting or removing an item at the top shifts every key, so React reuses the wrong component instances: input values, focus and local state appear on the wrong rows. Use a stable ID from the data. Changing a key on purpose is also a clean way to reset a component's state.
What a strong answer shows: They know index keys are fine for static lists and use the key-reset technique deliberately.
function Counter() {
const [count, setCount] = useState(0);
function addThree() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
// fix: setCount((c) => c + 1) three times
}
State is a snapshot for each render. All three calls read the same count, so they each request the same value. Updates are batched and applied on the next render. The updater form receives the latest pending state.
What a strong answer shows: They explain snapshot semantics and know batching applies inside promises and timeouts too since React 18.
// avoid
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(items.filter((i) => i.name.includes(query)));
}, [items, query]);
// prefer
const visible = items.filter((i) => i.name.includes(query));
The effect version renders twice, briefly shows stale data and adds a place for bugs. Values computable from props and state should be computed during render.
What a strong answer shows: They describe effects as a way to sync with external systems, not to transform data, and they know the "You might not need an effect" guidance.
{items.length && <List />} sometimes render a "0"?When items.length is 0, the expression evaluates to 0, and React renders numbers. Use items.length > 0 && ... or a ternary.
What a strong answer shows: They know which values React skips (false, null, undefined, booleans) and which it renders.
Controlled inputs keep the value in React state, which suits instant validation, formatting and dependent fields. Uncontrolled inputs keep the value in the DOM and read it on submit, which is simpler and pairs well with React 19 form Actions that receive FormData. Large forms often use uncontrolled inputs through a form library for performance.
What a strong answer shows: They pick per form instead of controlling every input by habit.
Actions and the useActionState, useOptimistic and useFormStatus hooks for async mutations; the use API for reading promises and context; ref as a normal prop, so forwardRef is no longer needed; <Context> usable as a provider; native support for document metadata tags; and stable Server Components. React 19.2 added useEffectEvent and <Activity>.
What a strong answer shows: They have used at least some of this in real code and can say what it replaced.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((e) => { if (e.name !== "AbortError") setError(e); });
return () => controller.abort();
}, [id]);
Without the cleanup, a slow response for an old id can arrive after a newer one and overwrite it. Aborting on cleanup prevents the race. For real apps, prefer a data library such as TanStack Query or framework loaders, which add caching, deduplication and retries.
What a strong answer shows: They know effect-based fetching causes waterfalls and that Strict Mode's double effect run in development exposes missing cleanup.
Every consumer re-renders when the context value changes, and an object literal value changes every render. Split fast-changing and slow-changing values into separate contexts, keep frequently updated state close to where it is used, and for many subscribers with fine-grained reads use a store with selectors, such as Zustand, which subscribes through useSyncExternalStore.
What a strong answer shows: They see context as dependency injection, not a state manager for high-frequency updates.
Server data goes in a server-state cache (TanStack Query, SWR, or the framework). Shareable view state such as filters, tabs and pagination goes in the URL. Form state stays in the form. UI state stays local and is lifted only as far as needed. A global client store is for genuinely shared client state, which is usually small.
What a strong answer shows: They treat server state and client state as different problems and avoid putting API responses into Redux by default.
function RenameForm({ project }) {
const [error, submitAction, isPending] = useActionState(
async (previousError, formData) => {
const result = await renameProject(project.id, formData.get("name"));
return result.ok ? null : result.error;
},
null,
);
return (
<form action={submitAction}>
<input name="name" defaultValue={project.name} />
<button disabled={isPending}>Save</button>
{error && <p role="alert">{error}</p>}
</form>
);
}
What a strong answer shows: They can add useOptimistic for instant feedback and explain how the optimistic value reverts if the action fails.
They catch errors thrown during rendering, in lifecycle methods and in child constructors, and show fallback UI. They do not catch errors in event handlers, in async code outside rendering, or in the boundary itself. Place boundaries around independent regions so one failing widget does not blank the page.
What a strong answer shows: They report caught errors to monitoring and provide a retry that resets the boundary.
React Testing Library with user-event, querying by role and accessible name as a user would, with MSW mocking the network. Test behavior, not implementation: no assertions on state or hook calls. Use Playwright for critical end-to-end flows.
What a strong answer shows: They avoid snapshot tests as the main strategy and use findBy queries for async UI instead of arbitrary waits.
useDebouncedValue hook.function useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
What a strong answer shows: Proper cleanup, and a note that useDeferredValue is often a better fit when the goal is responsiveness rather than fewer network requests.
The effect reads theme to show a toast, so theme is a dependency and changing it reconnects. Move the non-reactive logic into an Effect Event with useEffectEvent, stable since React 19.2, so it reads the latest theme without being a dependency.
const onConnected = useEffectEvent(() => showToast("Connected", theme));
useEffect(() => {
const conn = createConnection(roomId);
conn.on("connected", onConnected);
conn.connect();
return () => conn.disconnect();
}, [roomId]);
What a strong answer shows: They never silence the exhaustive-deps lint rule and understand stale closures.
useMemo, useCallback and memo?The compiler memoizes components and values automatically at build time, so most manual memoization becomes unnecessary in new code. It relies on code following the Rules of React (pure render, no mutation of props or state), and skips or bails out on code that breaks them. Manual memoization still has a place for effect dependencies that must stay stable, or in code the compiler does not optimize.
What a strong answer shows: They would adopt it incrementally, use the ESLint plugin to find rule violations, and measure before and after.
Profile with the React DevTools Profiler to see what renders and how long. Fixes, roughly in order: virtualize the list so only visible rows render, keep the input's state local so the whole page does not re-render, wrap the filtered list in useDeferredValue so typing stays responsive, and memoize row components if the compiler is not in use.
What a strong answer shows: Measurement first, and recognition that rendering 5,000 DOM rows is the real cost.
useTransition and useDeferredValue?Both mark updates as non-urgent so urgent ones such as typing interrupt them. useTransition wraps the state update you control and gives an isPending flag. In React 19 the transition function can be async, which is how Actions work. useDeferredValue takes a value you receive, often a prop, and gives a lagging version to render expensive UI.
What a strong answer shows: They know transitions do not make code faster: they keep the UI responsive while slow rendering happens.
useLayoutEffect or useSyncExternalStore?useLayoutEffect runs after DOM changes but before the browser paints, for measuring layout and positioning tooltips without a flicker. It blocks paint, so use it sparingly. useSyncExternalStore subscribes to stores outside React, such as a browser API or a custom store, without tearing during concurrent rendering.
What a strong answer shows: They know useLayoutEffect does not run on the server and how to handle that.
It mounts, unmounts and remounts components to expose effects without proper cleanup, and double-invokes render functions to expose impure rendering. It does not happen in production. If an effect breaks when run twice, it is missing cleanup that will matter with Fast Refresh, <Activity> and other features that reuse state.
What a strong answer shows: They fix the effect rather than disabling Strict Mode or adding a "has run" ref.
A component suspends when it reads data that is not ready, through use(promise), a Suspense-enabled library or lazy. React shows the nearest boundary's fallback until it resolves. Place boundaries around meaningful regions so the page fills in sensibly, and start data requests early, outside the suspending component, to avoid waterfalls.
What a strong answer shows: They know a promise created during render is recreated each render unless it is cached.
"use client" boundary.Server Components run only on the server or at build time, can read data directly, and send no JavaScript for themselves to the browser. They cannot use state, effects or browser APIs. "use client" marks the entry into client code. Everything it imports becomes client code, and props crossing the boundary must be serializable. Server Components can still be passed to Client Components as children.
What a strong answer shows: They push "use client" down to small interactive leaves and know the boundary is about module graphs, not file location.
A function marked "use server" becomes a callable endpoint, even if the UI only shows the button to admins. Each one must authenticate, authorize the specific action and validate its arguments. Returning whole database records to Client Components can leak fields, so use explicit DTOs.
What a strong answer shows: They treat Server Functions as public API routes.
Server and client rendered different output: dates and time zones, Math.random or Date.now() in render, reading window or localStorage during render, invalid HTML nesting, or browser extensions changing the DOM. Fix by rendering deterministic output on both sides, moving client-only values into an effect, and using useId for IDs.
What a strong answer shows: They know why suppressHydrationWarning is for a single known element, not a fix.
Composition over configuration: compound components and children instead of components with 40 props. Build on accessible headless primitives such as Radix or React Aria, forward ref and unknown props to the root element, keep styling customizable through tokens, and version with codemods for breaking changes.
What a strong answer shows: They think about consumers' escape hatches and the cost of every breaking change.
Move the build to Vite first, since CRA is deprecated, without changing components. Upgrade React in steps with codemods and fix Strict Mode warnings. Convert class components to hooks only when touching them. Decide on a framework such as Next.js or React Router only if you need server rendering or routing changes, not as part of the same project.
What a strong answer shows: Incremental steps, each shippable, with no big-bang rewrite.
Fetch at the route level in parallel with loaders or Server Components, prefetch on hover or intent, pass promises down and read them with use inside Suspense boundaries, and avoid nested components that each fetch only after their parent's data arrives.
What a strong answer shows: They can find waterfalls in the network panel and restructure data loading, not only add spinners.
Organize by feature, not by file type, with each feature owning its components, hooks and data access behind a public entry point. Share a design system package and enforce boundaries with lint rules. Consider micro-frontends only when teams truly need independent deploys, since they add runtime and consistency cost.
What a strong answer shows: They tie structure to team ownership and are skeptical of complexity that solves organizational problems poorly.
useEffect to compute values that could be calculated during render.useMemo and useCallback without measuring, or cannot explain what they do."use client" means "this component renders only in the browser".Run a 90-minute pairing session on a prepared repo: a React 19 app with a searchable, sortable list of 2,000 orders loaded from a mock API, and an edit drawer. Seed it with problems: an effect-based fetch with a race condition, derived state stored in state, a context that re-renders everything, and a form without pending or error states. Ask the candidate to find and fix what matters most in the time available, explaining priorities as they go.
If you need React engineers who already think this way, hire senior React developers through Ryz. They come from the top 1% of candidates we interview, work on your team and in your repos, and work within ±1h of US time zones. Our vetting process shows how we evaluate them, and you can adapt our React developer job description for your own posting.
Briefly, if your codebase still has them. New code uses function components and hooks, so focus there, but a senior candidate maintaining an older app should be able to read lifecycle methods and migrate them safely.
Only if you use it. Server Components and Server Functions are React features, but most teams use them through a framework such as Next.js. A strong React engineer can learn a framework's conventions quickly. Rendering and state fundamentals transfer.
A short live session on an existing repo usually works better than a long take-home. Debugging and extending real components shows how the candidate reads code, which is most of the job, and respects their time.
Questions we didn't answer? Email info@ryzlabs.com.