CASE STUDY

PDF Viewer Search Box that Cycles Through Matches (React)

4 min read·611 words·Intermediate

Asked at

1 candidate report in Jul 2026

How to use this case study

SDE-2 / Mid

Design the components (search box, match counter, next/prev buttons, page renderer) and how the search term highlights matches.

SDE-3 / Senior

Choose state management (context vs Redux/Zustand/MobX), communicate between the search box and pages (pub/sub), and scroll to matches on pages not yet rendered.

Staff / Principal

Handle huge PDFs (text index in a web worker, virtualized pages), performance budgets, accessibility and testing.


0) Problem Restatement

Adobe asked a frontend system design question: in a PDF viewer, build a search box. The user types a word, the viewer highlights all matches in the whole PDF, shows "match 3 of 27", and Next / Previous buttons (and Enter / Shift+Enter) cycle through matches, scrolling to each one, even on pages that aren't rendered yet. The interviewer discussed pub/sub, React context, and state libraries like Redux, Zustand and MobX.


1) Components

Architecture Diagram

flowchart LR
    SB["SearchBox - input, counter, next/prev"] -->|"setQuery / next / prev"| STORE["Search store"]
    STORE --> VIEW["PdfViewer - virtualized page list"]
    VIEW --> PAGE["Page - canvas + text layer + highlights"]
    WK["Web worker - text index + search"] --> STORE
    STORE --> WK
  • SearchBox: input (debounced), "i of N", next/prev buttons, and keyboard shortcuts.
  • PdfViewer: a virtualized list of pages (only visible pages are rendered).
  • Page: draws the page (canvas) plus a text layer (invisible positioned text spans, like PDF.js), and draws highlight boxes for matches on that page.
  • Search store: { query, matches: [{page, start, end}], activeIndex }.


2) Searching

  • At load time, extract text per page (PDF.js getTextContent) in a Web Worker, so the UI thread isn't blocked. Build a per-page text string plus a mapping from character offsets to text spans (for highlight positions).
  • On query (debounced ~200 ms): the worker finds all matches (case-insensitive, normalized whitespace) across pages and returns an ordered list (page, charStart, charEnd). Cancel an old search if the user keeps typing.
  • For large PDFs, stream results back page by page, so the counter grows ("27+ matches...").


3) State and Communication

  • Where to keep state: a small store shared by the search box and the pages. Options:
  • React Context + useReducer: fine, but any change re-renders every consumer. Avoid it for fast-changing state if many pages subscribe.
  • Zustand / Redux with selectors (our choice): each Page subscribes only to its own matches and whether the active match is on it (select(s => s.matchesByPage[pageNo])), so moving to the next match re-renders just 1–2 pages.
  • MobX: observable state with fine-grained updates, which also works well.
  • Pub/sub view: the store acts as the event bus. The search box publishes next(), and pages and the viewer react to activeIndex changes.


4) Cycling to a Match on an Unrendered Page

  1. next() sets activeIndex = (activeIndex + 1) % N (wrapping around).
  2. The viewer reads the active match's page, and scrolls the virtualized list to that page (we know each page's height from PDF metadata, so we can compute the offset without rendering).
  3. When the page mounts, it renders its text layer and highlights, then scrolls the exact match rectangle into view (centered).
  4. The active match gets a stronger highlight color, and all others a lighter one.


5) Performance and Accessibility

  • Only visible pages (plus a buffer) have canvases. Highlights are simple absolutely positioned divs over the text layer.
  • The search runs in a worker, and the result list is compact. Memoize per-page highlight rectangles.
  • Accessibility: aria-live announcements ("Match 3 of 27"), keyboard shortcuts (Ctrl+F focuses the box, Enter/Shift+Enter navigates, Esc clears), and visible focus.
  • Tests: unit tests for the store logic (wrap-around, empty results), and integration tests that navigate to a match on page 80.


6) Wrap-Up

Extract page text in a web worker and search it there, producing an ordered list of (page, start, end) matches. Keep query, matches and the active index in a selector-based store (Zustand/Redux or MobX), so only affected pages re-render. Cycle with wrap-around, scroll the virtualized viewer to the target page using known page heights, then scroll the exact highlight into view once the page's text layer renders, with debounced input, keyboard shortcuts and live announcements.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →