Skip to content

Expose cacheSignal() alongside cache() #33557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 17, 2025

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jun 17, 2025

This was really meant to be there from the beginning. A cache():ed entry has a life time. On the server this ends when the render finishes. On the client this ends when the cache of that scope gets refreshed.

When a cache is no longer needed, it should be possible to abort any outstanding network requests or other resources. That's what cacheSignal() gives you. It returns an AbortSignal which aborts when the cache lifetime is done based on the same execution scope as a cache()ed function - i.e. AsyncLocalStorage on the server or the render scope on the client.

import {cacheSignal} from 'react';
async function Component() {
  await fetch(url, { signal: cacheSignal() });
}

For fetch in particular, a patch should really just do this automatically for you. But it's useful for other resources like database connections.

Another reason it's useful to have a cacheSignal() is to ignore any errors that might have triggered from the act of being aborted. This is just a general useful JavaScript pattern if you have access to a signal:

async function getData(id, signal) {
  try {
     await queryDatabase(id, { signal });
  } catch (x) {
     if (!signal.aborted) {
       logError(x); // only log if it's a real error and not due to cancellation
     }
     return null;
  }
}

This just gets you a convenient way to get to it without drilling through so a more idiomatic code in React might look something like.

import {cacheSignal} from "react";

async function getData(id) {
  try {
     await queryDatabase(id);
  } catch (x) {
     if (!cacheSignal()?.aborted) {
       logError(x);
     }
     return null;
  }
}

If it's called outside of a React render, we normally treat any cached functions as uncached. They're not an error call. They can still load data. It's just not cached. This is not like an aborted signal because then you couldn't issue any requests. It's also not like an infinite abort signal because it's not actually cached forever. Therefore, cacheSignal() returns null when called outside of a React render scope.

Notably the signal option passed to renderToReadableStream in both SSR (Fizz) and RSC (Flight Server) is not the same instance that comes out of cacheSignal(). If you abort the signal passed in, then the cacheSignal() is also aborted with the same reason. However, the cacheSignal() can also get aborted if the render completes successfully or fatally errors during render - allowing any outstanding work that wasn't used to clean up. In the future we might also expand on this to give different TaskSignal to different scopes to pass different render or network priorities.

On the client version of "react" this exposes a noop (both for Fiber/Fizz) due to disableClientCache flag but it's exposed so that you can write shared code.

This is exposed every but is a noop on the client by disableClientCache
flag just like its cache() sibling.
This was already implemented but never exposed.
Notably this is currently always creating a new signal rather than passing
the one you pass into the Web Streams APIs.

It might be better ot pass the original one through so that it can contain
other information such as TaskSignal to handle priorities. However, I'm not
sure if we'd want to wrap it with our own TaskSignal yet.
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Jun 17, 2025
@sebmarkbage sebmarkbage changed the title Expose cacheSignal() along side cache() Expose cacheSignal() alongside cache() Jun 17, 2025
@react-sizebot
Copy link

