Prepare for React developer interviews with questions grouped by experience level, from components and hooks to SSR and Next.js.
Junior (0-2 years)
React is a JavaScript library for building user interfaces, developed by Facebook. It was built to solve the problem of keeping a UI in sync with changing data efficiently, without manually writing code to find and update every affected piece of the DOM by hand whenever something changes.
JSX is a syntax extension letting you write HTML-like markup directly inside JavaScript code. React uses it because it makes describing a UI's structure alongside the logic that drives it far more readable than building that same structure through nested JavaScript function calls. Under the hood, a build tool compiles JSX down into regular JavaScript calls to React.createElement.
The virtual DOM is a lightweight JavaScript representation of the actual DOM that React keeps in memory. When state changes, React builds a new virtual DOM tree, compares it against the previous one (a process called diffing), and only applies the specific, minimal changes needed to the real DOM instead of re-rendering the entire page. Direct DOM manipulation is comparatively slow, so minimizing it is where a lot of React's actual performance benefit comes from.
A component is a reusable, self-contained piece of UI, described as a function that returns what should be rendered. Functional components, plain JavaScript functions, are the standard approach today. Class components, an older style using ES6 classes, still show up in existing codebases but are rarely used for new code since hooks were introduced.
A functional component is a plain function accepting props and returning JSX, using hooks like useState for state. A class component extends React.Component, manages state through this.state, and uses lifecycle methods instead of hooks. Functional components with hooks became the standard approach after React 16.8, largely because they're more concise and easier to reuse logic between.
An element is a plain, lightweight description of what should appear on screen, essentially a JavaScript object, and it's immutable once created. A component is a function or class that returns elements. Rendering a component actually produces the elements that get compared and eventually applied to the real DOM.
Props (short for properties) are how data gets passed from a parent component into a child component. They flow in one direction only, from parent to child, and a child component can't directly modify the props it receives, since props are meant to be read-only from the receiving component's perspective.
React relies on props being immutable to reliably determine when and how to re-render a component. Directly mutating a prop wouldn't trigger React's normal update mechanism and would violate the predictable, one-way data flow React is built around, which is exactly why React doesn't provide a built-in way to do it and why attempting it is considered a clear anti-pattern.
The parent passes a function down to the child as a prop, and the child calls that function, typically with some data as an argument, whenever it needs to communicate something back up. This is the standard pattern for child-to-parent communication, since props themselves only flow downward.
Prop drilling is passing a prop down through several layers of components that don't actually use it themselves, just to get it to a deeply nested component that does. It becomes a real problem as an application grows, since intermediate components end up cluttered with props they don't care about, and tracing where a value actually originates gets genuinely harder the deeper the chain goes.
props.children represents whatever content is nested between a component's opening and closing tags when it's used. It lets you build components like a generic Card or Modal wrapper that render whatever content is passed inside them, without the wrapper component needing to know in advance exactly what that content will be.
Default parameter syntax directly in the function signature, function Greeting({ name = 'Guest' }), assigns a fallback value used whenever that specific prop isn't explicitly provided by the parent.
State is data a component manages internally that can change over time, typically in response to a user action. Props come from outside a component and are read-only from that component's perspective, while state belongs to the component itself and can be updated directly using the appropriate hook or method.
const [count, setCount] = useState(0); declares a state variable starting at 0 and a function to update it. Calling setCount(count + 1) updates the state and triggers React to re-render the component with the new value.
React determines whether to re-render partly by comparing the previous and new state references. Directly mutating an existing array or object leaves the reference unchanged, so React may not detect that anything actually changed and skip the re-render entirely, even though the underlying data did change. Creating a new array or object, like using the spread operator, ensures React correctly detects the update.
It schedules a re-render of the component with the new state value, though the actual state update and re-render don't necessarily happen synchronously, immediately at the point setCount is called. React can batch multiple state updates together for efficiency, which is why reading the state variable immediately after calling its setter often still shows the old value.
Local state, managed with useState inside one component, is appropriate when only that component (and maybe its direct children through props) needs the data. Once multiple, unrelated components need access to the same state, it typically needs to move up to a shared ancestor component, or into a more global state management solution, since local state can't be directly accessed by a sibling component.
It means moving state from a child component up to their closest common parent, when two or more sibling components need to share or stay in sync with the same piece of data. The parent then passes that state down as props to whichever children need it, keeping a single source of truth rather than each component maintaining its own separate, potentially conflicting copy.
React uses camelCase event names (onClick rather than onclick) and passes an actual function reference rather than a string of code, <button onClick={handleClick}>. React also wraps native browser events in its own SyntheticEvent object, which normalizes behavior consistently across different browsers.
A controlled component is a form input whose value is driven entirely by React state, rather than the DOM managing its own internal value. The input's value comes from state, and an onChange handler updates that state on every keystroke, keeping React as the single source of truth for the input's current value at all times.
A controlled component's value lives in React state, and every change goes through React first. An uncontrolled component lets the DOM manage its own value internally, and you'd read that value only when needed, typically using a ref, rather than tracking every keystroke in state. Controlled components are generally preferred for most use cases since they keep React fully in sync with the current value at all times.
Attach an onSubmit handler to the form element, and inside that handler, call event.preventDefault() to stop the browser's default full-page-reload form submission behavior, then handle the actual submission logic, typically reading values already tracked in state from the form's controlled inputs.
A ref provides direct access to a DOM element or holds a mutable value that doesn't trigger a re-render when it changes. You'd reach for a ref instead of state when you need to directly interact with a DOM element, like focusing an input, or when you need to persist a value across renders without that value itself needing to cause any re-rendering.
For simple cases, validation logic can live directly in the component, checking values in state and displaying an error message conditionally when a rule fails. For more complex forms, a dedicated library like Formik or React Hook Form, often paired with a schema validation library like Yup or Zod, handles validation, error messages, and form state together in a way hand-rolled logic usually can't match as cleanly.
useEffect lets a functional component perform side effects, fetching data, setting up a subscription, manually interacting with the DOM, things that fall outside of simply rendering UI based on props and state. It's how functional components handle the kind of logic that class components used to spread across separate lifecycle methods.
The dependency array, the second argument to useEffect, tells React when to re-run the effect. An empty array means the effect runs only once, after the initial render. Omitting the array entirely means it runs after every single render. Including specific values means it re-runs only when one of those specific values actually changes between renders.
A cleanup function is returned from inside useEffect and runs right before the component unmounts, or right before the effect runs again due to a dependency changing. It's used to clean up anything the effect set up, clearing a timer, unsubscribing from an event listener, canceling an in-flight network request, preventing that lingering side effect from continuing after it's no longer needed.
useEffect with no dependency array runs after every render, which is rarely what you actually want, since it can cause an effect (like a data fetch) to repeat far more often than intended. useEffect with an empty dependency array runs exactly once, right after the component's first render, commonly used for a one-time data fetch or subscription setup that shouldn't repeat on every subsequent render.
Call the fetch inside a useEffect with an empty dependency array, so it runs exactly once after the initial render, storing the result in state using useState so the component re-renders with the fetched data once it actually arrives.
Omitting a value that the effect actually uses, which can cause the effect to run with a stale, outdated version of that value captured from an earlier render. The React team's official ESLint plugin specifically flags this exact mistake, since it's genuinely easy to introduce it accidentally and hard to notice just by reading the code.
Plain CSS files imported into a component, CSS Modules for automatically scoped class names that avoid naming collisions across components, and CSS-in-JS libraries like styled-components that let you write actual CSS directly inside your JavaScript files, colocated with the component that uses it.
A ternary expression inside JSX, {isLoggedIn ? <Dashboard /> : <LoginPage />}, or the logical AND operator, {items.length > 0 && <ItemList items={items} />}, for cases where you either want to show something or render nothing at all.
Keys help React identify which specific items in a list changed, were added, or were removed between renders, letting it update the DOM efficiently and correctly rather than re-rendering the entire list from scratch every single time. Without stable, correctly assigned keys, React can misattribute state or DOM elements to the wrong item, especially when a list gets reordered.
If the list's order can change, items get inserted, removed, or reordered, the index no longer reliably corresponds to the same logical item across renders, which can cause React to preserve the wrong component's internal state after a reorder. A stable, unique identifier from the actual data itself, like a database ID, is the safer choice whenever one is available.
A Fragment, <>...</> or <React.Fragment>...</React.Fragment>, lets a component return multiple sibling elements without wrapping them in an unnecessary extra DOM element, like a redundant div that serves no real purpose beyond satisfying JSX's single-root-element requirement.
StrictMode is a development-only tool that intentionally double-invokes certain functions, like a component's render or specific effect functions, to help surface side effects or impure logic that wouldn't be safe under React's newer concurrent rendering features. It doesn't affect the production build at all, only development, and seeing an effect run twice in development under StrictMode is expected behavior, not a bug.
Mid-Level (3-6 years)
useContext lets a component read a value from a React Context directly, without that value needing to be passed down explicitly through props at every intermediate level. It's the standard solution to prop drilling for data that many components across an application genuinely need, like the current theme or the authenticated user.
React.createContext() creates a Context object. A Provider component wraps the part of the component tree that should have access to the value, supplying it via its value prop. Any descendant component can then read that value directly with useContext(YourContext), no matter how deeply nested it is.
useReducer manages state through a reducer function, similar in spirit to Redux, taking the current state and a dispatched action and returning the new state. It's worth reaching for once state logic becomes complex, several related pieces of state that update together, or the next state genuinely depends on a specific action rather than simply setting one new value directly.
A custom hook is just a regular JavaScript function whose name starts with use, internally calling other hooks like useState or useEffect. The benefit is extracting and reusing stateful logic across multiple components, like a useFetch hook handling loading and error state for a data request, without duplicating that same logic inside every component that needs it.
Hooks must be called at the top level of a component, never inside a loop, a condition, or a nested function, and only from a React function component or another custom hook. These rules exist because React relies on hooks being called in the exact same order on every single render to correctly associate each hook call with its corresponding state, and breaking that order would corrupt that association entirely.
Composition builds complex UI by combining and nesting simpler components together, often using the children prop or explicitly passing components as props, rather than extending a base component class the way inheritance would. React's own documentation actively recommends composition, since it tends to be more flexible and produces components that are easier to reason about in isolation.
A function that takes a component and returns a new, enhanced component, adding extra behavior or props to the original without modifying its actual source code. withAuth(SomeComponent), returning a new component that checks authentication before rendering SomeComponent, is a classic example of the pattern.
Render props share logic between components by passing a function as a prop, which the receiving component calls to determine what to actually render, letting reusable logic control rendering indirectly. Custom hooks now handle most of the same use cases with noticeably less nesting and boilerplate, which is why render props have become far less common in modern React code, mostly showing up in older, existing codebases.
Compound components let several related components work together implicitly, sharing state through context, while still being composed flexibly by whoever's using them, similar to how a native HTML <select> and its <option> children work together as a set. A custom Tabs component, made up of Tabs, Tab, and TabPanel components sharing context internally, is a common example of this pattern in practice.
Custom hooks are generally the more modern, straightforward choice for sharing pure logic, like a piece of state and its associated behavior, without needing to wrap or alter the rendered output at all. HOCs still make more sense when you specifically need to affect what actually gets rendered or intercept a component's behavior more directly, though even then, many of those cases have shifted toward hooks in current React code.
React.memo wraps a component so it only re-renders when its actual props have changed, skipping a re-render entirely if the props are the same as the previous render. It's worth using on a component that renders often but whose props genuinely don't change on most of those renders, particularly one that's expensive to render, since memoization itself carries a small overhead too.
useMemo memoizes a computed value, recalculating it only when its dependencies change, useful for an expensive calculation you don't want repeated on every render. useCallback memoizes a function reference itself, useful for preventing a function from being recreated on every render when it's passed down as a prop to a memoized child component that would otherwise re-render unnecessarily due to that new reference.
React.memo compares props by reference for objects and functions. An inline function or object literal, defined directly inside JSX, is recreated as a genuinely new reference on every single render, even if its actual content is identical, which means React.memo sees a different reference each time and re-renders the child anyway, defeating the whole point of memoizing it.
The React DevTools Profiler shows exactly which components rendered during a given interaction and roughly how long each one took, making it easy to spot a component re-rendering far more often than it actually needs to. Guessing which component is the problem without actually profiling first often leads you to optimize the wrong part of the tree entirely.
Code splitting breaks a large application bundle into smaller chunks that load only when actually needed, rather than shipping the entire application's JavaScript upfront on initial page load. React.lazy(), combined with Suspense to show a fallback while the chunk loads, is the standard way to lazily load a component and its associated code only when it's actually about to be rendered.
A library like React Router intercepts navigation, updating the URL and rendering the appropriate component based on the current path, all without triggering an actual full-page reload from the server. This is what lets a React application feel like it has multiple distinct pages while technically remaining a single HTML page whose content swaps dynamically.
A wrapper component checks the current authentication state, rendering the intended route's content if the user is authenticated, or a redirect to the login page if they're not, using React Router's Navigate component. This wrapper then gets applied around any route that specifically requires authentication, rather than duplicating that same check inside every individual protected page.
A plain useEffect-based fetch requires manually managing loading state, error state, and re-fetching logic yourself, and it's easy to get subtly wrong, like not handling a race condition when a component unmounts mid-request. A library like React Query handles caching, automatic re-fetching, and loading and error states out of the box, which removes a genuinely large amount of repetitive, error-prone boilerplate from a typical application.
Track a boolean (or a more descriptive status value) in state, set it to true right before starting the fetch, and set it back to false once the fetch completes, whether it succeeded or failed. The component then conditionally renders a loading indicator while that flag is true, and the actual content once it's false.
React Testing Library encourages testing a component the way an actual user would interact with it, finding elements by visible text or accessible role rather than by internal implementation details like a component's internal state or specific class names. This philosophy makes tests more resilient to refactoring, since a test written this way keeps passing as long as the user-facing behavior stays the same, even if the internal implementation changes.
Render the component, use fireEvent or userEvent to simulate a real click on the button, then assert that the expected resulting change actually appears in the rendered output, like a counter's displayed value increasing by one after the click.
Mock the fetch function or the specific API module the component depends on, using Jest's mocking capabilities, so the test controls exactly what data comes back without making a real network request. This keeps the test fast, deterministic, and able to simulate an error response you couldn't easily trigger against a real API on demand.
A unit test verifies one component's behavior in isolation, often with its child components or dependencies mocked out. An integration test renders several components together as they'd actually appear in the real application, verifying that they genuinely work correctly together, which catches a class of bug, a broken prop connection between components, that isolated unit tests could easily miss.
Senior (6-8 years)
Context works well for state that changes infrequently and doesn't need to trigger frequent, fine-grained re-renders across a large part of the tree, since every consumer of a Context re-renders whenever its value changes, with no built-in way to subscribe to just part of that value. A dedicated library becomes worth the added setup once an application has complex, frequently-changing shared state, or genuinely needs more sophisticated tooling like time-travel debugging or middleware.
Redux centralizes an application's state into a single, predictable store, making it easier to reason about how and why state changes across a large application. Its core principles are a single source of truth (one store), state being read-only (only changed through dispatched actions), and changes being made through pure reducer functions, which take the current state and an action and return a new state without side effects.
Context is a built-in React mechanism for passing a value down the tree without prop drilling, but it doesn't inherently provide structured actions, reducers, or dev tooling. Redux adds that structure explicitly, along with middleware support and dedicated dev tools for time-travel debugging, at the cost of noticeably more setup and boilerplate for a simple use case that Context alone could have handled just fine.
Colocate state as close as possible to where it's actually used, keeping most state local to the specific feature that owns it, and reserve a genuinely global store only for state multiple, unrelated feature areas actually need to share, like the authenticated user or a global notification system. Treating every piece of state as global by default tends to create unnecessary coupling and re-renders across parts of the application that have nothing to do with each other.
Normalization means storing related data in a flat structure, indexed by ID, similar to a relational database's tables, rather than deeply nested objects with duplicated data scattered across them. It matters because updating a deeply nested piece of data without normalization typically means finding and modifying it in several different places at once, while a normalized structure lets you update it in exactly one place with all consumers automatically reflecting that single change.
Local UI state, like whether a dropdown is open, exists only on the client and has no real source of truth elsewhere. Server state represents data that actually lives on a server and can go stale or be updated by other clients. Treating server state like plain local state, tracked with a basic useState and a manual useEffect fetch, misses genuinely important concerns like caching, background re-fetching, and staleness, which is exactly why a dedicated tool like React Query exists specifically for that category of state.
The React DevTools Profiler records an interaction and shows exactly which components rendered, how long each one took, and why each one re-rendered. Guessing at the cause without actually profiling usually leads to optimizing the wrong component entirely, since the actual bottleneck is often somewhere less obvious than where it initially seems.
Virtualization renders only the list items currently visible within the viewport, plus a small buffer, rather than rendering every single item in a potentially enormous list all at once. A library like react-window or react-virtualized handles the actual scroll-position math, and it's essential for keeping a list with thousands of items from rendering thousands of DOM nodes and grinding the browser to a halt.
Rendering is React calling a component function to produce a description of what it wants shown. Reconciliation is the process of comparing that new description against the previous one to figure out the minimal set of actual changes needed in the real DOM. Rendering happens on every update. Reconciliation is the diffing step that decides what, if anything, actually needs to change as a result.
Code splitting by route, so a user only downloads the JavaScript needed for the page they're actually viewing, is usually the biggest single win. Beyond that, lazy loading images and non-critical components, and analyzing the bundle with a tool like webpack-bundle-analyzer to find and trim unexpectedly large dependencies, both meaningfully reduce what has to load before the page becomes usable.
Hydration is React attaching event listeners and taking over an already-rendered HTML page sent from the server, rather than rendering everything from scratch on the client. A hydration mismatch, where the server-rendered markup doesn't exactly match what the client would have rendered, causes React to log a warning and can lead to visible flickering or, in some cases, genuinely broken interactivity.
Split a large context into several smaller, more focused contexts, so a component only re-renders when the specific slice of data it actually depends on changes, rather than re-rendering whenever any part of one large, combined context changes. Memoizing the context's value object itself also prevents an unnecessary re-render caused simply by a new object reference being created on every single render of the provider.
Lead (8-10 years)
SSR renders a page's initial HTML on the server for each request, sending fully-formed markup to the browser, which then hydrates it to make it interactive. Pure client-side rendering instead sends a mostly empty HTML shell, with JavaScript building the entire page in the browser after it loads. SSR generally improves initial load performance and search engine visibility, at the cost of added server-side complexity and load.
SSR renders a page fresh on every single request, which suits content that changes frequently or is personalized per user. SSG renders a page once, at build time, producing static HTML that's served identically to every visitor and can be served extremely fast from a CDN. SSG fits content that doesn't change per request, while SSR fits content that genuinely needs to be current or personalized on every load.
ISR lets a statically generated page be regenerated in the background after a specified time interval, without requiring a full site rebuild and redeploy for every single content update. It solves the problem of SSG becoming stale for content that changes occasionally, like a blog post being edited, without needing the full server-side rendering overhead of regenerating that page on literally every single request.
Content that rarely changes and doesn't need to be personalized per user, like a marketing page, fits SSG well. Content that changes frequently or is personalized, like a user's account dashboard, needs SSR or client-side rendering instead. Highly interactive, less SEO-critical parts of an application, like an internal admin tool, often work perfectly well with pure client-side rendering and don't need the added complexity of SSR at all.
It happens when the HTML rendered on the server doesn't exactly match what React would render on the client during hydration, often caused by using something like the current date, a random value, or a browser-only API like window during server rendering, where that same value or API produces different results (or doesn't exist at all) on the server versus in the browser.
A common approach reads an authentication token or session from a cookie during server-side rendering, so the initial HTML already reflects the correct authenticated (or unauthenticated) state, avoiding a visible flash of incorrect content before the client-side code catches up and corrects it.
React Server Components render exclusively on the server and never ship their component code to the client's JavaScript bundle at all, unlike traditional SSR, which still sends the full component code to the client for hydration afterward. This can meaningfully reduce the amount of JavaScript sent to the browser, particularly valuable for components that don't need any client-side interactivity in the first place.
Anything that doesn't need interactivity, event handlers, state, browser-only APIs, is a good candidate for a Server Component, since it ships zero JavaScript for that piece to the browser. Anything that genuinely needs to respond to user interaction or hold client-side state needs to be a Client Component, marked explicitly, and the general approach is pushing interactivity as far down the component tree as possible, keeping as much of the tree server-rendered as the actual functionality allows.
Build components that are genuinely flexible through well-thought-out props rather than hardcoded to one specific application's exact needs, document them thoroughly (a tool like Storybook helps a lot here), and version the library properly so consuming applications can upgrade deliberately rather than being forced onto every change immediately and unexpectedly.
Micro-frontends split a large frontend application into smaller, independently deployable pieces, often owned by different teams, similar in spirit to microservices on the backend. It makes sense once a frontend has grown large enough that multiple teams are genuinely stepping on each other working in the same codebase, but it adds real coordination overhead and shouldn't be adopted preemptively before that actual organizational pain exists.
Options include a shared, minimal global event bus, URL-based state that each micro-frontend can read independently, or a very thin, deliberately limited shared state layer specifically for the small amount of data that genuinely needs to cross micro-frontend boundaries. Keeping that shared surface area as small as possible is important, since it's exactly the coupling that a micro-frontend architecture is otherwise trying hard to avoid.
Follow semantic versioning strictly, treating a prop rename or removal as a breaking major version change with a documented migration path, while a new optional prop or feature is a safe minor version bump. Consuming teams then need confidence they can read a changelog and safely decide when and whether to actually upgrade, rather than a breaking change surprising them unexpectedly in what looked like a routine update.
I'd weigh actual, concrete needs, SEO requirements, initial load performance targets, whether server-side rendering solves a real problem the application currently has, against the real migration cost and the team's familiarity with the new framework's specific conventions. Migrating without a genuine, named driving need mostly just adds complexity without a matching, felt benefit.
Organize by feature rather than by technical type, grouping a feature's components, hooks, and related logic together, rather than one folder of every component and a separate folder of every hook across the entire application. This keeps related code physically close together and makes it far easier to reason about, change, or even eventually extract one feature area without needing to hunt across the entire codebase.
Staff (10+ years)
I'd weigh it by team ownership and deployment independence rather than defaulting to a separate application as automatically cleaner. If a different team owns it and needs to iterate and deploy independently, that argues for separation. Otherwise, a well-organized feature module inside the existing application usually ships faster and is simpler to maintain, and splitting prematurely mostly adds coordination overhead without a matching real benefit.
Run it incrementally. Get the codebase passing under the new version or pattern where possible, lean on existing test coverage to catch regressions, and migrate feature by feature or component by component rather than a single disruptive rewrite touching the entire codebase at once. A full stop-everything migration is rarely something the business will actually tolerate.
I look at whether it's solving the real problem or just its symptom, whether state is being managed and scoped sensibly rather than defaulting everything to global, and whether it's consistent with patterns already established elsewhere in the codebase. An inconsistent one-off pattern becomes a maintenance burden the whole team inherits later, so I'd rather ask pointed questions that surface the team's own blind spots than hand them a prescribed answer.
Automate what can be automated, linting, formatting, component-level testing requirements, enforced in CI so standards aren't a matter of opinion in code review. For architectural conventions that resist automation, I'd document the handful of decisions that actually matter, with the reasoning behind them, rather than a long style guide nobody reads end to end.
I'd weigh the concrete, measurable benefits against the real cost of retraining a team and migrating existing code that's already working. For most teams already productive with their current approach, that switch is worth making only when a specific, named limitation of the current approach is genuinely blocking something important, not as a general modernization exercise for its own sake.
First check whether it correlates with device or browser differences, a large, unoptimized list rendering fine on a powerful desktop can crawl on a lower-end mobile device. Real user monitoring, tracking actual performance metrics from real users rather than only from your own development machine, often reveals a very different picture than what shows up in a controlled, local testing environment.
Error boundaries catch a JavaScript error occurring anywhere in their child component tree, log it, and render a fallback UI instead of letting that one error crash and blank out the entire application. Placing error boundaries around genuinely independent sections of a page, rather than just one at the very root, means a failure in one section doesn't necessarily take down parts of the page that have nothing to do with it.
Treat the component's public prop interface as a contract. Adding a new optional prop is generally safe. Renaming or removing an existing one needs a documented deprecation period and direct communication with consuming teams before removal, rather than a silent breaking change that surfaces as someone else's broken build.
Mitigation first: roll back the most recent deploy if the timing lines up, since an unhandled JavaScript error crashing the whole render tree, with no error boundary in place to catch it, is a very common cause of exactly this symptom. I'd also check error monitoring and browser console logs from affected users to identify the actual failing component before writing a permanent fix.
Set and actually enforce a performance budget, a maximum acceptable bundle size and load time, checked automatically in CI, rather than letting it silently and gradually creep up feature by feature without anyone deliberately deciding that trade-off. Regular bundle analysis catches an unexpectedly large new dependency before it ships to every single user, not after.
This is a judgment question interviewers use to see how you reason under uncertainty, not to test a specific fact. A strong answer names the actual constraint that forced the decision, the realistic options that were genuinely on the table, why you picked one knowing it wasn't guaranteed to be right, and what you'd do differently with what you know now.
I'd walk through one of their actual components together, asking specifically which other parts of the application genuinely need that piece of state, and let them see firsthand that the honest answer is often just the one component itself. Seeing the actual, concrete answer to that question tends to change their instincts far more than a general rule about preferring local state.
I wouldn't push a disruptive full rewrite. I'd introduce the new pattern on new code first, let the team feel the concrete difference in readability or reduced boilerplate on something they already recognize, and let that build organic buy-in rather than mandating the change from the top down before anyone's actually seen the benefit for themselves.
I'd bring concrete performance data or a working prototype demonstrating the actual trade-off, rather than a vague, unsubstantiated concern about difficulty. Often there's a version that gets most of the desired user experience with meaningfully less performance cost, and showing that concrete alternative resolves the disagreement faster than simply pushing back on the original ask.
I'd translate the performance work into terms leadership already tracks: a conversion or bounce-rate metric that correlates with load time, a specific competitor comparison, or a customer complaint pattern already tied to slowness. Framed as a business metric with a number attached, it competes far better for prioritization than framed as a technical improvement for its own sake.




