Skip to the solution
HardFrontend6 min read

cache() and fetch memoization — request deduplication in RSC

483 views

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.

  • cache
  • rsc

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

6 min read · 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA cache-style memo shared by several components, and the key gotcha
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>
  );
}

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 102 of 119 in React.js

Keep your momentum going.

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

All React.js questions
Up nextHard

What is "tearing" in concurrent React?

Continue