Comparing: 90bee81...f7d041a

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.02% 530.47 kB 530.57 kB +0.03% 93.64 kB 93.67 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.01% 651.56 kB 651.66 kB +0.03% 114.75 kB 114.78 kB
facebook-www/ReactDOM-prod.classic.js +0.01% 674.72 kB 674.81 kB +0.02% 118.75 kB 118.78 kB
facebook-www/ReactDOM-prod.modern.js +0.01% 665.20 kB 665.30 kB +0.02% 117.17 kB 117.19 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-flight-server.production.js +18.80% 2.09 kB 2.48 kB +13.95% 0.72 kB 0.82 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-flight-server.production.js +18.80% 2.09 kB 2.48 kB +13.95% 0.72 kB 0.82 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-flight-server.production.js +18.80% 2.09 kB 2.48 kB +13.95% 0.72 kB 0.82 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-flight-server.development.js +17.61% 2.45 kB 2.89 kB +12.55% 0.80 kB 0.90 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-flight-server.development.js +17.61% 2.45 kB 2.89 kB +12.55% 0.80 kB 0.90 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-flight-server.development.js +17.61% 2.45 kB 2.89 kB +12.55% 0.80 kB 0.90 kB
oss-experimental/react-suspense-test-utils/cjs/react-suspense-test-utils.js +4.26% 1.22 kB 1.27 kB +2.01% 0.60 kB 0.61 kB
oss-stable-semver/react-suspense-test-utils/cjs/react-suspense-test-utils.js +4.26% 1.22 kB 1.27 kB +2.01% 0.60 kB 0.61 kB
oss-stable/react-suspense-test-utils/cjs/react-suspense-test-utils.js +4.26% 1.22 kB 1.27 kB +2.01% 0.60 kB 0.61 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-flight-server.production.js +18.80% 2.09 kB 2.48 kB +13.95% 0.72 kB 0.82 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-flight-server.production.js +18.80% 2.09 kB 2.48 kB +13.95% 0.72 kB 0.82 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-flight-server.production.js +18.80% 2.09 kB 2.48 kB +13.95% 0.72 kB 0.82 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-flight-server.development.js +17.61% 2.45 kB 2.89 kB +12.55% 0.80 kB 0.90 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-flight-server.development.js +17.61% 2.45 kB 2.89 kB +12.55% 0.80 kB 0.90 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-flight-server.development.js +17.61% 2.45 kB 2.89 kB +12.55% 0.80 kB 0.90 kB
oss-experimental/react-suspense-test-utils/cjs/react-suspense-test-utils.js +4.26% 1.22 kB 1.27 kB +2.01% 0.60 kB 0.61 kB
oss-stable-semver/react-suspense-test-utils/cjs/react-suspense-test-utils.js +4.26% 1.22 kB 1.27 kB +2.01% 0.60 kB 0.61 kB
oss-stable/react-suspense-test-utils/cjs/react-suspense-test-utils.js +4.26% 1.22 kB 1.27 kB +2.01% 0.60 kB 0.61 kB
oss-stable-semver/react-server/cjs/react-server-flight.production.js +1.04% 58.69 kB 59.30 kB +1.50% 11.76 kB 11.94 kB
oss-stable/react-server/cjs/react-server-flight.production.js +1.04% 58.69 kB 59.30 kB +1.50% 11.76 kB 11.94 kB
oss-stable-semver/react/cjs/react.react-server.production.js +1.03% 13.31 kB 13.45 kB +0.74% 3.67 kB 3.70 kB
oss-stable/react/cjs/react.react-server.production.js +1.03% 13.33 kB 13.47 kB +0.65% 3.70 kB 3.72 kB
oss-experimental/react-server/cjs/react-server-flight.production.js +0.94% 63.70 kB 64.30 kB +1.41% 12.66 kB 12.84 kB
oss-experimental/react/cjs/react.react-server.production.js +0.73% 18.89 kB 19.03 kB +0.44% 4.99 kB 5.02 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.production.js +0.69% 87.11 kB 87.71 kB +0.97% 18.13 kB 18.30 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.production.js +0.69% 87.11 kB 87.71 kB +0.97% 18.13 kB 18.30 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.production.js +0.65% 88.33 kB 88.91 kB +0.96% 18.38 kB 18.56 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.production.js +0.65% 88.33 kB 88.91 kB +0.96% 18.38 kB 18.56 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.production.js +0.64% 91.71 kB 92.30 kB +0.98% 18.88 kB 19.07 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.js +0.64% 90.58 kB 91.15 kB +0.93% 18.86 kB 19.04 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.js +0.64% 90.58 kB 91.15 kB +0.93% 18.86 kB 19.04 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.production.js +0.64% 94.11 kB 94.71 kB +0.94% 19.23 kB 19.41 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.production.js +0.64% 94.11 kB 94.71 kB +0.94% 19.23 kB 19.41 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js +0.63% 94.52 kB 95.12 kB +0.94% 19.33 kB 19.52 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js +0.63% 94.52 kB 95.12 kB +0.94% 19.33 kB 19.52 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.production.js +0.61% 93.02 kB 93.59 kB +0.93% 19.16 kB 19.34 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.production.js +0.61% 94.55 kB 95.12 kB +0.87% 19.34 kB 19.51 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.production.js +0.61% 94.55 kB 95.12 kB +0.87% 19.34 kB 19.51 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js +0.61% 95.33 kB 95.91 kB +0.92% 19.51 kB 19.69 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js +0.61% 95.33 kB 95.91 kB +0.92% 19.51 kB 19.69 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.production.js +0.61% 95.35 kB 95.92 kB +0.91% 19.50 kB 19.68 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.production.js +0.61% 95.35 kB 95.92 kB +0.91% 19.50 kB 19.68 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.production.js +0.60% 98.71 kB 99.30 kB +0.94% 20.07 kB 20.26 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.js +0.60% 95.13 kB 95.70 kB +0.98% 19.62 kB 19.82 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js +0.60% 99.12 kB 99.72 kB +0.91% 20.17 kB 20.36 kB
oss-stable-semver/react-server/cjs/react-server-flight.development.js +0.58% 110.12 kB 110.75 kB +0.87% 20.34 kB 20.52 kB
oss-stable/react-server/cjs/react-server-flight.development.js +0.58% 110.12 kB 110.75 kB +0.87% 20.34 kB 20.52 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.production.js +0.57% 99.10 kB 99.67 kB +0.92% 20.10 kB 20.29 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.js +0.57% 100.62 kB 101.20 kB +0.88% 20.30 kB 20.48 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.js +0.57% 100.62 kB 101.20 kB +0.88% 20.30 kB 20.48 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js +0.57% 100.02 kB 100.59 kB +0.89% 20.39 kB 20.57 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.production.js +0.57% 100.04 kB 100.60 kB +0.89% 20.38 kB 20.56 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.production.js +0.57% 101.63 kB 102.21 kB +0.87% 20.51 kB 20.68 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.production.js +0.57% 101.63 kB 102.21 kB +0.87% 20.51 kB 20.68 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.js +0.57% 101.68 kB 102.25 kB +0.89% 20.52 kB 20.70 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.js +0.57% 101.68 kB 102.25 kB +0.89% 20.52 kB 20.70 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.js +0.54% 105.18 kB 105.75 kB +0.94% 21.07 kB 21.27 kB
oss-experimental/react-server/cjs/react-server-flight.development.js +0.54% 118.00 kB 118.63 kB +0.83% 21.76 kB 21.94 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.production.js +0.54% 106.19 kB 106.76 kB +0.88% 21.36 kB 21.54 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.js +0.54% 106.23 kB 106.80 kB +0.88% 21.37 kB 21.55 kB
oss-stable-semver/react/cjs/react.react-server.development.js +0.53% 28.70 kB 28.85 kB +0.32% 6.95 kB 6.97 kB
oss-stable/react/cjs/react.react-server.development.js +0.53% 28.72 kB 28.87 kB +0.32% 6.97 kB 6.99 kB
oss-experimental/react/cjs/react.react-server.development.js +0.42% 36.51 kB 36.66 kB +0.25% 8.68 kB 8.70 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.development.js +0.42% 152.63 kB 153.26 kB +0.64% 28.09 kB 28.27 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.development.js +0.42% 152.63 kB 153.26 kB +0.64% 28.09 kB 28.27 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.41% 153.42 kB 154.06 kB +0.63% 28.46 kB 28.64 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.41% 153.42 kB 154.06 kB +0.63% 28.46 kB 28.64 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.development.js +0.40% 158.09 kB 158.72 kB +0.61% 28.92 kB 29.09 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.development.js +0.40% 158.09 kB 158.72 kB +0.61% 28.92 kB 29.09 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.40% 160.47 kB 161.11 kB +0.62% 29.46 kB 29.65 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.40% 160.47 kB 161.11 kB +0.62% 29.46 kB 29.65 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js +0.40% 160.50 kB 161.13 kB +0.61% 29.46 kB 29.64 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js +0.40% 160.50 kB 161.13 kB +0.61% 29.46 kB 29.64 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.development.js +0.39% 160.87 kB 161.51 kB +0.68% 29.52 kB 29.72 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.39% 164.93 kB 165.56 kB +0.58% 29.99 kB 30.16 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.39% 164.93 kB 165.56 kB +0.58% 29.99 kB 30.16 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +0.38% 166.08 kB 166.71 kB +0.57% 30.26 kB 30.44 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +0.38% 166.08 kB 166.71 kB +0.57% 30.26 kB 30.44 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.38% 166.13 kB 166.77 kB +0.57% 30.28 kB 30.45 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.38% 166.13 kB 166.77 kB +0.57% 30.28 kB 30.45 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.38% 168.72 kB 169.35 kB +0.62% 30.93 kB 31.12 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js +0.38% 168.75 kB 169.38 kB +0.62% 30.92 kB 31.12 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.36% 172.56 kB 173.18 kB +0.68% 32.01 kB 32.23 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.development.js +0.35% 177.22 kB 177.85 kB +0.65% 32.48 kB 32.69 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.34% 184.06 kB 184.69 kB +0.65% 33.55 kB 33.77 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +0.34% 185.21 kB 185.84 kB +0.64% 33.84 kB 34.06 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.34% 185.26 kB 185.89 kB +0.66% 33.85 kB 34.07 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.development.js +0.34% 149.06 kB 149.56 kB +0.71% 27.59 kB 27.78 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.development.js +0.34% 149.06 kB 149.56 kB +0.71% 27.59 kB 27.78 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.development.js +0.32% 156.89 kB 157.40 kB +0.65% 28.97 kB 29.16 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.development.js +0.32% 156.89 kB 157.40 kB +0.65% 28.97 kB 29.16 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.development.js +0.32% 156.95 kB 157.46 kB +0.67% 29.00 kB 29.19 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.32% 157.44 kB 157.94 kB +0.64% 29.09 kB 29.28 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.32% 157.44 kB 157.94 kB +0.64% 29.09 kB 29.28 kB
oss-stable-semver/react/cjs/react.production.js +0.32% 16.99 kB 17.05 kB +0.25% 4.39 kB 4.41 kB
oss-stable/react/cjs/react.production.js +0.32% 17.02 kB 17.07 kB +0.23% 4.42 kB 4.43 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.development.js +0.31% 164.79 kB 165.29 kB +0.69% 30.36 kB 30.57 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.30% 165.33 kB 165.84 kB +0.69% 30.48 kB 30.69 kB
facebook-react-native/react/cjs/React-prod.js +0.28% 19.09 kB 19.15 kB +0.22% 4.95 kB 4.96 kB
oss-experimental/react/cjs/react.production.js +0.28% 19.36 kB 19.42 kB +0.24% 4.93 kB 4.94 kB
facebook-react-native/react/cjs/React-profiling.js +0.28% 19.53 kB 19.58 kB +0.22% 5.03 kB 5.04 kB
facebook-www/React-prod.modern.js +0.26% 20.67 kB 20.73 kB +0.23% 5.31 kB 5.32 kB
facebook-www/React-prod.classic.js +0.26% 20.68 kB 20.73 kB +0.24% 5.31 kB 5.32 kB
facebook-www/React-profiling.modern.js +0.26% 21.11 kB 21.16 kB +0.24% 5.39 kB 5.40 kB
facebook-www/React-profiling.classic.js +0.26% 21.11 kB 21.16 kB +0.24% 5.39 kB 5.40 kB

