Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb9505c980 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
|
||||
"buildCommand": "download-build-in-codesandbox-ci",
|
||||
"node": "20",
|
||||
"node": "18",
|
||||
"publishDirectory": {
|
||||
"react": "build/oss-experimental/react",
|
||||
"react-dom": "build/oss-experimental/react-dom",
|
||||
|
||||
@@ -74,7 +74,13 @@ function getDebugChannel(req) {
|
||||
return activeDebugChannels.get(requestId);
|
||||
}
|
||||
|
||||
async function renderApp(res, returnValue, formState, noCache, debugChannel) {
|
||||
async function renderApp(
|
||||
res,
|
||||
returnValue,
|
||||
formState,
|
||||
noCache,
|
||||
promiseForDebugChannel
|
||||
) {
|
||||
const {renderToPipeableStream} = await import(
|
||||
'react-server-dom-webpack/server'
|
||||
);
|
||||
@@ -126,7 +132,7 @@ async function renderApp(res, returnValue, formState, noCache, debugChannel) {
|
||||
// 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,
|
||||
debugChannel: await promiseForDebugChannel,
|
||||
filterStackFrame,
|
||||
});
|
||||
pipe(res);
|
||||
@@ -379,20 +385,23 @@ 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});
|
||||
|
||||
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);
|
||||
});
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,52 +14,18 @@ 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') {
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
typeof WebSocketStream === 'function'
|
||||
) {
|
||||
const requestId = crypto.randomUUID();
|
||||
const debugChannel = await createWebSocketStream(
|
||||
`ws://localhost:3001/debug-channel?id=${requestId}`
|
||||
const wss = new WebSocketStream(
|
||||
'ws://localhost:3001/debug-channel?' + requestId
|
||||
);
|
||||
const debugChannel = await wss.opened;
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
method: 'POST',
|
||||
@@ -108,11 +74,15 @@ function Shell({data}) {
|
||||
|
||||
async function hydrateApp() {
|
||||
let response;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
typeof WebSocketStream === 'function'
|
||||
) {
|
||||
const requestId = crypto.randomUUID();
|
||||
const debugChannel = await createWebSocketStream(
|
||||
`ws://localhost:3001/debug-channel?id=${requestId}`
|
||||
const wss = new WebSocketStream(
|
||||
'ws://localhost:3001/debug-channel?' + requestId
|
||||
);
|
||||
const debugChannel = await wss.opened;
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
|
||||
@@ -22,22 +22,15 @@ yarn add eslint-plugin-react-hooks --dev
|
||||
|
||||
#### >= 6.0.0
|
||||
|
||||
For users of 6.0 and beyond, add the `recommended` config.
|
||||
For users of 6.0 and beyond, simply add the `recommended` config.
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import * as reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended'],
|
||||
},
|
||||
]);
|
||||
export default [
|
||||
// ...
|
||||
reactHooks.configs.recommended,
|
||||
];
|
||||
```
|
||||
|
||||
#### 5.2.0
|
||||
@@ -45,18 +38,12 @@ export default defineConfig([
|
||||
For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config.
|
||||
|
||||
```js
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import * as reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
extends: ['react-hooks/recommended-latest'],
|
||||
},
|
||||
]);
|
||||
export default [
|
||||
// ...
|
||||
reactHooks.configs['recommended-latest'],
|
||||
];
|
||||
```
|
||||
|
||||
### Legacy Config (.eslintrc)
|
||||
|
||||
@@ -1430,72 +1430,6 @@ 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,
|
||||
|
||||
@@ -147,8 +147,8 @@ function getNodeWithoutReactNamespace(
|
||||
return node;
|
||||
}
|
||||
|
||||
function isEffectIdentifier(node: Node): boolean {
|
||||
return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
|
||||
function isUseEffectIdentifier(node: Node): boolean {
|
||||
return node.type === 'Identifier' && node.name === 'useEffect';
|
||||
}
|
||||
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 (
|
||||
(isEffectIdentifier(nodeWithoutNamespace) ||
|
||||
(isUseEffectIdentifier(nodeWithoutNamespace) ||
|
||||
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
|
||||
node.arguments.length > 0
|
||||
) {
|
||||
|
||||
52
packages/react-client/src/ReactFlightClient.js
vendored
52
packages/react-client/src/ReactFlightClient.js
vendored
@@ -1010,15 +1010,10 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1074,14 +1069,7 @@ function getTaskName(type: mixed): string {
|
||||
}
|
||||
}
|
||||
|
||||
function initializeElement(
|
||||
response: Response,
|
||||
element: any,
|
||||
lazyType: null | LazyComponent<
|
||||
React$Element<any>,
|
||||
SomeChunk<React$Element<any>>,
|
||||
>,
|
||||
): void {
|
||||
function initializeElement(response: Response, element: any): void {
|
||||
if (!__DEV__) {
|
||||
return;
|
||||
}
|
||||
@@ -1148,18 +1136,6 @@ function initializeElement(
|
||||
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);
|
||||
@@ -1172,7 +1148,7 @@ function createElement(
|
||||
props: mixed,
|
||||
owner: ?ReactComponentInfo, // DEV-only
|
||||
stack: ?ReactStackTrace, // DEV-only
|
||||
validated: 0 | 1 | 2, // DEV-only
|
||||
validated: number, // DEV-only
|
||||
):
|
||||
| React$Element<any>
|
||||
| LazyComponent<React$Element<any>, SomeChunk<React$Element<any>>> {
|
||||
@@ -1249,7 +1225,7 @@ function createElement(
|
||||
handler.reason,
|
||||
);
|
||||
if (__DEV__) {
|
||||
initializeElement(response, element, null);
|
||||
initializeElement(response, element);
|
||||
// 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
|
||||
@@ -1268,7 +1244,7 @@ function createElement(
|
||||
}
|
||||
erroredChunk._debugInfo = [erroredComponent];
|
||||
}
|
||||
return createLazyChunkWrapper(erroredChunk, validated);
|
||||
return createLazyChunkWrapper(erroredChunk);
|
||||
}
|
||||
if (handler.deps > 0) {
|
||||
// We have blocked references inside this Element but we can turn this into
|
||||
@@ -1277,17 +1253,16 @@ 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, lazyType);
|
||||
/// After we have initialized any blocked references, initialize stack etc.
|
||||
const init = initializeElement.bind(null, response, element);
|
||||
blockedChunk.then(init, init);
|
||||
}
|
||||
return lazyType;
|
||||
return createLazyChunkWrapper(blockedChunk);
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
initializeElement(response, element, null);
|
||||
initializeElement(response, element);
|
||||
}
|
||||
|
||||
return element;
|
||||
@@ -1295,7 +1270,6 @@ 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,
|
||||
@@ -1307,8 +1281,6 @@ 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;
|
||||
}
|
||||
@@ -2113,7 +2085,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, 0);
|
||||
return createLazyChunkWrapper(chunk);
|
||||
}
|
||||
case '@': {
|
||||
// Promise
|
||||
@@ -2462,7 +2434,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, this);
|
||||
debugChannelRegistry.register(this, debugChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
};
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
const handleClick = ({metaKey, button}) => {
|
||||
if (id !== null && button === 0) {
|
||||
const handleClick = ({metaKey}) => {
|
||||
if (id !== null) {
|
||||
logEvent({
|
||||
event_name: 'select-element',
|
||||
metadata: {source: 'click-element'},
|
||||
|
||||
@@ -16,15 +16,14 @@
|
||||
|
||||
.TreeWrapper {
|
||||
border-top: 1px solid var(--color-border);
|
||||
flex: 1 1 65%;
|
||||
flex: 1 1 var(--horizontal-resize-tree-percentage);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.InspectedElementWrapper {
|
||||
flex: 0 0 calc(100% - var(--horizontal-resize-tree-percentage));
|
||||
flex: 1 1 35%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -60,12 +59,12 @@
|
||||
|
||||
.TreeWrapper {
|
||||
border-top: 1px solid var(--color-border);
|
||||
flex: 1 1 50%;
|
||||
flex: 1 1 var(--vertical-resize-tree-percentage);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.InspectedElementWrapper {
|
||||
flex: 0 0 calc(100% - var(--vertical-resize-tree-percentage));
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
|
||||
.TreeWrapper + .ResizeBarWrapper .ResizeBar {
|
||||
|
||||
@@ -2,18 +2,13 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0.25rem;
|
||||
padding: 0 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 {
|
||||
@@ -21,6 +16,20 @@
|
||||
max-width: 3rem;
|
||||
}
|
||||
|
||||
.SuspenseTimelineProgressIndicator {
|
||||
align-self: center;
|
||||
.SuspenseTimelineMarkers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.SuspenseTimelineMarkers > * {
|
||||
flex: 1 1 0;
|
||||
overflow: visible;
|
||||
visibility: hidden;
|
||||
width: 0
|
||||
}
|
||||
|
||||
.SuspenseTimelineActiveMarker {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,14 @@ import type {Element, SuspenseNode} from '../../../frontend/types';
|
||||
import type Store from '../../store';
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext, useLayoutEffect, useMemo, useRef, useState} from 'react';
|
||||
import {
|
||||
useContext,
|
||||
useId,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {BridgeContext, StoreContext} from '../context';
|
||||
import {TreeDispatcherContext} from '../Components/TreeContext';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
@@ -105,6 +112,30 @@ 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>;
|
||||
}
|
||||
@@ -188,26 +219,25 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default function SuspenseTreeList(_: {}): React$Node {
|
||||
return <div>Activity slices not implemented yet</div>;
|
||||
return <div>Activity slices</div>;
|
||||
}
|
||||
|
||||
@@ -2846,64 +2846,4 @@ 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>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,10 +59,7 @@ 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 {
|
||||
|
||||
@@ -804,14 +804,6 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,11 +822,3 @@ export function isValidElement(object) {
|
||||
object.$$typeof === REACT_ELEMENT_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
export function isLazyType(object) {
|
||||
return (
|
||||
typeof object === 'object' &&
|
||||
object !== null &&
|
||||
object.$$typeof === REACT_LAZY_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user