Compare commits

..

11 Commits

Author SHA1 Message Date
Jordan Brown
28a86ffe47 [lint] Allow useEffectEvent in useLayoutEffect and useInsertionEffect 2025-09-15 09:11:08 -04:00
Sebastian "Sebbie" Silbermann
8e60cb7ed5 [DevTools] Remove markers from Suspense timeline (#34357) 2025-09-02 14:59:15 +02:00
Sebastian "Sebbie" Silbermann
6a58b80020 [DevTools] Only inspect elements on left mouseclick (#34361) 2025-09-02 12:40:54 +02:00
Sebastian "Sebbie" Silbermann
b1b0955f2b [DevTools] Fix inspected element scroll in Suspense tab (#34355) 2025-09-01 16:40:30 +02:00
Hendrik Liebau
1549bda33f [Flight] Only assign _store in dev mode when creating lazy types (#34354)
Small follow-up to #34350. The `_store` property is now only assigned in
development mode when creating lazy types. It also uses the `validated`
value that was passed to `createElement`, if applicable.
2025-09-01 12:13:05 +02:00
Hendrik Liebau
bb6f0c8d2f [Flight] Fix wrong missing key warning when static child is blocked (#34350) 2025-09-01 11:03:57 +02:00
Hendrik Liebau
aad7c664ff [Flight] Don't try to close debug channel twice (#34340)
When the debug channel was already closed, we must not try to close it
again when the Response gets garbage collected.

**Test plan:**

1. reduce the Flight fixture `App` component to a minimum [^1]
    - remove everything from `<body>`
    - delete the `console.log` statement
2. open the app in Firefox (seems to have a more aggressive GC strategy)
3. wait a few seconds

On `main`, you will see the following error in the browser console:

```
TypeError: Can not close stream after closing or error
```

With this change, the error is gone.

[^1]: It's a bit concerning that step 1 is needed to reproduce the
issue. Either GC is behaving differently with the unmodified App, or we
may hold on to the Response under certain conditions, potentially
creating a memory leak. This needs further investigation.
2025-08-29 17:22:39 +02:00
Hendrik Liebau
3fe51c9e14 [Flight] Use more robust web socket implementation in fixture (#34338)
The `WebSocketStream` implementation seems to be a bit unreliable. We've
seen `Cannot close a ERRORED writable stream` errors when expanding the
logged deep object, for example. And when reducing the fixture to a
minimal app, we even get `Connection closed` errors, because the web
socket connection is closed before all debug chunks are sent.

We can improve the reliability of the web socket connection by using a
normal `WebSocket` instance on the client, along with manually creating
a `WritableStream` and a `ReadableStream` for processing the messages.

As an additional benefit, the debug channel now also works in Firefox
and Safari.

On the server, we're simplifying the integration with the Express server
a bit by utilizing the `server` property for `WebSocket.Server`, instead
of the `noServer` property with the manual upgrade handling.
2025-08-29 12:04:27 +02:00
Joseph Savona
4082b0e7d3 [compiler] Detect known incompatible libraries (#34027)
A few libraries are known to be incompatible with memoization, whether
manually via `useMemo()` or via React Compiler. This puts us in a tricky
situation. On the one hand, we understand that these libraries were
developed prior to our documenting the [Rules of
React](https://react.dev/reference/rules), and their designs were the
result of trying to deliver a great experience for their users and
balance multiple priorities around DX, performance, etc. At the same
time, using these libraries with memoization — and in particular with
automatic memoization via React Compiler — can break apps by causing the
components using these APIs not to update. Concretely, the APIs have in
common that they return a function which returns different values over
time, but where the function itself does not change. Memoizing the
result on the identity of the function will mean that the value never
changes. Developers reasonable interpret this as "React Compiler broke
my code".

Of course, the best solution is to work with developers of these
libraries to address the root cause, and we're doing that. We've
previously discussed this situation with both of the respective
libraries:
* React Hook Form:
https://github.com/react-hook-form/react-hook-form/issues/11910#issuecomment-2135608761
* TanStack Table:
https://github.com/facebook/react/issues/33057#issuecomment-2840600158
and https://github.com/TanStack/table/issues/5567

In the meantime we need to make sure that React Compiler can work out of
the box as much as possible. This means teaching it about popular
libraries that cannot be memoized. We also can't silently skip
compilation, as this confuses users, so we need these error messages to
be visible to users. To that end, this PR adds:

* A flag to mark functions/hooks as incompatible
* Validation against use of such functions
* A default type provider to provide declarations for two
known-incompatible libraries

Note that Mobx is also incompatible, but the `observable()` function is
called outside of the component itself, so the compiler cannot currently
detect it. We may add validation for such APIs in the future.

Again, we really empathize with the developers of these libraries. We've
tried to word the error message non-judgementally, because we get that
it's hard! We're open to feedback about the error message, please let us
know.
2025-08-28 16:21:15 -07:00
Smruti Ranjan Badatya
6b49c449b6 Update Code Sandbox CI to Node 20 to Match .nvmrc (#34329)
## Summary
Update the CodeSandbox CI configuration to use Node 20 instead of Node
18, so that it matches the Node version specified in .nvmrc. This
ensures consistency between local development environments and CI
builds, reducing the risk of version-related build issues.

Closes #34328

## How did you test this change?
- Verified that .nvmrc specifies Node 20 and .codesandbox/ci.json is
updated accordingly.
- Locally switched to Node 20 using nvm use 20 and successfully ran
build scripts for all packages: `react`, `react-dom`,
`react-server-dom-webpack`, and `scheduler`.
- Confirmed there are no Node 20–specific build errors or warnings
locally.
- CI on the feature branch will now run with Node 20, and all builds are
expected to succeed.
2025-08-28 18:33:12 -04:00
lauren
872b4fef6d [eprh] Update installation instructions in readme (#34331)
Small PR to update our readme for eslint-plugin-react-hooks, to better
describe what a minimal but complete eslint config would look like.
2025-08-28 18:27:49 -04:00
15 changed files with 310 additions and 141 deletions

View File

@@ -1,7 +1,7 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "18",
"node": "20",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",

View File

@@ -74,13 +74,7 @@ function getDebugChannel(req) {
return activeDebugChannels.get(requestId);
}
async function renderApp(
res,
returnValue,
formState,
noCache,
promiseForDebugChannel
) {
async function renderApp(res, returnValue, formState, noCache, debugChannel) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@@ -132,7 +126,7 @@ async function renderApp(
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState};
const {pipe} = renderToPipeableStream(payload, moduleMap, {
debugChannel: await promiseForDebugChannel,
debugChannel,
filterStackFrame,
});
pipe(res);
@@ -385,23 +379,20 @@ app.on('error', function (error) {
if (process.env.NODE_ENV === 'development') {
// Open a websocket server for Debug information
const WebSocket = require('ws');
const webSocketServer = new WebSocket.Server({noServer: true});
httpServer.on('upgrade', (request, socket, head) => {
const DEBUG_CHANNEL_PATH = '/debug-channel?';
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
const promiseForWs = new Promise(resolve => {
webSocketServer.handleUpgrade(request, socket, head, ws => {
ws.on('close', () => {
activeDebugChannels.delete(requestId);
});
resolve(ws);
});
});
activeDebugChannels.set(requestId, promiseForWs);
} else {
socket.destroy();
}
const webSocketServer = new WebSocket.Server({
server: httpServer,
path: '/debug-channel',
});
webSocketServer.on('connection', (ws, req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const requestId = url.searchParams.get('id');
activeDebugChannels.set(requestId, ws);
ws.on('close', (code, reason) => {
activeDebugChannels.delete(requestId);
});
});
}

View File

@@ -14,18 +14,52 @@ function findSourceMapURL(fileName) {
);
}
async function createWebSocketStream(url) {
const ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
await new Promise((resolve, reject) => {
ws.addEventListener('open', resolve, {once: true});
ws.addEventListener('error', reject, {once: true});
});
const writable = new WritableStream({
write(chunk) {
ws.send(chunk);
},
close() {
ws.close();
},
abort(reason) {
ws.close(1000, reason && String(reason));
},
});
const readable = new ReadableStream({
start(controller) {
ws.addEventListener('message', event => {
controller.enqueue(event.data);
});
ws.addEventListener('close', () => {
controller.close();
});
ws.addEventListener('error', err => {
controller.error(err);
});
},
});
return {readable, writable};
}
let updateRoot;
async function callServer(id, args) {
let response;
if (
process.env.NODE_ENV === 'development' &&
typeof WebSocketStream === 'function'
) {
if (process.env.NODE_ENV === 'development') {
const requestId = crypto.randomUUID();
const wss = new WebSocketStream(
'ws://localhost:3001/debug-channel?' + requestId
const debugChannel = await createWebSocketStream(
`ws://localhost:3001/debug-channel?id=${requestId}`
);
const debugChannel = await wss.opened;
response = createFromFetch(
fetch('/', {
method: 'POST',
@@ -74,15 +108,11 @@ function Shell({data}) {
async function hydrateApp() {
let response;
if (
process.env.NODE_ENV === 'development' &&
typeof WebSocketStream === 'function'
) {
if (process.env.NODE_ENV === 'development') {
const requestId = crypto.randomUUID();
const wss = new WebSocketStream(
'ws://localhost:3001/debug-channel?' + requestId
const debugChannel = await createWebSocketStream(
`ws://localhost:3001/debug-channel?id=${requestId}`
);
const debugChannel = await wss.opened;
response = createFromFetch(
fetch('/', {
headers: {

View File

@@ -22,15 +22,22 @@ yarn add eslint-plugin-react-hooks --dev
#### >= 6.0.0
For users of 6.0 and beyond, simply add the `recommended` config.
For users of 6.0 and beyond, add the `recommended` config.
```js
import * as reactHooks from 'eslint-plugin-react-hooks';
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default [
// ...
reactHooks.configs.recommended,
];
export default defineConfig([
{
files: ["src/**/*.{js,jsx,ts,tsx}"],
plugins: {
'react-hooks': reactHooks,
},
extends: ['react-hooks/recommended'],
},
]);
```
#### 5.2.0
@@ -38,12 +45,18 @@ export default [
For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config.
```js
import * as reactHooks from 'eslint-plugin-react-hooks';
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default [
// ...
reactHooks.configs['recommended-latest'],
];
export default defineConfig([
{
files: ["src/**/*.{js,jsx,ts,tsx}"],
plugins: {
'react-hooks': reactHooks,
},
extends: ['react-hooks/recommended-latest'],
},
]);
```
### Legacy Config (.eslintrc)

View File

@@ -1430,6 +1430,72 @@ if (__EXPERIMENTAL__) {
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useLayoutEffect(() => {
onClick();
});
React.useLayoutEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useInsertionEffect(() => {
onClick();
});
React.useInsertionEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
// and useInsertionEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = useEffectEvent(() => {
debounce(onClick);
debounce(() => onClick());
debounce(() => { onClick() });
deboucne(() => debounce(onClick));
});
useLayoutEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
React.useLayoutEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
useInsertionEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
React.useInsertionEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
return null;
}
`,
},
];
allTests.invalid = [
...allTests.invalid,

View File

@@ -147,8 +147,8 @@ function getNodeWithoutReactNamespace(
return node;
}
function isUseEffectIdentifier(node: Node): boolean {
return node.type === 'Identifier' && node.name === 'useEffect';
function isEffectIdentifier(node: Node): boolean {
return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
}
function isUseEffectEventIdentifier(node: Node): boolean {
if (__EXPERIMENTAL__) {
@@ -726,7 +726,7 @@ const rule = {
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
if (
(isUseEffectIdentifier(nodeWithoutNamespace) ||
(isEffectIdentifier(nodeWithoutNamespace) ||
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
node.arguments.length > 0
) {

View File

@@ -1010,10 +1010,15 @@ export function reportGlobalError(
if (__DEV__) {
const debugChannel = response._debugChannel;
if (debugChannel !== undefined) {
// If we don't have any more ways of reading data, we don't have to send any
// more neither. So we close the writable side.
// If we don't have any more ways of reading data, we don't have to send
// any more neither. So we close the writable side.
closeDebugChannel(debugChannel);
response._debugChannel = undefined;
// Make sure the debug channel is not closed a second time when the
// Response gets GC:ed.
if (debugChannelRegistry !== null) {
debugChannelRegistry.unregister(response);
}
}
}
}
@@ -1069,7 +1074,14 @@ function getTaskName(type: mixed): string {
}
}
function initializeElement(response: Response, element: any): void {
function initializeElement(
response: Response,
element: any,
lazyType: null | LazyComponent<
React$Element<any>,
SomeChunk<React$Element<any>>,
>,
): void {
if (!__DEV__) {
return;
}
@@ -1136,6 +1148,18 @@ function initializeElement(response: Response, element: any): void {
if (owner !== null) {
initializeFakeStack(response, owner);
}
// In case the JSX runtime has validated the lazy type as a static child, we
// need to transfer this information to the element.
if (
lazyType &&
lazyType._store &&
lazyType._store.validated &&
!element._store.validated
) {
element._store.validated = lazyType._store.validated;
}
// TODO: We should be freezing the element but currently, we might write into
// _debugInfo later. We could move it into _store which remains mutable.
Object.freeze(element.props);
@@ -1148,7 +1172,7 @@ function createElement(
props: mixed,
owner: ?ReactComponentInfo, // DEV-only
stack: ?ReactStackTrace, // DEV-only
validated: number, // DEV-only
validated: 0 | 1 | 2, // DEV-only
):
| React$Element<any>
| LazyComponent<React$Element<any>, SomeChunk<React$Element<any>>> {
@@ -1225,7 +1249,7 @@ function createElement(
handler.reason,
);
if (__DEV__) {
initializeElement(response, element);
initializeElement(response, element, null);
// Conceptually the error happened inside this Element but right before
// it was rendered. We don't have a client side component to render but
// we can add some DebugInfo to explain that this was conceptually a
@@ -1244,7 +1268,7 @@ function createElement(
}
erroredChunk._debugInfo = [erroredComponent];
}
return createLazyChunkWrapper(erroredChunk);
return createLazyChunkWrapper(erroredChunk, validated);
}
if (handler.deps > 0) {
// We have blocked references inside this Element but we can turn this into
@@ -1253,16 +1277,17 @@ function createElement(
createBlockedChunk(response);
handler.value = element;
handler.chunk = blockedChunk;
const lazyType = createLazyChunkWrapper(blockedChunk, validated);
if (__DEV__) {
/// After we have initialized any blocked references, initialize stack etc.
const init = initializeElement.bind(null, response, element);
// After we have initialized any blocked references, initialize stack etc.
const init = initializeElement.bind(null, response, element, lazyType);
blockedChunk.then(init, init);
}
return createLazyChunkWrapper(blockedChunk);
return lazyType;
}
}
if (__DEV__) {
initializeElement(response, element);
initializeElement(response, element, null);
}
return element;
@@ -1270,6 +1295,7 @@ function createElement(
function createLazyChunkWrapper<T>(
chunk: SomeChunk<T>,
validated: 0 | 1 | 2, // DEV-only
): LazyComponent<T, SomeChunk<T>> {
const lazyType: LazyComponent<T, SomeChunk<T>> = {
$$typeof: REACT_LAZY_TYPE,
@@ -1281,6 +1307,8 @@ function createLazyChunkWrapper<T>(
const chunkDebugInfo: ReactDebugInfo =
chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo));
lazyType._debugInfo = chunkDebugInfo;
// Initialize a store for key validation by the JSX runtime.
lazyType._store = {validated: validated};
}
return lazyType;
}
@@ -2085,7 +2113,7 @@ function parseModelString(
}
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
return createLazyChunkWrapper(chunk, 0);
}
case '@': {
// Promise
@@ -2434,7 +2462,7 @@ function ResponseInstance(
// When a Response gets GC:ed because nobody is referring to any of the
// objects that lazily load from the Response anymore, then we can close
// the debug channel.
debugChannelRegistry.register(this, debugChannel);
debugChannelRegistry.register(this, debugChannel, this);
}
}
}

View File

@@ -80,8 +80,8 @@ export default function Element({data, index, style}: Props): React.Node {
};
// $FlowFixMe[missing-local-annot]
const handleClick = ({metaKey}) => {
if (id !== null) {
const handleClick = ({metaKey, button}) => {
if (id !== null && button === 0) {
logEvent({
event_name: 'select-element',
metadata: {source: 'click-element'},

View File

@@ -16,14 +16,15 @@
.TreeWrapper {
border-top: 1px solid var(--color-border);
flex: 1 1 var(--horizontal-resize-tree-percentage);
flex: 1 1 65%;
display: flex;
flex-direction: row;
height: 100%;
overflow: auto;
}
.InspectedElementWrapper {
flex: 1 1 35%;
flex: 0 0 calc(100% - var(--horizontal-resize-tree-percentage));
overflow-x: hidden;
overflow-y: auto;
}
@@ -59,12 +60,12 @@
.TreeWrapper {
border-top: 1px solid var(--color-border);
flex: 1 1 var(--vertical-resize-tree-percentage);
flex: 1 1 50%;
overflow: hidden;
}
.InspectedElementWrapper {
flex: 1 1 50%;
flex: 0 0 calc(100% - var(--vertical-resize-tree-percentage));
}
.TreeWrapper + .ResizeBarWrapper .ResizeBar {

View File

@@ -2,13 +2,18 @@
width: 100%;
display: flex;
flex-direction: row;
padding: 0 0.25rem;
padding: 0.25rem;
}
.SuspenseTimelineInput {
display: flex;
flex-direction: column;
flex-grow: 1;
/*
* `overflow: auto` will add scrollbars but the input will not actually grow beyond visible content.
* `overflow: hidden` will constrain the input to its visible content.
*/
overflow: hidden;
}
.SuspenseTimelineRootSwitcher {
@@ -16,20 +21,6 @@
max-width: 3rem;
}
.SuspenseTimelineMarkers {
display: flex;
flex-direction: row;
justify-content: space-between;
.SuspenseTimelineProgressIndicator {
align-self: center;
}
.SuspenseTimelineMarkers > * {
flex: 1 1 0;
overflow: visible;
visibility: hidden;
width: 0
}
.SuspenseTimelineActiveMarker {
visibility: visible;
}

View File

@@ -11,14 +11,7 @@ import type {Element, SuspenseNode} from '../../../frontend/types';
import type Store from '../../store';
import * as React from 'react';
import {
useContext,
useId,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import {useContext, useLayoutEffect, useMemo, useRef, useState} from 'react';
import {BridgeContext, StoreContext} from '../context';
import {TreeDispatcherContext} from '../Components/TreeContext';
import {useHighlightHostInstance} from '../hooks';
@@ -112,30 +105,6 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
setValue(max);
}
const markersID = useId();
const markers: React.Node[] = useMemo(() => {
return timeline.map((suspense, index) => {
const takesUpSpace =
suspense.rects !== null &&
suspense.rects.some(rect => {
return rect.width > 0 && rect.height > 0;
});
return takesUpSpace ? (
<option
key={suspense.id}
className={
index === value ? styles.SuspenseTimelineActiveMarker : undefined
}
value={index}>
#{index + 1}
</option>
) : (
<option key={suspense.id} />
);
});
}, [timeline, value]);
if (rootID === undefined) {
return <div className={styles.SuspenseTimelineInput}>Root not found.</div>;
}
@@ -219,25 +188,26 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
}
return (
<div className={styles.SuspenseTimelineInput}>
<input
className={styles.SuspenseTimelineSlider}
type="range"
min={min}
max={max}
list={markersID}
value={value}
onBlur={handleBlur}
onChange={handleChange}
onFocus={handleFocus}
onPointerMove={handlePointerMove}
onPointerUp={clearHighlightHostInstance}
ref={inputRef}
/>
<datalist id={markersID} className={styles.SuspenseTimelineMarkers}>
{markers}
</datalist>
</div>
<>
<div>
{value}/{max}
</div>
<div className={styles.SuspenseTimelineInput}>
<input
className={styles.SuspenseTimelineSlider}
type="range"
min={min}
max={max}
value={value}
onBlur={handleBlur}
onChange={handleChange}
onFocus={handleFocus}
onPointerMove={handlePointerMove}
onPointerUp={clearHighlightHostInstance}
ref={inputRef}
/>
</div>
</>
);
}

View File

@@ -10,5 +10,5 @@
import * as React from 'react';
export default function SuspenseTreeList(_: {}): React$Node {
return <div>Activity slices</div>;
return <div>Activity slices not implemented yet</div>;
}

View File

@@ -2846,4 +2846,64 @@ describe('ReactFlightDOMBrowser', () => {
expect(container.innerHTML).toBe('<p>Hi</p>');
});
it('should not have missing key warnings when a static child is blocked on debug info', async () => {
const ClientComponent = clientExports(function ClientComponent({element}) {
return (
<div>
<span>Hi</span>
{element}
</div>
);
});
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientComponent element={<span>Sebbie</span>} />,
webpackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream, {
debugChannel: {readable: createDelayedStream(debugReadableStream)},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
// Wait for the debug info to be processed.
await act(() => {});
expect(container.innerHTML).toBe(
'<div><span>Hi</span><span>Sebbie</span></div>',
);
});
});

View File

@@ -59,7 +59,10 @@ export type LazyComponent<T, P> = {
$$typeof: symbol | number,
_payload: P,
_init: (payload: P) => T,
// __DEV__
_debugInfo?: null | ReactDebugInfo,
_store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail
};
function lazyInitializer<T>(payload: Payload<T>): T {

View File

@@ -804,6 +804,14 @@ function validateChildKeys(node) {
if (node._store) {
node._store.validated = 1;
}
} else if (isLazyType(node)) {
if (node._payload.status === 'fulfilled') {
if (isValidElement(node._payload.value) && node._payload.value._store) {
node._payload.value._store.validated = 1;
}
} else if (node._store) {
node._store.validated = 1;
}
}
}
}
@@ -822,3 +830,11 @@ export function isValidElement(object) {
object.$$typeof === REACT_ELEMENT_TYPE
);
}
export function isLazyType(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_LAZY_TYPE
);
}