Generated by 🚫 dangerJS against f7d041a

Copy link
Collaborator

@unstubbable unstubbable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this will be very useful for frameworks and libraries!

expect(clientError.message).toBe('Timed out');
}
expect(clientError.digest).toBe('hi');
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fatal error case is missing.

@sebmarkbage sebmarkbage merged commit e1dc034 into facebook:main Jun 17, 2025
247 checks passed
github-actions bot pushed a commit that referenced this pull request Jun 17, 2025
This was really meant to be there from the beginning. A `cache()`:ed
entry has a life time. On the server this ends when the render finishes.
On the client this ends when the cache of that scope gets refreshed.

When a cache is no longer needed, it should be possible to abort any
outstanding network requests or other resources. That's what
`cacheSignal()` gives you. It returns an `AbortSignal` which aborts when
the cache lifetime is done based on the same execution scope as a
`cache()`ed function - i.e. `AsyncLocalStorage` on the server or the
render scope on the client.

```js
import {cacheSignal} from 'react';
async function Component() {
  await fetch(url, { signal: cacheSignal() });
}
```

For `fetch` in particular, a patch should really just do this
automatically for you. But it's useful for other resources like database
connections.

Another reason it's useful to have a `cacheSignal()` is to ignore any
errors that might have triggered from the act of being aborted. This is
just a general useful JavaScript pattern if you have access to a signal:

