-
Notifications
You must be signed in to change notification settings - Fork 47.9k
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
[Flight] Encode Symbols as special rows that can be referenced by models … #20171
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,28 +25,20 @@ import { | |
close, | ||
processModelChunk, | ||
processModuleChunk, | ||
processSymbolChunk, | ||
processErrorChunk, | ||
resolveModuleMetaData, | ||
isModuleReference, | ||
} from './ReactFlightServerConfig'; | ||
|
||
import { | ||
REACT_ELEMENT_TYPE, | ||
REACT_DEBUG_TRACING_MODE_TYPE, | ||
REACT_FORWARD_REF_TYPE, | ||
REACT_FRAGMENT_TYPE, | ||
REACT_LAZY_TYPE, | ||
REACT_LEGACY_HIDDEN_TYPE, | ||
REACT_MEMO_TYPE, | ||
REACT_OFFSCREEN_TYPE, | ||
REACT_PROFILER_TYPE, | ||
REACT_SCOPE_TYPE, | ||
REACT_STRICT_MODE_TYPE, | ||
REACT_SUSPENSE_TYPE, | ||
REACT_SUSPENSE_LIST_TYPE, | ||
} from 'shared/ReactSymbols'; | ||
|
||
import * as React from 'react'; | ||
import ReactSharedInternals from 'shared/ReactSharedInternals'; | ||
import invariant from 'shared/invariant'; | ||
|
||
|
@@ -86,6 +78,7 @@ export type Request = { | |
completedModuleChunks: Array<Chunk>, | ||
completedJSONChunks: Array<Chunk>, | ||
completedErrorChunks: Array<Chunk>, | ||
writtenSymbols: Map<Symbol, number>, | ||
flowing: boolean, | ||
toJSON: (key: string, value: ReactModel) => ReactJSONValue, | ||
}; | ||
|
@@ -107,6 +100,7 @@ export function createRequest( | |
completedModuleChunks: [], | ||
completedJSONChunks: [], | ||
completedErrorChunks: [], | ||
writtenSymbols: new Map(), | ||
flowing: false, | ||
toJSON: function(key: string, value: ReactModel): ReactJSONValue { | ||
return resolveModelToJSON(request, this, key, value); | ||
|
@@ -118,10 +112,13 @@ export function createRequest( | |
return request; | ||
} | ||
|
||
function attemptResolveElement(element: React$Element<any>): ReactModel { | ||
const type = element.type; | ||
const props = element.props; | ||
if (element.ref !== null && element.ref !== undefined) { | ||
function attemptResolveElement( | ||
type: any, | ||
key: null | React$Key, | ||
ref: mixed, | ||
props: any, | ||
): ReactModel { | ||
if (ref !== null && ref !== undefined) { | ||
// When the ref moves to the regular props object this will implicitly | ||
// throw for functions. We could probably relax it to a DEV warning for other | ||
// cases. | ||
|
@@ -135,34 +132,30 @@ function attemptResolveElement(element: React$Element<any>): ReactModel { | |
return type(props); | ||
} else if (typeof type === 'string') { | ||
// This is a host element. E.g. HTML. | ||
return [REACT_ELEMENT_TYPE, type, element.key, element.props]; | ||
} else if ( | ||
type === REACT_FRAGMENT_TYPE || | ||
type === REACT_STRICT_MODE_TYPE || | ||
type === REACT_PROFILER_TYPE || | ||
type === REACT_SCOPE_TYPE || | ||
type === REACT_DEBUG_TRACING_MODE_TYPE || | ||
type === REACT_LEGACY_HIDDEN_TYPE || | ||
type === REACT_OFFSCREEN_TYPE || | ||
// TODO: These are temporary shims | ||
// and we'll want a different behavior. | ||
type === REACT_SUSPENSE_TYPE || | ||
type === REACT_SUSPENSE_LIST_TYPE | ||
) { | ||
return element.props.children; | ||
return [REACT_ELEMENT_TYPE, type, key, props]; | ||
} else if (typeof type === 'symbol') { | ||
if (type === REACT_FRAGMENT_TYPE) { | ||
// For key-less fragments, we add a small optimization to avoid serializing | ||
// it as a wrapper. | ||
// TODO: If a key is specified, we should propagate its key to any children. | ||
// Same as if a server component has a key. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This actually reminded me.. What's the plan for different types? Do those also get encoded somehow when we get to encoding keys? Or does the type change trick not work for Server Components? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, it doesn't work for server components but that was already the plan for function components too as part of the "inlining optimization" or "call through". At least stateless ones and only if they don't have an explicit key. The reason it needs to kill state is because the state doesn't "line up" with a different type. A stateless doesn't need to. It shouldn't be relied upon. If you need it, it should be a different key. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh right. So it doesn't matter until you run into a Client one, at which point you have the types on the client anyway. I guess I still find this a bit counter-intuitive in terms of guarantees. It allows for some fun patterns though. Like SCs could use recursion and then have a terminal CC, and it could still match up on refetch despite different SC depth? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. You could also just call a recursive function. I guess you could use it as a resuming hack. |
||
return props.children; | ||
} | ||
// This might be a built-in React component. We'll let the client decide. | ||
// Any built-in works as long as its props are serializable. | ||
return [REACT_ELEMENT_TYPE, type, key, props]; | ||
} else if (type != null && typeof type === 'object') { | ||
if (isModuleReference(type)) { | ||
// This is a reference to a client component. | ||
return [REACT_ELEMENT_TYPE, type, element.key, element.props]; | ||
return [REACT_ELEMENT_TYPE, type, key, props]; | ||
} | ||
switch (type.$$typeof) { | ||
case REACT_FORWARD_REF_TYPE: { | ||
const render = type.render; | ||
return render(props, undefined); | ||
} | ||
case REACT_MEMO_TYPE: { | ||
const nextChildren = React.createElement(type.type, element.props); | ||
return attemptResolveElement(nextChildren); | ||
return attemptResolveElement(type.type, key, ref, props); | ||
} | ||
} | ||
} | ||
|
@@ -399,7 +392,12 @@ export function resolveModelToJSON( | |
const element: React$Element<any> = (value: any); | ||
try { | ||
// Attempt to render the server component. | ||
value = attemptResolveElement(element); | ||
value = attemptResolveElement( | ||
element.type, | ||
element.key, | ||
element.ref, | ||
element.props, | ||
); | ||
} catch (x) { | ||
if (typeof x === 'object' && x !== null && typeof x.then === 'function') { | ||
// Something suspended, we'll need to create a new segment and resolve it later. | ||
|
@@ -526,14 +524,26 @@ export function resolveModelToJSON( | |
} | ||
|
||
if (typeof value === 'symbol') { | ||
const writtenSymbols = request.writtenSymbols; | ||
const existingId = writtenSymbols.get(value); | ||
if (existingId !== undefined) { | ||
return serializeByValueID(existingId); | ||
} | ||
const name = value.description; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😲 TIL There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, it was actually added late or even later version of the spec. |
||
invariant( | ||
false, | ||
'Symbol values (%s) cannot be passed to client components. ' + | ||
Symbol.for(name) === value, | ||
'Only global symbols received from Symbol.for(...) can be passed to client components. ' + | ||
'The symbol Symbol.for(%s) cannot be found among global symbols. ' + | ||
'Remove %s from this object, or avoid the entire object: %s', | ||
value.description, | ||
describeKeyForErrorMessage(key), | ||
describeObjectForErrorMessage(parent), | ||
); | ||
request.pendingChunks++; | ||
const symbolId = request.nextChunkId++; | ||
emitSymbolChunk(request, symbolId, name); | ||
writtenSymbols.set(value, symbolId); | ||
return serializeByValueID(symbolId); | ||
} | ||
|
||
// $FlowFixMe: bigint isn't added to Flow yet. | ||
|
@@ -588,6 +598,11 @@ function emitModuleChunk( | |
request.completedModuleChunks.push(processedChunk); | ||
} | ||
|
||
function emitSymbolChunk(request: Request, id: number, name: string): void { | ||
const processedChunk = processSymbolChunk(request, id, name); | ||
request.completedModuleChunks.push(processedChunk); | ||
} | ||
|
||
function retrySegment(request: Request, segment: Segment): void { | ||
const query = segment.query; | ||
let value; | ||
|
@@ -604,7 +619,12 @@ function retrySegment(request: Request, segment: Segment): void { | |
// Doing this here lets us reuse this same segment if the next component | ||
// also suspends. | ||
segment.query = () => value; | ||
value = attemptResolveElement(element); | ||
value = attemptResolveElement( | ||
element.type, | ||
element.key, | ||
element.ref, | ||
element.props, | ||
); | ||
} | ||
const processedChunk = processModelChunk(request, segment.id, value); | ||
request.completedJSONChunks.push(processedChunk); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what you fixed here? I don't get it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a separate commit 29b9646
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah but I don't understand what it was fixing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see the
REACT_MEMO_TYPE
case. Was it losingelement.key
andelement.ref
earlier because you didcreateElement(type, element.props)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You did. :) But yea and we might remove the export at some point too. Regardless, it's just less efficient to create an unnecessary element.