Scenario Debugging

Frontend and React Scenario Bank

Sixteen frontend cases: old data after a save, live dashboards that freeze, SEO on a client-rendered app, token expiry, flaky E2E tests, micro-frontends, design systems and step-by-step migrations.

Lesson 1222 min read#frontend#react#scenario#architecture

On this page

  1. The scenarios
  2. Interview questions
  3. Key takeaways
  4. What's next

In one line: sixteen "what would you do?" questions about frontend and React. They go from old data and flaky tests to micro-frontends, design systems and step-by-step migrations. Each is answered as Diagnose, Fix, Prevent.

The earlier lessons covered the basics of performance (Lessons 01 to 08). This lesson adds wider scenarios that come up in senior frontend interviews: product bugs, architecture and delivery.

The scenarios

1. "After saving a form the list still shows old data."

Diagnose: first open the Network tab. Did the list request run again after the save, and what did it return? Then think about the causes. The save worked, but the client cache was not refreshed. A client cache is a library's saved copy of server data (for example TanStack Query, also called React Query). Or a CDN (a network of servers that keep copies of pages) or HTTP cache is giving an old GET response. Or the component copied server data into local state and never updated it.

Fix: after the save, mark the right query as old with queryClient.invalidateQueries, so the library fetches it again. Or write the new value into the cache yourself with queryClient.setQueryData. For server-rendered pages in Next.js 16, call revalidatePath (rebuild the saved page for one path) or updateTag (inside a Server Action, so the next read already shows the new data). Outside a Server Action, use revalidateTag. Use Cache-Control: no-cache where fresh data matters. It lets the browser keep a copy, but it must ask the server if the copy is still good.

Prevent: do not copy server data into state. Keep all query keys in one place. Add a test: "create an item, then see it in the list". (Frontend Depth, Lesson 03)

2. "Users lose what they typed when they hit Back or refresh."

Diagnose: the form state lives only in the component's memory. Memory is cleared when the page reloads.

Fix: save drafts to sessionStorage (browser storage that lasts until the tab closes) or to the server. Wait a short time after the last key press before saving (this is debounce). Restore the draft when the component loads. Never store passwords, tokens or card data in a draft. Warn about unsaved changes with the beforeunload event (it fires when the user leaves the page), but only when there really are unsaved changes. Keep filters and wizard steps in the URL. Also think about the Back/Forward cache (bfcache, a browser feature that keeps whole pages in memory for a fast Back). Do not use unload handlers, because in many browsers they turn the bfcache off.

Prevent: make one rule for saving drafts in your form library. Test the back-button flow. (Browser Contract, Lessons 00 and 01)

3. "The app is fine on my laptop but unusable on older Android phones."

Diagnose: repeat the problem in Chrome DevTools. In the Performance panel, set CPU throttling to 4x or 6x slowdown, and set a slow network profile. Look at field data (real measurements from real users) for that kind of device. Common causes:

  • a large bundle (the phone must read and run all that code)
  • heavy hydration (the browser must attach React to server-made HTML to make it interactive)
  • long tasks (JavaScript that runs for more than 50 ms and blocks the screen)
  • big images
  • too many DOM nodes

Fix: send less JavaScript. Split code into parts (code splitting), use Server Components (React components that run only on the server and send no JavaScript to the browser), and choose lighter libraries. Delay work that is not urgent (lazy loading). Use virtual lists (draw only the visible rows). Serve images in the right size. Make animations simpler.

Prevent: set performance budgets for mobile settings in CI. Use RUM (real user monitoring, which collects speed numbers from real visitors) split by device class. Keep a test device in the team. (Lesson 06)

4. "A real-time dashboard receives 5,000 updates per second and the tab freezes."

Diagnose: each message causes a state update and a re-render. The main thread (the one thread that runs your page code and draws the screen) is full, and memory grows with the history.

Fix:

  • Batch the messages. Collect them in a plain array or a store outside React state, and flush it on a timer. Update the screen at most once per frame, or every 100 to 250 ms. requestAnimationFrame is a browser function that runs your code just before the next screen redraw.
  • Keep only the latest value for each key, not every event.
  • Update only the rows that changed. Use stable keys, memoised rows and selectors. Memoised means remembered: a memoised row (with React.memo) does not redraw if its data is the same. A selector is a small function that picks one piece of the data, so a component updates only when that piece changes.
  • Limit how much history you keep.
  • For very fast data, use sampling or summing on the server (send only some of the events, or totals).
  • Think about drawing charts on a <canvas> (one drawing surface instead of thousands of page elements) or in a Web Worker (a script that runs on a separate thread).

