Episode 2 — React Frontend Architecture NextJS / 2.6 — Component Architecture Principles
Interview Questions: Component Architecture Principles
How to use this material:
- Read each question and formulate your answer before reading the model answer
- Practice speaking your answer out loud — interviews are verbal
- Pay attention to "Why interviewers ask" — it reveals what they're really testing
- Time yourself: aim for 2-3 minute answers, not monologues
- Focus on the Advanced section for senior-level interviews
Beginner (Q1–Q6)
Q1. What is the Single Responsibility Principle in React?
Why interviewers ask: Testing whether you understand basic architectural principles and can apply them to React.
Model answer: The Single Responsibility Principle means each component should have only one reason to change. In practice, a component should serve one cohesive purpose — for example, a SearchBar handles search input, while a UserCard displays user info. The test is: list all reasons the component might need to change. If there are more than 2-3, it should be split. Common violations include components with 7+ state variables, multiple unrelated useEffect calls, or names that require "and" to describe. SRP leads to components that are easier to read, test, reuse, and review.
Q2. What are presentational and container components?
Why interviewers ask: Testing separation of concerns — a fundamental architectural concept.
Model answer: Presentational components handle how things look — they receive data via props, render DOM, and have no side effects or data fetching. Container components handle how things work — they fetch data, manage state, and pass results to presentational children. For example, a ProductCard (presentational) receives a product object and renders it, while ProductCardContainer uses a useProduct hook to fetch data and passes it down. In modern React with hooks, the container is often replaced by a custom hook, but the underlying principle of separating data logic from UI remains essential, especially for design system components and testability.
Q3. What does "lifting state up" mean?
Why interviewers ask: Testing understanding of React's data flow model.
Model answer: Lifting state up means moving shared state to the nearest common ancestor of the components that need it. React has unidirectional data flow — data flows down via props, events flow up via callbacks. When two siblings need the same data, neither can access the other's state directly, so we move the state to their parent. For example, if SearchBar and SearchResults both need the search query, the query state lives in their parent component. The parent passes the value as a prop to both children and a setter function to SearchBar so it can update the query. The key principle is "single source of truth" — one component owns the state, others derive from it.
Q4. What is prop drilling?
Why interviewers ask: Checking awareness of common React pain points and solutions.
Model answer: Prop drilling occurs when you pass props through intermediate components that don't use them, just to deliver data to deeply nested children. For example, passing user through Layout → Header → NavBar → UserMenu where only UserMenu actually uses it. It's problematic because intermediate components become coupled to data they don't use, adding a new prop requires changing every component in the chain, and refactoring the tree breaks the chain. Solutions include component composition (passing elements as children instead of data as props), React Context for app-wide data like auth or theme, or state management libraries like Zustand for frequently changing data. The general rule: 1-2 levels of passthrough is fine; 3+ needs a solution.
Q5. What is the children prop in React?
Why interviewers ask: Testing understanding of React's core composition mechanism.
Model answer: children is a special prop that contains whatever JSX is placed between a component's opening and closing tags. It's React's primary composition mechanism. For example, <Card><p>Hello</p></Card> passes <p>Hello</p> as Card's children prop. Children can be strings, elements, arrays, or even functions (render props). This enables containment — components that don't know their content ahead of time, like Modal, Card, or Layout. You can also use React.Children.map to iterate and cloneElement to inject props, though these are considered escape hatches. Named slots (passing ReactNode through other props like header, footer) extend this pattern for multiple insertion points.
Q6. What is component composition and why does React prefer it over inheritance?
Why interviewers ask: Testing understanding of React's design philosophy.
Model answer: Composition means building complex components by combining simpler ones, using props and children rather than class inheritance. React prefers composition because it's more flexible — you can mix and match behaviors without rigid hierarchies. Instead of class DangerButton extends Button, you write <Button variant="danger"> using props. The React team has said they've never found a use case at Facebook requiring inheritance hierarchies. Composition patterns include: children for containment, named slots for multi-slot layouts, specialization for pre-filling props, compound components for shared implicit state, and render props for consumer-defined UI. Major libraries like Radix UI, React Router, and shadcn/ui all use composition patterns.
Intermediate (Q7–Q12)
Q7. How do you decide when to split a component? What's the decision framework?
Why interviewers ask: Testing practical judgment, not just theoretical knowledge.
Model answer: I use several signals. First, the "reason to change" test — list all reasons the component might change. More than 3 means split. Second, code smells: 7+ useState, 3+ useEffect, comment section dividers in JSX, mixed abstraction levels, or needing "and" to describe the component. Third, practical checks: is the component > 200 lines? Do I need to test part of it in isolation? Will part be reused elsewhere? If splitting, I identify responsibility clusters — group related state, effects, and UI into natural units. Then extract custom hooks for data/logic concerns and child components for UI sections. The parent becomes a pure compositor. I also guard against over-splitting — if the result requires 10 files to understand one feature, or components are just 5-line wrappers, I've gone too far. The sweet spot is 5-10 files of 30-80 lines each.
Q8. Compare Context API, Zustand, and prop drilling for state sharing. When do you use each?
Why interviewers ask: Testing ability to make architectural trade-offs.
Model answer: Prop drilling is appropriate for 1-2 levels of passthrough — it's explicit, traceable, and zero-overhead. Context API is best for app-wide, infrequently-changing data like auth, theme, or locale. It re-renders all consumers on any value change, so I separate contexts by rate of change. Zustand is better for frequently-changing state or when I need selective subscriptions — it's only ~1KB, requires no Provider wrapper, and only re-renders components that subscribe to the specific changed data. My decision framework: adjacent siblings → lift state with props. 3+ levels apart, infrequent changes → Context. 3+ levels, frequent changes or performance-sensitive → Zustand. Shareable via URL → useSearchParams. Server data → TanStack Query.
Q9. Explain the compound component pattern with a practical example.
Why interviewers ask: Testing knowledge of advanced composition patterns.
Model answer: Compound components are a set of components that work together by sharing implicit state through Context, providing a clean declarative API. The classic example is <select> + <option> — they're designed to work together. In React, I'd implement a Tabs system: <Tabs> is the parent that manages activeTab state via Context. <Tabs.List> renders the tab bar, <Tabs.Tab value="x"> reads the context to highlight when active and calls setActiveTab on click, and <Tabs.Panel value="x"> renders only when its value matches activeTab. The consumer API is declarative and flexible — they compose tabs however they want. I attach sub-components as properties: Tabs.Tab, Tabs.Panel. This is the pattern used by Radix UI, Headless UI, and shadcn/ui. The key benefit: the consumer controls the layout and content while the library handles the behavior and state.
Q10. How does lifting state up affect performance, and how do you mitigate it?
Why interviewers ask: Testing performance awareness in architecture decisions.
Model answer: When state lives in a parent, every state change re-renders the parent and ALL its children — even those that don't use the changed state. For example, typing in a search bar that's lifted to a parent will re-render sibling components like charts or tables on every keystroke. Three mitigations: First, React.memo on expensive siblings that don't depend on the changing state — they skip re-rendering if their props haven't changed. Second, keep state as low as possible — if only SearchBar and Results need the query, wrap them in a SearchSection sub-parent so other siblings aren't affected. Third, the composition pattern — pass children as props so they're created in a different scope and don't re-render when parent state changes. For frequently-updated state (typing, dragging), I might use useRef for the value and useState only for committed values, or debounce the state updates.
Q11. How do you solve prop drilling with component composition (without Context)?
Why interviewers ask: Testing whether you know the simpler solution before reaching for complex tools.
Model answer: Instead of passing data through intermediaries, the parent assembles the components and passes the assembled elements as slots. For example, instead of <Layout user={user}><Header user={user}><NavBar user={user}><UserMenu user={user} /></NavBar></Header></Layout> where Layout, Header, and NavBar just pass user through — I restructure: <Layout header={<Header right={<UserMenu user={user} />} />}>. Now Layout receives an already-assembled header element, Header receives an already-assembled UserMenu. Neither needs to know about user. The parent (App), which already has user, connects it directly to UserMenu when composing the elements. This works for layout patterns with clear "slots" — header, sidebar, footer, actions. It doesn't work well when many scattered leaf components need the same data — that's when Context is appropriate.
Q12. A designer gives you a complex dashboard mockup. Walk me through how you'd decompose it into components.
Why interviewers ask: Testing real-world architectural thinking.
Model answer: I'd follow a systematic process. First, I identify visual boundaries — draw boxes around distinct UI regions. A dashboard might have: top nav, sidebar, stat cards row, main chart, and activity feed. Second, I look for repetition — stat cards repeat, activity items repeat, navigation items repeat. These become list items. Third, I identify data domains — stats need analytics data, activity needs event data, nav needs auth data. Each domain suggests a container boundary. Fourth, I map interaction patterns — does clicking a stat card filter the chart? That means shared state between stat cards and chart. Fifth, I determine the component tree: a top-level DashboardPage orchestrator that fetches data via hooks, layout components (DashboardLayout with slots), and presentational components for each visual unit (StatCard, Chart, ActivityFeed, ActivityItem). Each component follows SRP — one purpose. I'd create a quick outline before coding, identify shared state that needs lifting, and decide early which data goes in hooks vs props vs context.
Advanced (Q13–Q18)
Q13. How do you design a component library that maximizes reusability across different teams?
Why interviewers ask: Testing system design thinking at scale.
Model answer: The key principle is that library components must be purely presentational — zero data fetching, zero business logic, zero side effects. They accept all data via props and signal user intent via callbacks. I structure with three layers: primitives (Box, Text, Stack — layout atoms), components (Button, Card, Input, Modal — visual units), and patterns (DataTable, FormField — composed from components). All use the compound component pattern for flexibility — <Card><Card.Header /><Card.Body /></Card> rather than <Card header="..." body="..." />. I ensure composability through children, named slots, and the polymorphic as prop. TypeScript is mandatory for the API surface — discriminated unions for variants, generics for data-driven components. I use Storybook for documentation and visual testing, with stories for every state. The critical rule: never make assumptions about where data comes from. A UserCard receives a user object; it doesn't call useUser(). This lets every team use the same components regardless of their data layer.
Q14. When do the smart/dumb and lifting-state patterns break down? What are the escape hatches?
Why interviewers ask: Testing depth of understanding beyond textbook patterns.
Model answer: Smart/dumb breaks down in three cases. First, when the same component needs different data in different contexts — you end up with many container variants. The solution is hooks: useProduct extracts the data logic, any component can use it. Second, when state needs to be shared across routes or deeply nested trees — lifting state reaches the root and prop drilling becomes unbearable. The solution is global state (Context/Zustand) or server state libraries (TanStack Query). Third, with Server Components in Next.js — the server/client boundary creates a different kind of smart/dumb separation that doesn't map cleanly to the traditional pattern. Lifting state breaks down when: the component tree is too deep (3+ passthrough levels), state is needed in unrelated tree branches, or updates are too frequent (each keystroke re-renders the whole subtree). Escape hatches: refs for imperative state that doesn't trigger renders, useReducer for complex state transitions within one component, event buses for truly decoupled communication (rare in React), and URL state for shareable state.
Q15. How would you architect state management for a large-scale application with 100+ components?
Why interviewers ask: Testing ability to make scalable architectural decisions.
Model answer: I'd use a layered approach with clear boundaries. Server state (API data) goes in TanStack Query — it handles caching, refetching, optimistic updates, and deduplication. This eliminates 60-70% of what people put in global stores. Client state splits into three categories: global (auth, theme, feature flags) in Context or Zustand — things the whole app needs; feature-local state in feature-level hooks or Zustand stores — things like a checkout flow's multi-step state; and component-local state with useState — UI concerns like isOpen, activeTab. The critical principle: each piece of state has exactly one owner. I avoid duplicating server data in client stores. The folder structure mirrors this: /hooks for shared custom hooks, /stores for Zustand stores (one per domain), /contexts for React contexts. Each feature module has its own hooks/stores that aren't shared globally. I'd also establish conventions: no useState for server data, no global store for UI state, every new store goes through architecture review.
Q16. Compare the composition patterns of Radix UI, React Hook Form, and React Router. What design decisions do they share?
Why interviewers ask: Testing awareness of library design patterns in the ecosystem.
Model answer: All three use composition as their primary API. Radix UI uses compound components: <Dialog.Root>, <Dialog.Trigger>, <Dialog.Content> — each sub-component handles one concern (trigger opens, overlay dims, content positions) while sharing state via Context. The consumer controls rendering completely. React Hook Form uses a hybrid: useForm() returns a register function and handleSubmit — the hook provides behavior, the consumer composes their own inputs. It avoids controlled components for performance, using refs internally. React Router uses declarative composition: <Routes><Route path="/" element={<Home />} /> — routes compose like JSX, with nesting for layouts via <Outlet />. Shared decisions: (1) Headless or near-headless — behavior without forced styling. (2) Composable — user controls the DOM structure. (3) Progressive disclosure — simple API for common cases, escape hatches for complex ones. (4) Inversion of control — the library provides logic, the consumer provides UI. This is the direction modern React libraries are heading.
Q17. How do Server Components in Next.js change the traditional component architecture patterns?
Why interviewers ask: Testing knowledge of cutting-edge patterns.
Model answer: Server Components create a new kind of smart/dumb split at the module level. Server Components are "smart" — they can fetch data directly, access databases, read files, and use async/await. Client Components are the interactive UI layer — they handle state, effects, and event handlers. The key architectural change: the boundary is a module-level declaration ('use client'), not a runtime pattern. This means: data fetching moves from hooks to top-level async functions in Server Components; the props boundary between server and client must be serializable (no functions, no classes); and composition now has a constraint — Server Components can render Client Components but not vice versa (except through children). The practical architecture becomes: pages and layouts as Server Components (fetch data directly), interactive features as Client Components (forms, modals, real-time), and a clear serialization boundary between them. This often eliminates the need for loading states at the component level since data is available before render.
Q18. You're tasked with refactoring a 2000-line "god component" that's been in production for 3 years. What's your strategy?
Why interviewers ask: Testing real-world refactoring judgment and risk management.
Model answer: I'd approach this incrementally, not as a big-bang rewrite. Phase 1: Understand. Read the entire component, map responsibilities, identify state clusters, and trace the data flow. Write integration tests if none exist — these are my safety net. Phase 2: Extract utilities first. Pull out pure functions (calculations, formatting, validation) into utility files — zero risk, no behavior change. Phase 3: Extract custom hooks. Move each data-fetching effect and its associated state into a custom hook. The component calls the hook instead — same behavior, cleaner code. Phase 4: Extract leaf components bottom-up. Start with the innermost, most self-contained UI sections — these are easiest and safest. Phase 5: Create intermediate containers. Now that leaves are extracted, identify natural groupings and create mid-level components. Phase 6: The parent becomes a compositor — 20-30 lines that assembles child components and passes hooks' data to them. Throughout: make small PRs, one extraction per PR, run tests after each step, and never refactor while fixing bugs (or vice versa). If I break something, git revert immediately rather than debugging forward.