LIMITED TIME 🎁: Register now to get 60 minutes of AI Mock Interviewing for FREE!

Join
    Openai Interview Insights
    Openai logo
    Openai·Frontend Engineer·Technical Phone Screen·Senior
    Senior
    Jul 2026
    8

    Summary

    Frontend coding round at OpenAI for a frontend engineer role. The whole thing was one meaty component design question about building a streaming chat UI, with a bunch of follow-ups that got progressively more painful the further in you went.

    Questions Asked(8)

    System DesignTechnical Trade-offsAPI & Integrations
    A
    Author's notesFirst line only

    The state model tripped me up at first.

    Suggested Approach

    Start by outlining the component hierarchy and data flow before diving into implementation details, demonstrating systems thinking. Then walk through the streaming mechanism using the Fetch API with ReadableStream or EventSource, explaining how incremental state updates drive re-renders. Conclude by addressing edge cases like error handling, abort control, and UX considerations such as auto-scrolling and loading indicators.

    Pro tip: Mention using a ref to accumulate streamed chunks and batching state updates with a flush strategy (e.g., requestAnimationFrame or a small debounce) to avoid excessive re-renders on every token — this signals you understand the performance implications of high-frequency state updates, which is critical at OpenAI's scale.
    1

    Define Component Structure

    Sketch a clear hierarchy: a top-level ChatContainer managing state, a MessageList rendering conversation history, a MessageBubble for individual messages, a StreamingMessage component for the in-progress assistant response, and a PromptInput for user submission. Keeping streaming logic isolated in StreamingMessage prevents unnecessary re-renders of the full list.

    2

    Design State Management

    Use a messages array in state (each with role, content, and optional isStreaming flag) plus a separate streamingContent string for the active response being built. Separating committed messages from the live stream avoids mutating history mid-flight and makes the data model predictable.

    3

    Implement the Streaming Fetch

    Use fetch() with the OpenAI Chat Completions API in streaming mode, reading response.body as a ReadableStream and decoding chunks with TextDecoder. Parse SSE-formatted data lines, extract delta content, and append each token to a ref-backed accumulator before syncing to state.

    4

    Handle Incremental Rendering & UX

    Flush the accumulated ref value to state on each chunk (or batched via requestAnimationFrame for performance), triggering a re-render of only the StreamingMessage component. Add auto-scroll-to-bottom behavior using a useEffect on message updates, a visible cursor/blinking indicator during streaming, and disable the input while a response is in flight.

    5

    Address Error Handling & Cleanup

    Attach an AbortController to the fetch so the user can cancel mid-stream, and clean up in a useEffect return. Handle network errors, malformed chunks, and the [DONE] SSE sentinel gracefully, finalizing the streamed content into the messages array and resetting streaming state.

    Key Points to Mention

    ReadableStream + TextDecoder for consuming SSE/chunked responses from the OpenAI API without a third-party library
    Separating in-flight streaming state from committed message history to keep the data model clean and avoid mutation
    Using a useRef accumulator to collect tokens and syncing to useState strategically to minimize re-render frequency
    AbortController for user-initiated cancellation and proper useEffect cleanup to prevent state updates on unmounted components
    Component isolation — rendering the streaming bubble as a dedicated component so only it re-renders on each token, not the entire message list
    UX polish: auto-scroll, typing cursor animation, disabling input during streaming, and graceful error/retry states
    Technical Trade-offsAPI & Integrations
    A
    Author's notesFirst line only

    Disabling the button is the obvious part.

    Suggested Approach

    Frame your answer around two distinct problems: preventing new duplicate submissions from being triggered, and gracefully handling the already-in-flight streaming request. Walk through a concrete implementation strategy that covers UI state management, request lifecycle control, and cleanup, demonstrating you've thought about both the user experience and the technical mechanics.

    Pro tip: Mention the AbortController API explicitly and discuss how you'd expose an abort signal to the streaming fetch/SSE connection — this signals hands-on experience with real streaming cancellation patterns rather than just theoretical knowledge, which is directly relevant to OpenAI's streaming API usage.
    1

    Disable the UI to prevent new submissions

    As soon as a stream begins, set a loading/streaming state flag (e.g., isStreaming: true) and use it to disable or hide the submit button. This is the first line of defense against duplicate submissions at the UI layer.

    2

    Track the in-flight request with a ref or controller

    Store an AbortController instance in a ref (e.g., useRef in React) tied to the current streaming request. This gives you a handle to cancel the ongoing stream if needed without triggering unnecessary re-renders.

    3

    Decide on a cancellation policy

    Define what happens if a new request must be initiated — either block it entirely until the stream completes, or implement 'cancel-and-replace' by calling abort() on the existing controller before starting a new one. Discuss the trade-offs of each approach with respect to UX and data consistency.

    4

    Abort and clean up the running stream

    If cancellation is chosen, call abortController.abort() to terminate the fetch/SSE connection, then clear partial streamed state and reset the UI. Handle the resulting AbortError gracefully so it doesn't surface as an unhandled error to the user.

    5

    Handle edge cases and cleanup on unmount

    Ensure the AbortController is also called in a cleanup function (e.g., useEffect cleanup) to prevent memory leaks or state updates on unmounted components. Consider race conditions where a response arrives just after an abort signal is sent.

    Key Points to Mention

    AbortController and AbortSignal for cancelling fetch or SSE-based streaming requests
    UI state flag (isStreaming / isLoading) to disable the submit button and block duplicate triggers
    Cancel-and-replace vs. queue-and-wait trade-off and when each is appropriate for UX
    Graceful AbortError handling to distinguish intentional cancellations from real network errors
    useRef (in React) to persist the controller across renders without causing re-renders
    Cleanup on component unmount to avoid state updates on unmounted components and memory leaks
    System DesignTechnical Trade-offs
    A
    Author's notesFirst line only

    Talked about batching updates with requestAnimationFrame or a small timeout, and appending to a ref instead of state for the in-progress text.

    Suggested Approach

    Frame your answer around the core tension between latency (showing updates quickly) and throughput (batching updates efficiently), then walk through concrete techniques like debouncing, buffering, and virtual DOM optimizations. Demonstrate that you understand both the browser rendering pipeline and React's reconciliation model, and explain how you'd measure and validate your approach.

    Pro tip: Mention that you'd use the browser's native requestAnimationFrame or scheduler APIs to align UI updates with the display refresh rate (typically 60fps), rather than naively re-rendering on every incoming chunk — this signals deep browser internals knowledge that separates senior candidates from mid-level ones.
    1

    Identify the Root Problem

    Explain that re-rendering on every chunk causes layout thrashing and excessive reconciliation work, especially when hundreds of chunks arrive per second over a streaming connection. Clarify that the goal is to decouple the data ingestion rate from the render rate.

    2

    Buffer and Batch Incoming Chunks

    Describe accumulating chunks in a mutable buffer (e.g., a ref or module-level variable) outside of React state, then flushing the buffer to state on a controlled schedule. This prevents each chunk from independently triggering a re-render.

    3

    Throttle or Schedule Renders

    Explain using requestAnimationFrame, setTimeout with a small interval (e.g., 16ms for ~60fps), or React 18's startTransition/useDeferredValue to schedule state updates at a human-perceptible cadence rather than at the network chunk rate.

    4

    Minimize Reconciliation Cost

    Discuss keeping the rendered component tree shallow and stable — for example, rendering the streaming text in a single leaf node rather than mapping each chunk to a separate element, and using React.memo or avoiding unnecessary parent re-renders.

    5

    Measure and Validate

    Emphasize profiling with Chrome DevTools Performance tab and React DevTools Profiler to confirm frame times stay under 16ms, and mention setting up synthetic benchmarks that simulate high-frequency chunk arrival to catch regressions.

    Key Points to Mention

    Buffering chunks in a mutable ref or external variable to decouple ingestion from React state updates
    requestAnimationFrame or time-based throttling (e.g., 16ms flush interval) to align renders with the display refresh rate
    React 18 concurrent features — startTransition and useDeferredValue — to mark streaming updates as non-urgent and avoid blocking user interactions
    Keeping the DOM diff minimal by appending to a single text node or string rather than creating new elements per chunk
    Web Workers or off-main-thread processing if chunks require heavy parsing or markdown rendering before display
    Profiling tools (Chrome Performance tab, React DevTools Profiler, PerformanceObserver) to measure real frame budgets and validate the solution
    Technical Trade-offsAPI & Integrations
    A
    Author's notesFirst line only

    Classic stale closure / race condition question.

    Suggested Approach

    Frame your answer around the real-world problem of race conditions in streaming APIs, then walk through concrete mitigation strategies you would implement on the frontend. Demonstrate that you understand both the symptom (stale/interleaved content) and the root cause (async, out-of-order delivery), and tie your solution to patterns like request IDs or AbortController.

    Pro tip: Mentioning that you would pair a unique request ID with an AbortController to cancel the previous stream — rather than just ignoring stale chunks — shows you understand both correctness and resource efficiency, which signals senior-level thinking to an OpenAI interviewer.
    1

    Define the Problem

    Clearly articulate the race condition: a slow streaming response from request N can deliver chunks after request N+1 has already begun rendering, causing corrupted or interleaved UI output. Establish why this is non-trivial in streaming (SSE/fetch ReadableStream) contexts.

    2

    Explain the Impact

    Describe the user-facing consequences — garbled text, incorrect state, or flickering — and any downstream side effects like incorrect token counts or broken markdown rendering. This shows you think beyond just 'wrong data' to actual UX degradation.

    3

    Present the Core Solution

    Introduce a request-ID or generation-counter pattern: tag each request with a unique ID, store the current active ID in a ref or closure, and discard any incoming chunk whose ID doesn't match the latest. Combine this with AbortController to cancel the previous fetch/stream at the network level.

    4

    Address Edge Cases

    Discuss scenarios like rapid successive requests (debouncing vs. immediate cancel), partial renders that must be cleared when a new request starts, and error handling when an aborted stream throws a DOMException. Show awareness of cleanup in useEffect or equivalent lifecycle hooks.

    5

    Mention Trade-offs & Alternatives

    Acknowledge trade-offs: aborting immediately wastes any useful partial output already received, while ignoring stale chunks risks subtle bugs. Briefly mention alternatives like queuing requests sequentially or using a state machine (e.g., XState) to manage streaming lifecycle for more complex UIs.

    Key Points to Mention

    Race condition / stale closure problem in async streaming (SSE or fetch ReadableStream)
    AbortController to cancel the in-flight previous request at the network level, freeing resources
    Request ID or generation counter pattern to tag and validate each incoming chunk before applying it to state
    Clearing or resetting UI state atomically when a new request begins to prevent partial renders from mixing
    Debouncing or throttling user input to reduce the frequency of superseded requests in the first place
    React-specific considerations: using useRef for the active request ID to avoid stale closure issues inside async callbacks
    System DesignTechnical Trade-offs
    A
    Author's notesFirst line only

    Didn't get deep into this one.

    Suggested Approach

    Frame your answer around the core tension between maintaining a smooth streaming UX and managing the growing complexity of a multi-turn conversation list. Start by identifying the key architectural challenges (virtualization, state management, scroll anchoring), then walk through concrete solutions for each. Tie your decisions back to measurable performance outcomes like frame rate and time-to-interactive.

    Pro tip: Mention 'scroll anchoring' and 'reverse infinite scroll' patterns explicitly — these are non-obvious pain points that senior engineers at AI chat companies deal with daily, and naming them signals you've shipped real chat UIs before rather than just theorized about them.
    1

    Define the Problem Space

    Articulate the two competing concerns: the DOM growing unboundedly as conversation turns accumulate, and the active streaming bubble needing high-frequency, low-jitter re-renders. Establish that naive approaches (re-rendering the full list on each token) will degrade quickly.

    2

    Virtualize the Message List

    Introduce a windowed/virtualized list (e.g., react-window, TanStack Virtual) to keep only visible message nodes in the DOM, capping render cost regardless of conversation length. Explain how dynamic row heights for variable-length messages require careful measurement caching.

    3

    Isolate the Streaming Bubble

    Decouple the actively streaming message from the virtualized list — render it as a separate, fixed component outside the scroll container so token updates never trigger list re-renders. Merge it into the virtualized list only once streaming is complete.

    4

    Handle Scroll Anchoring & Auto-Scroll

    Implement 'stick to bottom' behavior that auto-scrolls during streaming but gracefully pauses if the user scrolls up to read history. Use an IntersectionObserver on a sentinel element at the bottom to detect user intent and toggle auto-scroll without polling.

    5

    State Management & Memory Hygiene

    Store conversation turns in a normalized, append-only structure (e.g., a Map keyed by message ID) to avoid expensive array mutations. Discuss strategies like summarizing or paginating older turns server-side to cap client-side memory for very long sessions.

    Key Points to Mention

    Virtual/windowed list rendering (react-window, TanStack Virtual) to bound DOM node count
    Isolating the streaming bubble as a detached component to prevent cascading re-renders across the full message list
    Scroll anchoring: distinguishing between user-initiated scroll-up (pause auto-scroll) vs. programmatic scroll-to-bottom during streaming
    Using requestAnimationFrame or batched state updates to throttle token-level re-renders and maintain 60fps
    Normalized append-only state shape to make adding new turns O(1) without mutating existing message objects
    Server-side conversation truncation or summarization to prevent unbounded memory growth in very long sessions
    Technical Trade-offsAPI & Integrations
    A
    Author's notesFirst line only

    Call abort on the controller, transition status from streaming to something like stopped or idle, and make sure the partial text is preserved rather than wiped.

    Suggested Approach

    Frame your answer around the full lifecycle of a streaming request — from initiation to cancellation — and map each phase to concrete UI state and network-level actions. Walk through the problem systematically: what the user sees, what the client holds in memory, and what actually needs to be aborted on the wire. Conclude by addressing edge cases like partial responses and race conditions to show production-level thinking.

    Pro tip: Mention that cancelling the UI state and cancelling the network request are two separate concerns that must both be handled — many candidates only address one. Bonus points for noting that the server may still finish generating even after the client aborts, so you should design the backend to respect a cancellation signal (e.g., checking if the response stream is still being consumed).
    1

    Define the UI States

    Identify the distinct states the component needs: idle, streaming, and cancelled/stopped. Explain how a boolean or enum flag (e.g., `isStreaming`) drives button visibility and disables the input field during generation.

    2

    Set Up the AbortController

    When the user submits a prompt, instantiate an `AbortController` and pass its `signal` to the `fetch` call (or EventSource / WebSocket equivalent). Store the controller reference in a ref or state so the Stop button can access it.

    3

    Wire Up the Stop Button

    On click, call `abortController.abort()` to cancel the in-flight network request and catch the resulting `AbortError` in your stream-reading loop. Immediately transition UI state back to idle and preserve whatever partial response text has already been rendered.

    4

    Handle Partial Response & Cleanup

    Decide on the product behavior for partial text — typically you keep it visible but mark it as incomplete (e.g., a truncation indicator). Clear the AbortController reference, re-enable the input, and ensure no further state updates are applied from the now-dead stream.

    5

    Address Race Conditions & Edge Cases

    Guard against stale closures or async chunks arriving after abort by checking a cancellation flag before each state update. Also consider what happens if the user clicks Stop and immediately resubmits — ensure the old controller is fully cleaned up before creating a new one.

    Key Points to Mention

    AbortController / AbortSignal as the native browser mechanism for cancelling fetch requests mid-stream
    Separating UI state cancellation (isStreaming flag, button visibility) from actual network cancellation — both must happen
    Preserving and displaying partial streamed text after cancellation rather than discarding it
    Server-side awareness: the backend should detect a dropped connection and stop token generation to avoid wasted compute
    Race condition handling — guarding state updates after abort using a ref-based cancellation flag or checking signal.aborted
    Cleanup responsibilities: clearing the AbortController ref, re-enabling inputs, and resetting any loading/error states
    System DesignProduct Sense & Ideation
    A
    Author's notesFirst line only

    Honestly a nice practical question.

    Suggested Approach

    Frame your answer around a 'scroll lock' pattern: auto-scroll only when the user is already at (or near) the bottom, and pause it the moment they scroll up. Walk through the detection logic, the UX affordances you'd add, and edge cases like new messages arriving while the user is reading history.

    Pro tip: Mention using a small threshold (e.g., 50–100px from the bottom) rather than a strict equality check, and bring up the 'resume scroll' toast/button pattern used in products like Slack and Discord — it signals you've thought about real-world UX, not just the algorithm.
    1

    Define the Core Invariant

    Establish the rule: auto-scroll should fire only when the user is already pinned to the bottom. Any upward scroll by the user breaks this pin and suspends auto-scroll until they explicitly or implicitly re-engage.

    2

    Implement Scroll Position Detection

    Track whether the user is 'at the bottom' by comparing scrollTop + clientHeight against scrollHeight with a small tolerance threshold (e.g., 50px). Listen to the scroll event to toggle an isUserScrolledUp boolean flag.

    3

    Conditionally Trigger Auto-Scroll

    On each new message or content update, check the flag: if isUserScrolledUp is false, call scrollToBottom(); otherwise, suppress the scroll and optionally queue a 'new messages' indicator. Use requestAnimationFrame or a MutationObserver to scroll after the DOM has updated.

    4

    Provide a Re-Engagement UX Affordance

    Show a sticky 'Jump to latest' button or toast when new messages arrive while the user is scrolled up. Clicking it scrolls to the bottom and resets the pin flag, giving users a clear, non-disruptive path back.

    5

    Handle Edge Cases & Performance

    Address cases like streaming tokens (rapid DOM mutations), virtualized lists (where scrollHeight changes unpredictably), and mobile rubber-band scrolling. Debounce the scroll listener and consider using IntersectionObserver on a sentinel element at the bottom for a more performant detection approach.

    Key Points to Mention

    Scroll position detection with a tolerance threshold (scrollTop + clientHeight >= scrollHeight - threshold) rather than exact equality
    A boolean 'isUserScrolledUp' flag toggled by the scroll event to gate auto-scroll behavior
    IntersectionObserver on a bottom sentinel element as a performant alternative to polling scrollTop
    A 'Jump to latest' / 'New messages ↓' UI affordance that lets users opt back into auto-scroll without losing context
    Handling streaming / token-by-token content where the DOM updates dozens of times per second — debouncing or batching scroll triggers
    Virtualized list considerations (e.g., react-virtuoso, react-window) where scroll metrics behave differently and require library-specific APIs
    Technical Trade-offsSystem Design
    A
    Author's notesFirst line only

    Weak answer from me here.

    Suggested Approach

    Frame your answer around two distinct but related challenges: the accessibility problem (making dynamic, streaming content perceivable and operable for screen reader users) and the testing problem (making non-deterministic async behavior deterministic and reliable). Demonstrate awareness of ARIA live regions, chunked rendering strategies, and how to mock streaming APIs to write stable tests.

    Pro tip: Mention the tension between announcing every token (too noisy) versus batching announcements (introduces lag) — showing you've thought about the UX trade-off at the token level signals real-world experience with LLM streaming interfaces, which is directly relevant to OpenAI's products.
    1

    Identify the Core Accessibility Challenge

    Explain that screen readers don't automatically detect DOM mutations, so streaming text appended to the page is invisible to AT users without explicit ARIA instrumentation. Establish that the goal is to surface content progressively without overwhelming the user with per-token announcements.

    2

    Design the ARIA Live Region Strategy

    Propose using an `aria-live='polite'` region (or `assertive` for critical updates) and discuss chunking strategies — e.g., announcing every N tokens, every sentence boundary, or on punctuation — to balance responsiveness with cognitive load. Mention `aria-atomic` and `aria-relevant` attributes for fine-grained control.

    3

    Address Visual and Focus Management

    Discuss ensuring the streaming container is reachable via keyboard, that focus is not hijacked during streaming, and that a 'stop generating' control is keyboard-accessible and announced. Consider a visually-hidden status region that announces stream completion.

    4

    Design a Deterministic Testing Strategy

    Explain how to replace the real streaming API with a controlled mock that emits chunks synchronously or on a fake timer (e.g., using Jest fake timers or a ReadableStream stub), allowing tests to assert on intermediate and final DOM states without race conditions. Mention tools like Testing Library's `waitFor` or manual tick-stepping.

    5

    Validate Accessibility in Tests

    Describe running axe-core or jest-axe assertions after each simulated chunk to catch live-region misconfiguration, and using tools like NVDA/VoiceOver in manual QA to verify the announcement cadence feels natural. Emphasize combining automated checks with real AT smoke tests.

    Key Points to Mention

    ARIA live regions (`aria-live`, `aria-atomic`, `aria-relevant`) and when to use `polite` vs `assertive`
    Chunking/batching strategy to avoid per-token announcement noise while keeping latency acceptable
    Mocking `ReadableStream` or SSE with fake timers (e.g., Jest's `useFakeTimers`) for deterministic async tests
    Keyboard accessibility of streaming controls (pause/stop) and focus management during and after streaming
    Using axe-core / jest-axe for automated accessibility assertions within the test suite
    Trade-off between announcement frequency (responsiveness) and cognitive overload for screen reader users

    Discussion(8)

    Sign in to join the discussion.

    J
    Jamie_Clicks· 58d ago
    Q8How would you make streaming text accessible to screen reader users, and how would you test the streaming behavior in a deterministic way?

    The aria-live polite approach is correct but there's a subtlety worth knowing: some screen readers throttle polite regions if updates come too fast, so you might want to batch your live region updates the same way you batch visual updates, rather than updating it on every single token. Some implementations use a separate visually-hidden div as the live region and only push text to it every few hundred milliseconds. For testing, a fake ReadableStream backed by an async generator is the right shape. You yield chunks with small awaits between them, wrap it in a Response object so it looks like a real fetch response, and then your component code doesn't need to know it's fake. The deterministic part means controlling exactly when each chunk arrives, which lets you assert intermediate states like that the streaming bubble shows partial text before the final chunk lands.

    M
    MisterReview· 58d ago
    Q7How would you handle auto-scroll-to-bottom behavior without yanking the user's scroll position if they've scrolled up to read earlier messages?

    Nice practical question. I'd track a boolean in a ref (not state, no need to re-render) that flips to false when the user scrolls up past a threshold, say 100px from the bottom. On each new chunk, if that ref is true, call scrollIntoView or set scrollTop on the container. When the user scrolls back down to the bottom, flip the ref back to true. The scroll listener needs to be passive and probably debounced slightly so it's not firing on every pixel of scroll.

    A
    ArrayOfHope· 58d ago
    Q6Walk through adding a Stop Generating button. What state changes and what do you actually cancel?

    Pretty much exactly what you described. The one thing I'd add is that the button label itself can do some work here: during streaming it says Stop Generating, after stopped it could say Resume or just go back to the send icon. That label state is derivable from your status enum so you're not managing extra booleans. The AbortError catch is the part candidates most often miss, and it's the part that makes the feature feel polished versus janky.

    MT
    Marcus Thorne· 58d ago
    Q5How would you extend this to a multi-turn scrolling conversation while keeping the streaming bubble performant?

    Scoping the streaming updates to a ref inside the last bubble is the key insight. If your messages array lives in state and you append a character to the in-progress message on every chunk, every message bubble re-renders. Instead, keep the in-progress text in a ref and have only the active bubble subscribe to it, maybe via a context or just by passing a ref down. Older bubbles are static and never touch that ref, so they're completely inert during streaming. Virtualization is worth mentioning for very long conversations but the ref scoping gets you most of the way there without the complexity of a virtual list.

    D
    Dev_Dan92· 58d ago
    Q1Build a minimal chat interface where the user submits a prompt and the assistant's response streams in token by token. Walk through the component structure, state management, and how you'd handle the incremental rendering.

    The boolean trap is so real. I did the exact same thing at a different company, ended up with isLoading, isStreaming, isError, isDone, and then the conditional logic for what the button should look like became this gnarly nested mess. A status enum collapses all of that: one field, exhaustive switch, done. For the TextDecoder thing, don't be too hard on yourself, that's genuinely obscure. The reason you need to reuse the same instance with stream: true is that a multi-byte UTF-8 character like an emoji can be split across two separate Uint8Array chunks, and a fresh decoder on each chunk would misinterpret the partial byte sequence. The stateful decoder holds the incomplete bytes in an internal buffer and flushes them when the next chunk arrives. I only knew this because I hit the bug in production first and then went spelunking through the spec. The component structure I'd reach for is pretty flat: one parent that owns the status enum and the messages array, a controlled input, and a message bubble component that for the streaming case reads from a ref rather than state so you're not triggering a full React reconcile on every token. The ref gets flushed into state at natural pause points or on completion.

    J
    Jamie_Clicks· 58d ago
    Q4What happens if a slow chunk from a previous request arrives after a newer request has already started?

    Increment a requestId counter on each submission, capture the value in the closure, and after every await check if the captured id still matches the current ref. If not, return early. Two lines of code, prevents a whole class of bugs. AbortController handles the network side but this check handles the React side.

    N
    NullPointerNikki· 58d ago
    Q3A response can be hundreds of small chunks. How do you keep the UI responsive and avoid performance problems from re-rendering on every single chunk?

    The await-yields-to-event-loop point is the crux of it. Each iteration of your read loop does await reader.read(), and that await is a microtask checkpoint. The browser gets to process paint frames and input events between chunks. So the loop isn't blocking even though it looks synchronous. Where you can still get into trouble is if you call setState on every single chunk and React schedules a synchronous re-render each time. Batching helps: accumulate chunks into a ref, then flush to state either on requestAnimationFrame or after a small debounce. The visual difference is imperceptible to users but the frame budget difference is real.

    J
    Jordan_Fullstack· 58d ago
    Q2How would you prevent duplicate submissions while a stream is still in progress, and what do you do about the request that's already running?

    The abort-on-unmount piece is easy to forget and it matters a lot in a Next.js app where route transitions can leave a fetch dangling. But the user-abort-vs-network-error distinction is the actually interesting bit. When you call controller.abort(), fetch rejects with a DOMException whose name is AbortError. If you catch all errors the same way and set status to error, you get a red error banner when the user just clicked Stop, which looks broken. The fix is a simple name check in your catch block: if err.name === 'AbortError' and the abort was user-initiated (you can track this with a ref flag), transition to stopped or idle and keep the partial text. If it's a real network failure, then show the error state. One thing I'd add: store the AbortController in a ref, not state, so creating a new one for each submission doesn't trigger a render.

    Interview Details

    CompanyOpenai
    RoleFrontend Engineer
    RoundTechnical Phone Screen
    LevelSenior
    DateJul 2026

    Questions in this post

    Share your own experience

    Help the community by sharing what you went through.