Prevent: agree with the backend on how to handle too much data (backpressure means a slow receiver tells the sender to slow down), and let the backend sum the data. Write a performance test with a message generator. (Lesson 07)

5. "Our React single-page app is not getting indexed, and shares look empty."

Diagnose: the content is drawn in the browser (client-rendered). Search crawlers and link-preview bots see only an empty shell, because many of them do not run JavaScript, or run it late. The page metadata is set only in JavaScript.

Fix: draw public pages on the server. SSR (server-side rendering) builds the HTML for each request. Static generation builds it once, at build time. Put the title, description, canonical and Open Graph tags for each page in the server HTML (in Next.js, use generateMetadata). A canonical tag tells search engines which URL is the main one. Open Graph tags tell chat apps and social sites what title and image to show in a link preview. Add a sitemap (a file that lists your pages for crawlers). Make navigation real links (<a href>). Do not hide important content behind lazy fetches that run only in the browser.

Prevent: in review, check "View source" and use a crawler tool. Run Lighthouse (Google's tool that scores pages) SEO checks in CI (the automatic build and test run). (Frontend Depth, Lesson 02)

6. "The API token expires while users are working, and they get kicked to the login page."

Diagnose: the app has no refresh handling. Or many requests try to refresh the token at once, and token rotation (each refresh token works only once) makes some of them fail. A token is a string that proves who the user is. An access token is short-lived, and a refresh token is used to get a new access token.

Fix: build one refresh-and-retry layer. When a request gets 401, refresh the token only once. Use one shared promise (an object that stands for a result that is not ready yet), so other requests wait for it. Then retry each original request once, so it cannot loop. If the refresh fails, log the user out cleanly and remember where they were. Also refresh a little before the token expires.

Prevent: write a test with expiring tokens and parallel calls. Use a BFF (backend for frontend, a small server for your UI), so the browser never handles tokens. (Browser Contract, Lessons 02 and 03)

7. "An optimistic update shows success, then the server rejects it."

Diagnose: an optimistic update shows the result before the server answers. Here the UI assumed success and had no plan for failure.

Fix: keep the previous state. Roll back if there is an error. Show a clear message and offer a retry. After that, match the screen to the server's answer (refetch in onSettled). Use idempotency keys (a unique key per action, which lets the server see a repeat and not do it twice), so a retry is safe. In React 19, useOptimistic is a hook that shows a temporary value while an action runs and goes back to the real value when the action ends, so a failed save rolls back by itself.

Prevent: test the failure path, not only the happy path. (Lesson 05)

8. "Typing in a big form (80 fields) is laggy."

Diagnose: record a typing session in the React DevTools Profiler. Usually each key press updates the parent state, so the whole form re-renders. Validation also runs on every field each time.

Fix: use a form library with uncontrolled inputs (the browser keeps the value, not React state), such as React Hook Form. Validate on blur (when the user leaves the field) or after a short delay. Split the form into memoised sections. Keep expensive widgets apart. Use useDeferredValue (a React hook that gives you a copy of a value that updates a little later, so typing stays fast) for previews that come from the typed values.

Prevent: profile big forms in review. Set a render budget. (Lessons 03 and 08)

9. "A bug appears only in Safari (or only on iOS)."

Diagnose: repeat the bug in the real browser or in an accurate emulator. For iOS, connect the phone to a Mac and use Safari's Web Inspector (Develop menu). Check the console and the network tab. Common causes:

  • a missing API or CSS feature
  • different date parsing (new Date("2026-05-01 10:00") uses a format that the JavaScript standard does not require browsers to accept; the ISO form 2026-05-01T10:00:00Z is guaranteed to parse; the Z, or an offset such as +02:00, makes the time zone clear)
  • 100vh (100% of the viewport height) and the browser toolbar that changes size
  • iOS zooms into inputs when their font size is smaller than 16 px
  • limits on storage and cookies (ITP, Intelligent Tracking Prevention, Safari's rules that delete or limit data from sites you do not visit often)
  • missing support for newer CSS in older versions (for example gap in flexbox before Safari 14.1)

Fix: use standard formats such as ISO 8601. Use feature detection (check if the feature exists before you use it) or polyfills (code that adds a missing feature). Use dvh units (dynamic viewport height, which follows the toolbar), 16 px inputs and @supports fallbacks (CSS that applies only when the browser supports a feature).

Prevent: keep a browser support matrix (based on your analytics). Run cross-browser E2E tests on key flows (Playwright can run WebKit, which is close to Safari but not identical, so check iOS-only bugs on a real iPhone). Use autoprefixer (a tool that adds vendor prefixes to CSS) and browserslist (a config that lists the browsers you support). Add linting for APIs that are not supported.

10. "State is out of sync between two open tabs."

Diagnose: each tab keeps its own state in memory.

Fix: for server state, refetch when the tab gets focus and use cache invalidation. For client state, sync with BroadcastChannel (a browser API that lets tabs of the same site send messages to each other) or the storage event (it fires in other tabs when localStorage changes). For login, send a logout message to all tabs. Decide which data really needs to match across tabs.

Prevent: treat the server as the source of truth. Test with two tabs. (Browser Contract, Lesson 01)

11. "Our end-to-end tests fail randomly in CI."

Diagnose: an end-to-end (E2E) test drives the real app like a user would. A flaky test passes sometimes and fails sometimes. Sort the flakes by cause:

  • timing (waiting with fixed sleeps)
  • shared state or data between tests
  • order dependence
  • network or third-party services
  • animation or screen-size differences
  • CI resource limits

Use traces (a recording of each step, for example the Playwright trace viewer), videos and how often each test fails. Repeat one test many times (--repeat-each) to reproduce it.

Fix: use web-first assertions and auto-waiting locators instead of sleeps. Give each test its own data. Mock third parties. Turn off animations. Retry once and save a trace while you fix the root causes. Move tests that are truly broken to a quarantine (a separate group that does not block merges), with a ticket.

Prevent: show the flake rate on a dashboard. Make a rule that a red CI is a bug. Keep the E2E suite small and add more lower-level tests. (Frontend Depth, Lesson 05)

12. "A third-party script (chat, analytics, ads) breaks the page for some users."

Diagnose: look for console errors from the vendor domain. Check if an ad blocker or a CSP (Content Security Policy, a browser rule about which scripts may load) blocks it. Look for a long task from that origin. Look for an unhandled error (one that no catch handles) that escapes.

Fix: load it with async, or late (in Next.js, next/script with strategy="lazyOnload"). Make it fail soft: the page must work without it. Wrap the integration code in try/catch and keep it isolated. Think about a self-hosted or lighter option. Remove it if it costs more than it gives.

Prevent: keep a list of third-party scripts with an owner for each. Set performance and error budgets. Use SRI (Subresource Integrity, a hash in the <script> tag that lets the browser check the file was not changed) where it fits. It works only for files that never change. Run a CSP in report-only mode to watch. (Browser Contract, Lesson 07)

13. "Design asks for a micro-frontend architecture so five teams can deploy independently. What do you consider?"

Think about trade-offs. A micro-frontend is a way to build one web app from several parts that different teams build and deploy on their own. It fits large organisations with truly independent areas of work. It also adds cost.

  • Composition (how the parts join): at build time (npm packages), at runtime (Module Federation, a webpack feature that loads code from another app while the page runs; import maps, which tell the browser where to find each module; or web components, which are custom HTML elements), by server or edge composition (the server or the CDN joins page fragments), or just separate routes or apps under one domain. The last one is the simplest.
  • Shared concerns: design system, login and session, routing, analytics, error handling, translations. Agree one shared contract for events and navigation.
  • Performance: many copies of the framework in the browser, slow chains of loading remote parts, and layout shift (the page jumps while parts load). Share dependencies on purpose and set budgets.
  • Consistency and UX: a shared design system and tokens, accessibility standards, and one navigation shell.
  • Versions and deploys: independent releases need contracts that stay backward-compatible and a rollback plan. Run integration tests across the boundaries.
  • Ownership: clear boundaries by business area, not by technical layer.

My answer: start with a modular monolith (one app that you deploy as a whole, with clear boundaries between its parts). Split only when team coordination, not code size, is the problem. If you split, prefer separation by route over sharing components at runtime.

14. "We need to roll out a design system across three products. How?"

Plan:

  1. Review the current UI and decide the scope.
  2. Define design tokens (named values for colour, spacing and type) as the shared source.
  3. Build accessible basic parts first (button, input, dialog). Add documentation and examples (Storybook is a tool that shows each component on its own page).
  4. Publish it as a versioned package with a changelog and codemods (scripts that update code for you) for breaking changes.
  5. Migrate step by step: new screens first, then the busiest ones. Products stay shippable.
  6. Set rules (governance): how to contribute, who owns what, and accessibility review.
  7. Measure adoption and visual regressions with screenshot tests.

Pitfalls: a big-bang rewrite, no versioning policy, parts that are not accessible, and a team that blocks others instead of helping them.

15. "Migrate a large JavaScript codebase to TypeScript (or CRA to Next.js) without stopping feature work."

Approach: go step by step, in the strangler style (replace the old system piece by piece). Set up the tools first. allowJs lets TypeScript accept .js files in the same project. Strict mode is a group of TypeScript checks. Keep it off for old code. Convert leaf modules (files that import little or nothing from the app) and shared utilities first. Require strict rules for new files, and tighten them directory by directory (a "ratchet"; for example, a second tsconfig file turns strict on for the converted folders). Add types at boundaries, such as API responses and shared data shapes, because that gives the most value. any is the TypeScript type that turns checking off. Track progress with a number, such as the percentage typed or the count of any. Keep CI green all the time. For a framework migration, run both side by side behind routing and move route by route. Keep shared components working in both. Have a rollback plan.

Avoid: a migration branch that lives for a long time, a rewrite for its own sake, and stopping delivery for months.

16. "Accessibility audit found 200 issues. Where do you start?"

Sort by impact and effort:

  1. Blockers on important user journeys, such as keyboard traps (focus enters an element and cannot leave), form controls with no labels, and missing focus handling in modals (focus must move into the dialog and come back when it closes).
  2. System-wide fixes in shared components. One fix clears many problems.
  3. Simple fixes that tools can find, such as colour contrast and missing alt text.
  4. The long tail of small issues.

Add lint and axe checks in CI, so the count only goes down. A linter reads your code for mistakes. axe is a tool that tests a page for accessibility problems. Accessibility means people with disabilities can use the page, for example with a keyboard or a screen reader. Automatic tools find only part of the problems, so test with a keyboard and a screen reader too. Make accessibility part of the definition of done. (Frontend Depth, Lesson 06)

Interview questions

Scenario: a dashboard receives thousands of updates per second and freezes. What do you do?

I batch and throttle the updates. I update the screen once per frame or every 100 to 250 ms. I keep only the latest value for each key. I update only the rows that changed, with stable keys and memoised components or selectors. I limit the history. I move heavy work to a worker or the server, and I ask the backend to sum the data. I check the result in the Performance panel.

Scenario: five teams want to deploy their parts of the UI independently. How do you design the frontend?

I look at the cost first. I would begin with a modular monolith and separation by route. I would use runtime micro-frontends (for example Module Federation or web components) only when team coordination is the real problem. I would define clear boundaries, a shared design system, and shared contracts for login and navigation. I would share dependencies with performance budgets. Each team gets its own versioned deploys with backward-compatible contracts, and I would test across the boundaries.

Scenario: the token expires mid-session and users are logged out. Fix it.

I put the refresh in one place, the HTTP layer. When a request gets 401, I do one shared refresh. Other requests wait behind it and then retry with the new token. I log the user out only if the refresh fails, and I remember where they were. I also refresh a little before expiry. I would think about a backend-for-frontend, so tokens never live in the browser.

Scenario: how do you migrate a large JavaScript app to TypeScript while shipping features?

I go step by step. I turn on allowJs. I type shared utilities and API boundaries first. I require strict types for new files and tighten directory by directory. I track the any count and keep CI green. I avoid a branch that lives for a long time. I measure progress and treat the work as ongoing, next to features.

Scenario: E2E tests are flaky in CI. What is your approach?

I sort the failures by cause: timing, shared data, order, third parties, animation and resources. I use traces to find root causes. I replace sleeps with assertions that wait automatically. I give each test its own data and mock outside services. I quarantine broken tests with a ticket instead of ignoring them. I keep the E2E suite small and test logic at lower levels.

Scenario: a design system must serve three products. What is your rollout plan?

I define tokens and accessible basic parts first, and I write documentation. I ship a versioned package with a changelog and codemods. I migrate new screens and busy screens step by step. I add visual regression tests and accessibility tests. I set rules for ownership, so the team helps others and does not block them.

Key takeaways

  • Frontend scenarios are about caches, the order of async work, who owns state, real devices, and delivery. They are not only about React APIs.
  • Step-by-step migration beats a rewrite. Clear boundaries and contracts make micro-frontends and design systems work.
  • Always link your answer to a measurement, a guard and a rollout plan.

What's next

Lesson 13: Security and Auth Scenario Bank. Fourteen "you found a flaw" scenarios, from XSS in a profile bio to a secret leaked in git history.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Related series