Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ chrome-user-data
.idea
*.iml
.vscode
.zed
*.swp
*.swo
/tmp
Expand All @@ -40,4 +41,3 @@ packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist

13 changes: 13 additions & 0 deletions packages/eslint-plugin-react-hooks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
94 changes: 94 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Text text={use(thenable)} />;
}

let allReadyCount = 0;
await act(() => {
const {pipe} = renderToPipeableStream(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncContent />
</Suspense>,
{
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 <Text text={use(contentPromise)} />;
}
function AsyncFallback() {
return <Text text={use(fallbackPromise)} />;
}

let allReadyCount = 0;
await act(() => {
const {pipe} = renderToPipeableStream(
<Suspense fallback={<AsyncFallback />}>
<AsyncContent />
</Suspense>,
{
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 () => {
Expand Down
16 changes: 11 additions & 5 deletions packages/react-native-renderer/src/ReactFiberConfigFabric.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const {
unstable_ContinuousEventPriority: FabricContinuousPriority,
unstable_IdleEventPriority: FabricIdlePriority,
unstable_getCurrentEventPriority: fabricGetCurrentEventPriority,
suspendOnActiveViewTransition: fabricSuspendOnActiveViewTransition,
} = nativeFabricUIManager;

import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -180,8 +186,7 @@ export function createInstance(
hostContext: HostContext,
internalInstanceHandle: InternalInstanceHandle,
): Instance {
const tag = nextReactTag;
nextReactTag += 2;
const tag = allocateTag();

const viewConfig = getViewConfigForType(type);

Expand Down Expand Up @@ -231,8 +236,7 @@ export function createTextInstance(
}
}

const tag = nextReactTag;
nextReactTag += 2;
const tag = allocateTag();

const node = createNode(
tag, // reactTag
Expand Down Expand Up @@ -652,7 +656,9 @@ export function suspendInstance(
export function suspendOnActiveViewTransition(
state: SuspendedState,
container: Container,
): void {}
): void {
fabricSuspendOnActiveViewTransition();
}

export function waitForCommitToBeReady(
state: SuspendedState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -251,6 +257,7 @@ export function startViewTransition(

transition.ready.then(() => {
spawnedWorkCallback();
fabricStartViewTransitionReadyFinished();
});

transition.finished.finally(() => {
Expand Down
19 changes: 19 additions & 0 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -4963,6 +4975,7 @@ function finishedTask(
finishSuspenseListRow(request, boundaryRow);
}
}
request.allPendingTasks--;
}

if (
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions scripts/flow/react-native-host-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,12 @@ declare const nativeFabricUIManager: {
name: string,
className: ?string,
) => void,
createViewTransitionInstance: (name: string, tag: number) => void,
startViewTransition: (mutationCallback: () => void) => {
finished: Promise<void>,
ready: Promise<void>,
},
startViewTransitionReadyFinished: () => void,
suspendOnActiveViewTransition: () => void,
...
};
Loading