```js
async function getData(id, signal) {
  try {
     await queryDatabase(id, { signal });
  } catch (x) {
     if (!signal.aborted) {
       logError(x); // only log if it's a real error and not due to cancellation
     }
     return null;
  }
}
```

This just gets you a convenient way to get to it without drilling
through so a more idiomatic code in React might look something like.

```js
import {cacheSignal} from "react";

async function getData(id) {
  try {
     await queryDatabase(id);
  } catch (x) {
     if (!cacheSignal()?.aborted) {
       logError(x);
     }
     return null;
  }
}
```

If it's called outside of a React render, we normally treat any cached
functions as uncached. They're not an error call. They can still load
data. It's just not cached. This is not like an aborted signal because
then you couldn't issue any requests. It's also not like an infinite
abort signal because it's not actually cached forever. Therefore,
`cacheSignal()` returns `null` when called outside of a React render
scope.

Notably the `signal` option passed to `renderToReadableStream` in both
SSR (Fizz) and RSC (Flight Server) is not the same instance that comes
out of `cacheSignal()`. If you abort the `signal` passed in, then the
`cacheSignal()` is also aborted with the same reason. However, the
`cacheSignal()` can also get aborted if the render completes
successfully or fatally errors during render - allowing any outstanding
work that wasn't used to clean up. In the future we might also expand on
this to give different
[`TaskSignal`](https://developer.mozilla.org/en-US/docs/Web/API/TaskSignal)
to different scopes to pass different render or network priorities.

On the client version of `"react"` this exposes a noop (both for
Fiber/Fizz) due to `disableClientCache` flag but it's exposed so that
you can write shared code.

DiffTrain build for [e1dc034](e1dc034)
github-actions bot pushed a commit that referenced this pull request Jun 17, 2025
This was really meant to be there from the beginning. A `cache()`:ed
entry has a life time. On the server this ends when the render finishes.
On the client this ends when the cache of that scope gets refreshed.

When a cache is no longer needed, it should be possible to abort any
outstanding network requests or other resources. That's what
`cacheSignal()` gives you. It returns an `AbortSignal` which aborts when
the cache lifetime is done based on the same execution scope as a
`cache()`ed function - i.e. `AsyncLocalStorage` on the server or the
render scope on the client.

```js
import {cacheSignal} from 'react';
async function Component() {
  await fetch(url, { signal: cacheSignal() });
}
```

For `fetch` in particular, a patch should really just do this
automatically for you. But it's useful for other resources like database
connections.

Another reason it's useful to have a `cacheSignal()` is to ignore any
errors that might have triggered from the act of being aborted. This is
just a general useful JavaScript pattern if you have access to a signal:

```js
async function getData(id, signal) {
  try {
     await queryDatabase(id, { signal });
  } catch (x) {
     if (!signal.aborted) {
       logError(x); // only log if it's a real error and not due to cancellation
     }
     return null;
  }
}
```

This just gets you a convenient way to get to it without drilling
through so a more idiomatic code in React might look something like.

```js
import {cacheSignal} from "react";

async function getData(id) {
  try {
     await queryDatabase(id);
  } catch (x) {
     if (!cacheSignal()?.aborted) {
       logError(x);
     }
     return null;
  }
}
```

If it's called outside of a React render, we normally treat any cached
functions as uncached. They're not an error call. They can still load
data. It's just not cached. This is not like an aborted signal because
then you couldn't issue any requests. It's also not like an infinite
abort signal because it's not actually cached forever. Therefore,
`cacheSignal()` returns `null` when called outside of a React render
scope.

Notably the `signal` option passed to `renderToReadableStream` in both
SSR (Fizz) and RSC (Flight Server) is not the same instance that comes
out of `cacheSignal()`. If you abort the `signal` passed in, then the
`cacheSignal()` is also aborted with the same reason. However, the
`cacheSignal()` can also get aborted if the render completes
successfully or fatally errors during render - allowing any outstanding
work that wasn't used to clean up. In the future we might also expand on
this to give different
[`TaskSignal`](https://developer.mozilla.org/en-US/docs/Web/API/TaskSignal)
to different scopes to pass different render or network priorities.

On the client version of `"react"` this exposes a noop (both for
Fiber/Fizz) due to `disableClientCache` flag but it's exposed so that
you can write shared code.

DiffTrain build for [e1dc034](e1dc034)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants