diff --git a/.gitignore b/.gitignore
index 438d125b47a0..16982164c421 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ chrome-user-data
.idea
*.iml
.vscode
+.zed
*.swp
*.swo
/tmp
@@ -40,4 +41,3 @@ packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
-
diff --git a/packages/eslint-plugin-react-hooks/CHANGELOG.md b/packages/eslint-plugin-react-hooks/CHANGELOG.md
index 9f88047193e7..4ad4bb411c7e 100644
--- a/packages/eslint-plugin-react-hooks/CHANGELOG.md
+++ b/packages/eslint-plugin-react-hooks/CHANGELOG.md
@@ -1,3 +1,16 @@
+## 7.1.0
+
+This release adds ESLint v10 support, improves performance by skipping compilation for non-React files, and includes compiler lint improvements including better `set-state-in-effect` detection, improved ref validation, and more helpful error reporting.
+
+- Add ESLint v10 support. ([@nicolo-ribaudo](https://github.com/nicolo-ribaudo) in [#35720](https://github.com/facebook/react/pull/35720))
+- Skip compilation for non-React files to improve performance. ([@josephsavona](https://github.com/josephsavona) in [#35589](https://github.com/facebook/react/pull/35589))
+- Fix exhaustive deps bug with Flow type casting. ([@jorge-cab](https://github.com/jorge-cab) in [#35691](https://github.com/facebook/react/pull/35691))
+- Fix `useEffectEvent` checks in component syntax. ([@jbrown215](https://github.com/jbrown215) in [#35041](https://github.com/facebook/react/pull/35041))
+- Improved `set-state-in-effect` validation with fewer false negatives. ([@jorge-cab](https://github.com/jorge-cab) in [#35134](https://github.com/facebook/react/pull/35134), [@josephsavona](https://github.com/josephsavona) in [#35147](https://github.com/facebook/react/pull/35147), [@jackpope](https://github.com/jackpope) in [#35214](https://github.com/facebook/react/pull/35214), [@chesnokov-tony](https://github.com/chesnokov-tony) in [#35419](https://github.com/facebook/react/pull/35419), [@jsleitor](https://github.com/jsleitor) in [#36107](https://github.com/facebook/react/pull/36107))
+- Improved ref validation for non-mutating functions and event handler props. ([@josephsavona](https://github.com/josephsavona) in [#35893](https://github.com/facebook/react/pull/35893), [@kolvian](https://github.com/kolvian) in [#35062](https://github.com/facebook/react/pull/35062))
+- Compiler now reports all errors instead of stopping at the first. ([@josephsavona](https://github.com/josephsavona) in [#35873](https://github.com/facebook/react/pull/35873)–[#35884](https://github.com/facebook/react/pull/35884))
+- Improved source locations and error display in compiler diagnostics. ([@nathanmarks](https://github.com/nathanmarks) in [#35348](https://github.com/facebook/react/pull/35348), [@josephsavona](https://github.com/josephsavona) in [#34963](https://github.com/facebook/react/pull/34963))
+
## 7.0.1
- Disallowed passing inline `useEffectEvent` values as JSX props to guard against accidental propagation. ([#34820](https://github.com/facebook/react/pull/34820) by [@jf-eirinha](https://github.com/jf-eirinha))
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 6ff107786f3b..efb7e887cf18 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -6289,6 +6289,100 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(container)).toEqual('Hi');
});
+ // Regression: finishedTask aborting remaining fallback tasks from a
+ // completed boundary could reenter itself via abortTaskSoft and fire
+ // onAllReady twice (the inner call drained allPendingTasks to 0 and
+ // called completeAll, then the outer call re-observed the same 0).
+ it('only fires onAllReady once when a boundary with an instrumented sync-resolving thenable completes', async () => {
+ // Mirrors Flight-client chunk behavior: the status-probe .then() in
+ // trackUsedThenable stays pending, but the ping-attaching .then() in
+ // renderNode's catch resolves synchronously. This reorders the work
+ // queue so the fallback task is still in fallbackAbortableTasks when
+ // the content task completes.
+ function createDeferredSyncThenable(value) {
+ let thenCallCount = 0;
+ return {
+ status: 'pending',
+ value: undefined,
+ then(resolve) {
+ thenCallCount++;
+ if (thenCallCount > 1) {
+ this.status = 'fulfilled';
+ this.value = value;
+ resolve(value);
+ }
+ },
+ };
+ }
+
+ const thenable = createDeferredSyncThenable('hello');
+ function AsyncContent() {
+ return ;
+ }
+
+ let allReadyCount = 0;
+ await act(() => {
+ const {pipe} = renderToPipeableStream(
+ }>
+
+ ,
+ {
+ onAllReady() {
+ allReadyCount++;
+ },
+ },
+ );
+ pipe(writable);
+ });
+
+ expect(allReadyCount).toBe(1);
+ expect(getVisibleChildren(container)).toEqual('hello');
+ });
+
+ // Same bug, hit without any sync-thenable trickery: if the fallback
+ // also suspends, its spawned sub-task lives in fallbackAbortableTasks
+ // and can still be there when the content task completes first.
+ it('only fires onAllReady once when both content and fallback suspend on real promises', async () => {
+ let resolveContent;
+ const contentPromise = new Promise(r => (resolveContent = r));
+ // The fallback promise never resolves — the fallback-sub-task gets
+ // soft-aborted when the content completes, so we never need it.
+ const fallbackPromise = new Promise(() => {});
+
+ function AsyncContent() {
+ return ;
+ }
+ function AsyncFallback() {
+ return ;
+ }
+
+ let allReadyCount = 0;
+ await act(() => {
+ const {pipe} = renderToPipeableStream(
+ }>
+
+ ,
+ {
+ onAllReady() {
+ allReadyCount++;
+ },
+ },
+ );
+ pipe(writable);
+ });
+
+ // Resolving content alone is enough: the fallback-sub-task is still
+ // in fallbackAbortableTasks when the content task completes, and
+ // abortTaskSoft on it reenters finishedTask.
+ await act(async () => {
+ resolveContent('hello');
+ await contentPromise;
+ });
+
+ expect(allReadyCount).toBe(1);
+ expect(getVisibleChildren(container)).toEqual('hello');
+ });
+
it('promise as node', async () => {
const promise = Promise.resolve('Hi');
await act(async () => {
diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js
index 7b603201cc5d..fe3536c2050b 100644
--- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js
+++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js
@@ -60,6 +60,7 @@ const {
unstable_ContinuousEventPriority: FabricContinuousPriority,
unstable_IdleEventPriority: FabricIdlePriority,
unstable_getCurrentEventPriority: fabricGetCurrentEventPriority,
+ suspendOnActiveViewTransition: fabricSuspendOnActiveViewTransition,
} = nativeFabricUIManager;
import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
@@ -88,6 +89,11 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
// % 2 === 0 means it is a Fabric tag.
// This means that they never overlap.
let nextReactTag = 2;
+export function allocateTag(): number {
+ const tag = nextReactTag;
+ nextReactTag += 2;
+ return tag;
+}
type InternalInstanceHandle = Object;
@@ -180,8 +186,7 @@ export function createInstance(
hostContext: HostContext,
internalInstanceHandle: InternalInstanceHandle,
): Instance {
- const tag = nextReactTag;
- nextReactTag += 2;
+ const tag = allocateTag();
const viewConfig = getViewConfigForType(type);
@@ -231,8 +236,7 @@ export function createTextInstance(
}
}
- const tag = nextReactTag;
- nextReactTag += 2;
+ const tag = allocateTag();
const node = createNode(
tag, // reactTag
@@ -652,7 +656,9 @@ export function suspendInstance(
export function suspendOnActiveViewTransition(
state: SuspendedState,
container: Container,
-): void {}
+): void {
+ fabricSuspendOnActiveViewTransition();
+}
export function waitForCommitToBeReady(
state: SuspendedState,
diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js
index cbe70a0d88f8..89aa1ee4c840 100644
--- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js
+++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js
@@ -16,9 +16,13 @@ import type {
GestureTimeline,
} from './ReactFiberConfigFabric';
+import {allocateTag} from './ReactFiberConfigFabric';
+
const {
applyViewTransitionName: fabricApplyViewTransitionName,
+ createViewTransitionInstance: fabricCreateViewTransitionInstance,
startViewTransition: fabricStartViewTransition,
+ startViewTransitionReadyFinished: fabricStartViewTransitionReadyFinished,
} = nativeFabricUIManager;
export type InstanceMeasurement = {
@@ -196,6 +200,8 @@ export function addViewTransitionFinishedListener(
export function createViewTransitionInstance(
name: string,
): ViewTransitionInstance {
+ const tag = allocateTag();
+ fabricCreateViewTransitionInstance(name, tag);
return {
name,
old: new (ViewTransitionPseudoElement: any)('old', name),
@@ -251,6 +257,7 @@ export function startViewTransition(
transition.ready.then(() => {
spawnedWorkCallback();
+ fabricStartViewTransitionReadyFinished();
});
transition.finished.finally(() => {
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 0b115199fc3b..73b3e435d189 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -4438,9 +4438,15 @@ function erroredTask(
const boundaryRow = boundary.row;
if (boundaryRow !== null) {
// Unblock the SuspenseListRow that was blocked by this boundary.
+ // finishSuspenseListRow → unblockSuspenseListRow → finishedTask reenters
+ // and decrements allPendingTasks. Pin the counter above zero so those
+ // nested calls can't trip completeAll before this outer frame's own
+ // zero check at the end.
+ request.allPendingTasks++;
if (--boundaryRow.pendingTasks === 0) {
finishSuspenseListRow(request, boundaryRow);
}
+ request.allPendingTasks--;
}
// Regardless of what happens next, this boundary won't be displayed,
@@ -4955,6 +4961,12 @@ function finishedTask(
hoistHoistables(boundaryRow.hoistables, boundary.contentState);
}
if (!isEligibleForOutlining(request, boundary)) {
+ // abortTaskSoft (below) and finishSuspenseListRow → unblockSuspenseListRow
+ // → finishedTask (further below) both reenter finishedTask and decrement
+ // allPendingTasks. Pin the counter above zero for the duration of these
+ // fan-outs so a nested finishedTask can't observe 0 and call completeAll
+ // before this outer call reaches its own zero check.
+ request.allPendingTasks++;
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
boundary.fallbackAbortableTasks.clear();
if (boundaryRow !== null) {
@@ -4963,6 +4975,7 @@ function finishedTask(
finishSuspenseListRow(request, boundaryRow);
}
}
+ request.allPendingTasks--;
}
if (
@@ -4988,11 +5001,17 @@ function finishedTask(
boundaryRow.next,
);
}
+ // finishSuspenseListRow → unblockSuspenseListRow → finishedTask reenters
+ // and decrements allPendingTasks. Pin the counter above zero so those
+ // nested calls can't trip completeAll before this outer frame's own
+ // zero check at the end.
+ request.allPendingTasks++;
if (--boundaryRow.pendingTasks === 0) {
// This is really unnecessary since we've already postponed the boundaries but
// for pairity with other track+finish paths. We might end up using the hoisting.
finishSuspenseListRow(request, boundaryRow);
}
+ request.allPendingTasks--;
}
}
} else {
diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js
index 7fd039cb01e5..c7e83368dd68 100644
--- a/scripts/flow/react-native-host-hooks.js
+++ b/scripts/flow/react-native-host-hooks.js
@@ -213,9 +213,12 @@ declare const nativeFabricUIManager: {
name: string,
className: ?string,
) => void,
+ createViewTransitionInstance: (name: string, tag: number) => void,
startViewTransition: (mutationCallback: () => void) => {
finished: Promise,
ready: Promise,
},
+ startViewTransitionReadyFinished: () => void,
+ suspendOnActiveViewTransition: () => void,
...
};