Skip to the solution
HardFrontend13 min read

Build a data-fetching hook with caching, deduplication and retry

880 views

The problem

Question presented to candidate: "Several components on our dashboard load the same user, and each one fires its own request. Build a small useQuery-style data-fetching helper: components that ask for the same key should share one cached result and one in-flight request, and a failed request should retry a few times with backoff before showing an error. No libraries; a fake fetcher is fine."

Requirements:

  • useQuery(key, fetcher) (a hook in React, a composable in Vue, a service-backed helper in Angular) returns status, data, error, isFetching and failureCount.
  • Caching: a key fetched less than staleTime ago is served from cache with no new call.
  • Deduplication: concurrent callers of the same key share one promise, so the fetcher runs once.
  • Retry: on failure, retry up to retry times with exponential backoff (retryDelay * 2^attempt), then show the error.
  • invalidate(key) marks a key stale and refetches it for every mounted reader, keeping the old data on screen while it loads.

What a strong answer should cover:

  • The cache must live outside any one component (module scope or an app-wide service), keyed by a stable string.
  • Storing the in-flight promise in the cache entry is what makes dedupe work, and clearing it in finally is what lets the next fetch happen.
  • A subscription model so every reader re-renders when the entry changes: useSyncExternalStore in React, a shallowRef in Vue, a signal in Angular.
  • Retry with exponential backoff, a capped number of attempts, and keeping the error in state rather than throwing into the UI.
  • Stale-while-revalidate: show cached data while a background refetch runs.
  • Proof by counting calls to a fake fetcher, including React StrictMode running effects twice.

Clarifying questions expected:

  • "How fresh does the data need to be: is a few seconds of stale data acceptable?"
  • "Which errors should be retried? A 404 probably should not be, a 503 should."
  • "Do we need cancellation or garbage collection of unused cache entries for this round?"

Code / implementation expected: Yes. A working cache module plus the hook/composable/service, and a demo that shows the call counts.

  • Data fetching
  • Caching
  • Hooks

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

13 min read ยท 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { useState, useSyncExternalStore } from "react";
import UserCard from "./src/components/UserCard";
import { getCalls, subscribeCalls } from "./src/api/fakeApi";
import { invalidate } from "./src/api/queryClient";

function CallLog() {
  // Re-render on every fetcher call; the string is the snapshot.
  const summary = useSyncExternalStore(subscribeCalls, () =>
    Object.entries(getCalls())
      .map(([k, n]) => k + "=" + n)
      .join(" "),
  );
  return (
    <p className="log" data-testid="calls">
      Fetcher calls: {summary || "none"}
    </p>
  );
}

export default function App() {
  const [showThird, setShowThird] = useState(false);
  const [showBroken, setShowBroken] = useState(false);

  return (
    <main className="app">
      <h1>useQuery demo</h1>
      <CallLog />
      <section className="grid">
        <UserCard label="Header" id={1} testId="card-header" />
        <UserCard label="Sidebar" id={1} testId="card-sidebar" />
        <UserCard label="Flaky" id={2} testId="card-flaky" />
        {showThird && <UserCard label="Late" id={1} testId="card-late" />}
        {showBroken && <UserCard label="Broken" id={3} testId="card-broken" />}
      </section>
      <div className="actions">
        <button data-testid="btn-late" onClick={() => setShowThird((v) => !v)}>
          {showThird ? "Unmount" : "Mount"} late card
        </button>
        <button
          data-testid="btn-invalidate"
          onClick={() => invalidate("user/1")}
        >
          Invalidate user/1
        </button>
        <button data-testid="btn-broken" onClick={() => setShowBroken(true)}>
          Load broken user
        </button>
      </div>
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 85 of 107 in Machine coding

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

Build Connect Four

Continue