Skip to solution
hardFrontend

cache() and fetch memoization — request deduplication in RSC

483 views
01

Understand the problem

Question presented to candidate: "Several Server Components each need the current user. Do you thread it down from the top, or fetch it in each one?"

What a strong answer should cover:

  • React.cache() memoises a function for the duration of one server render pass. Identical calls with identical arguments return the same result and run the underlying work once.
  • That is what makes colocation safe: each component fetches what it needs, and the deduplication means N components produce one request rather than N.
  • The scope is deliberately narrow — per request, not a persistent cache. Two different users' page renders never share results.
  • Frameworks additionally extend fetch itself with request memoisation, so identical fetch calls dedupe without wrapping.
  • Cache keys are the arguments, compared by identity — so passing a fresh object each call defeats it.
  • It only works inside a React server render. Verified: outside one it does not deduplicate at all.
  • Why not just fetch at the top and pass down: that reintroduces prop drilling and couples every component to its parent's data-loading.
  • Related but distinct from HTTP caching, unstable_cache, and a client query library — different lifetimes entirely.

Clarifying questions expected:

  • "Is this per-request deduplication, or caching across requests? Those are different tools."
  • "Are we in an RSC framework, or a client-only app?"

Code / implementation expected: Yes — a cached data function called from several components.

cachersc
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes Server Components basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. A note on verification: cache()'s deduplication on

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A cache-style memo shared by several components, and the key gotcha
Run Playground
import { useState } from "react";

// This playground is a CLIENT environment, so React.cache() cannot dedupe here
// — it only operates inside a server render pass. This is a faithful stand-in
// so the SEMANTICS are demonstrable: memoise by argument identity, for the
// duration of one pass, then discard.
function createRenderCache(fn) {
  const store = new Map();
  const wrapped = (arg) => {
    if (store.has(arg)) return store.get(arg);
    const result = fn(arg);
    store.set(arg, result);
    return result;
  };
  wrapped.reset = () => store.clear();     // a new "render pass"
  return wrapped;
}

let queries = 0;
const getUser = createRenderCache((id) => {
  queries++;                               // stands in for a database round trip
  return { id, name: "User " + id };
});

// Three independent components, each asking for what IT needs. No prop
// drilling, no coordination — the deduplication makes this cost one query.
function Header() {
  const user = getUser(1);
  return <Row where="Header" text={"welcome, " + user.name} />;
}
function Sidebar() {
  const user = getUser(1);
  return <Row where="Sidebar" text={user.name} />;
}
function Profile() {
  const user = getUser(1);
  const other = getUser(2);                // a different key: its own query
  return <Row where="Profile" text={user.name + " and " + other.name} />;
}

// ❌ The key gotcha: an object literal is a new reference every call, so the
//    memo never matches and every call runs the work again.
function Careless() {
  const a = getUser({ id: 1 });
  const b = getUser({ id: 1 });
  return <Row where="Careless" text={"two calls, ids " + a.id + " and " + b.id} />;
}

function Row({ where, text }) {
  return (
    <p style={{ margin: "3px 0", fontSize: 14 }}>
      <code style={{ display: "inline-block", minWidth: 90 }}>{where}</code>
      {text}
    </p>
  );
}

export default function App() {
  const [pass, setPass] = useState(0);
  const [careless, setCareless] = useState(false);

  const nextPass = () => {
    getUser.reset();                       // the cache lives for ONE pass only
    queries = 0;
    setPass((p) => p + 1);
  };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <div key={pass} style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12 }}>
        <Header />
        <Sidebar />
        <Profile />
        {careless && <Careless />}
      </div>

      <p style={{ fontSize: 14, marginTop: 10 }}>
        underlying queries this pass: <strong>{queries}</strong>{" "}
        <span style={{ color: "#666", fontSize: 13 }}>
          (3 components want user 1, plus user 2 — expect 2)
        </span>
      </p>

      <p>
        <button onClick={nextPass}>start a new render pass</button>{" "}
        <button onClick={() => setCareless((c) => !c)}>
          {careless ? "remove" : "add"} the object-key component
        </button>
      </p>

      <p style={{ color: "#666", fontSize: 13 }}>
        Add the careless component: the query count jumps by two, because
        <code> {"{ id: 1 }"} </code> is a fresh object each call and never
        matches a previous key. Pass primitives.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 102 of 119 decoded in the React.js track. One more won't hurt.

Back to track