Skip to content

Commit 5f7ef8c

Browse files
authored
[Float] handle resource Resource creation inside svg context (#25599)
`title` is a valid element descendent of `svg`. this PR adds a prohibition on turning titles in svg into Resources. This PR also adds additional warnings if you render something that is almost a Resource inside an svg.
1 parent 36426e6 commit 5f7ef8c

4 files changed

Lines changed: 449 additions & 21 deletions

File tree

‎packages/react-dom-bindings/src/client/ReactDOMFloatClient.js‎

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
getHostContext,
3535
}from'react-reconciler/src/ReactFiberHostContext';
3636
import{getResourceFormOnly}from'./validateDOMNesting';
37+
import{getNamespace}from'./ReactDOMHostConfig';
38+
import{SVG_NAMESPACE}from'../shared/DOMNamespaces';
3739

3840
// The resource types we support. currently they match the form for the as argument.
3941
// In the future this may need to change, especially when modules / scripts are supported
@@ -201,6 +203,28 @@ function getCurrentResourceRoot(): null | FloatRoot {
201203
returncurrentContainer ? currentContainer.getRootNode() : null;
202204
}
203205

206+
// This resource type constraint can be loosened. It really is everything except PreloadResource
207+
// because that is the only one that does not have an optional instance type. Expand as needed.
208+
functionresetInstance(resource: ScriptResource|HeadResource){
209+
resource.instance=undefined;
210+
}
211+
212+
exportfunctionclearRootResources(rootContainer: Container): void{
213+
constrootNode: FloatRoot=(rootContainer.getRootNode(): any);
214+
constresources=getResourcesFromRoot(rootNode);
215+
216+
// We can't actually delete the resource cache because this function is called
217+
// during commit after we have rendered. Instead we detatch any instances from
218+
// the Resource object if they are going to be cleared
219+
220+
// Styles stay put
221+
// Scripts get reset
222+
resources.scripts.forEach(resetInstance);
223+
// Head Resources get reset
224+
resources.head.forEach(resetInstance);
225+
// lastStructuredMeta stays put
226+
}
227+
204228
// Preloads are somewhat special. Even if we don't have the Document
205229
// used by the root that is rendering a component trying to insert a preload
206230
// we can still seed the file cache by doing the preload on any document we have
@@ -1077,7 +1101,14 @@ function acquireHeadResource(resource: HeadResource): Instance {
10771101
props,
10781102
root,
10791103
);
1080-
insertResourceInstanceBefore(root,instance,titles.item(0));
1104+
constfirstTitle=titles[0];
1105+
insertResourceInstanceBefore(
1106+
root,
1107+
instance,
1108+
firstTitle&&firstTitle.namespaceURI!==SVG_NAMESPACE
1109+
? firstTitle
1110+
: null,
1111+
);
10811112
break;
10821113
}
10831114
case'meta': {
@@ -1397,16 +1428,21 @@ function insertResourceInstanceBefore(
13971428

13981429
exportfunctionisHostResourceType(type: string,props: Props): boolean{
13991430
letresourceFormOnly: boolean;
1431+
letnamespace: string;
14001432
if(__DEV__){
14011433
consthostContext=getHostContext();
14021434
resourceFormOnly=getResourceFormOnly(hostContext);
1435+
namespace=getNamespace(hostContext);
14031436
}
14041437
switch(type){
14051438
case 'base':
1406-
case 'meta':
1407-
case 'title': {
1439+
case 'meta': {
14081440
returntrue;
14091441
}
1442+
case 'title': {
1443+
consthostContext=getHostContext();
1444+
returngetNamespace(hostContext)!==SVG_NAMESPACE;
1445+
}
14101446
case'link': {
14111447
const{onLoad,onError}=props;
14121448
if(onLoad||onError){
@@ -1417,6 +1453,11 @@ export function isHostResourceType(type: string, props: Props): boolean {
14171453
' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or'+
14181454
' somewhere in the <body>.',
14191455
);
1456+
}elseif(namespace===SVG_NAMESPACE){
1457+
console.error(
1458+
'Cannot render a <link> with onLoad or onError listeners as a descendent of <svg>.'+
1459+
' Try removing onLoad={...} and onError={...} or moving it above the <svg> ancestor.',
1460+
);
14201461
}
14211462
}
14221463
returnfalse;
@@ -1426,11 +1467,18 @@ export function isHostResourceType(type: string, props: Props): boolean {
14261467
const{href,precedence,disabled}=props;
14271468
if(__DEV__){
14281469
validateLinkPropsForStyleResource(props);
1429-
if(typeofprecedence!=='string'&&resourceFormOnly){
1430-
console.error(
1431-
'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.'+
1432-
' Consider adding precedence="default" or moving it into the root <head> tag.',
1433-
);
1470+
if(typeofprecedence!=='string'){
1471+
if(resourceFormOnly){
1472+
console.error(
1473+
'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.'+
1474+
' Consider adding precedence="default" or moving it into the root <head> tag.',
1475+
);
1476+
}elseif(namespace===SVG_NAMESPACE){
1477+
console.error(
1478+
'Cannot render a <link rel="stylesheet" /> as a descendent of an <svg> element without knowing its precedence.'+
1479+
' Consider adding precedence="default" or moving it above the <svg> ancestor.',
1480+
);
1481+
}
14341482
}
14351483
}
14361484
return(
@@ -1450,17 +1498,31 @@ export function isHostResourceType(type: string, props: Props): boolean {
14501498
// precedence with these for style resources
14511499
const{src,async,onLoad,onError}=props;
14521500
if(__DEV__){
1453-
if(async!==true&&resourceFormOnly){
1454-
console.error(
1455-
'Cannot render a sync or defer <script> outside the main document without knowing its order.'+
1456-
' Try adding async="" or moving it into the root <head> tag.',
1457-
);
1458-
}elseif((onLoad||onError)&&resourceFormOnly){
1459-
console.error(
1460-
'Cannot render a <script> with onLoad or onError listeners outside the main document.'+
1461-
' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or'+
1462-
' somewhere in the <body>.',
1463-
);
1501+
if(async!==true){
1502+
if(resourceFormOnly){
1503+
console.error(
1504+
'Cannot render a sync or defer <script> outside the main document without knowing its order.'+
1505+
' Try adding async="" or moving it into the root <head> tag.',
1506+
);
1507+
}elseif(namespace===SVG_NAMESPACE){
1508+
console.error(
1509+
'Cannot render a sync or defer <script> as a descendent of an <svg> element.'+
1510+
' Try adding async="" or moving it above the ancestor <svg> element.',
1511+
);
1512+
}
1513+
}elseif(onLoad||onError){
1514+
if(resourceFormOnly){
1515+
console.error(
1516+
'Cannot render a <script> with onLoad or onError listeners outside the main document.'+
1517+
' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or'+
1518+
' somewhere in the <body>.',
1519+
);
1520+
}elseif(namespace===SVG_NAMESPACE){
1521+
console.error(
1522+
'Cannot render a <script> with onLoad or onError listeners as a descendent of an <svg> element.'+
1523+
' Try removing onLoad={...} and onError={...} or moving it above the ancestor <svg> element.',
1524+
);
1525+
}
14641526
}
14651527
}
14661528
return(async: any)&&typeofsrc==='string'&&!onLoad&&!onError;

‎packages/react-dom-bindings/src/client/ReactDOMHostConfig.js‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
8888
import{
8989
prepareToRenderResources,
9090
cleanupAfterRenderResources,
91+
clearRootResources,
9192
isHostResourceType,
9293
}from'./ReactDOMFloatClient';
9394

@@ -219,6 +220,16 @@ export function getPublicInstance(instance: Instance): Instance {
219220
returninstance;
220221
}
221222

223+
exportfunctiongetNamespace(hostContext: HostContext): string{
224+
if(__DEV__){
225+
consthostContextDev: HostContextDev=(hostContext: any);
226+
returnhostContextDev.namespace;
227+
}else{
228+
consthostContextProd: HostContextProd=(hostContext: any);
229+
returnhostContextProd;
230+
}
231+
}
232+
222233
export functionprepareForCommit(containerInfo: Container): Object|null{
223234
eventsEnabled=ReactBrowserEventEmitterIsEnabled();
224235
selectionInformation=getSelectionInformation();
@@ -715,11 +726,18 @@ export function clearContainer(container: Container): void {
715726
if(enableHostSingletons){
716727
constnodeType=container.nodeType;
717728
if(nodeType===DOCUMENT_NODE){
729+
clearRootResources(container);
718730
clearContainerSparingly(container);
719731
}elseif(nodeType===ELEMENT_NODE){
720732
switch(container.nodeName){
733+
case'HEAD': {
734+
// If we are clearing document.head as a container we are essentially clearing everything
735+
// that was hoisted to the head and should forget the instances that will no longer be in the DOM
736+
clearRootResources(container);
737+
// fall through to clear child contents
738+
}
739+
// eslint-disable-next-line-no-fallthrough
721740
case'HTML':
722-
case'HEAD':
723741
case'BODY':
724742
clearContainerSparingly(container);
725743
return;
@@ -910,6 +928,19 @@ function getNextHydratable(node) {
910928
if(nodeType===ELEMENT_NODE){
911929
constelement: Element=(node: any);
912930
switch(element.tagName){
931+
// This is subtle. in SVG scope the title tag is case sensitive. we don't want to skip
932+
// titles in svg but we do want to skip them outside of svg. there is an edge case where
933+
// you could do `React.createElement('TITLE', ...)` inside an svg scope but the SSR serializer
934+
// will still emit lowercase. Practically speaking the only time the DOM will have a non-uppercased
935+
// title tagName is if it is inside an svg.
936+
// Other Resource types like META, BASE, LINK, and SCRIPT should be treated as resources even inside
937+
// svg scope because they are invalid otherwise. We still don't need to handle the lowercase variant
938+
// because if they are present in the DOM already they would have been hoisted outside the SVG scope
939+
// as Resources. So while it would be correct to skip a <link> inside <svg> and this algorithm won't
940+
// skip that link because the tagName will not be uppercased it functionally is irrelevant. If one
941+
// tries to render incompatible types such as a non-resource stylesheet inside an svg the server will
942+
// emit that invalid html and hydration will fail. In Dev this will present warnings guiding the
943+
// developer on how to fix.
913944
case'TITLE':
914945
case'META':
915946
case'BASE':

‎packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,7 @@ function pushTitle(
13711371
target: Array<Chunk|PrecomputedChunk>,
13721372
props: Object,
13731373
responseState: ResponseState,
1374+
insertionMode: InsertionMode,
13741375
noscriptTagInScope: boolean,
13751376
): ReactNodeList{
13761377
if(__DEV__){
@@ -1415,6 +1416,8 @@ function pushTitle(
14151416

14161417
if(
14171418
enableFloat&&
1419+
// title is valid in SVG so we avoid resour
1420+
insertionMode!==SVG_MODE&&
14181421
!noscriptTagInScope&&
14191422
resourcesFromElement('title',props)
14201423
){
@@ -1926,6 +1929,7 @@ export function pushStartInstance(
19261929
target,
19271930
props,
19281931
responseState,
1932+
formatContext.insertionMode,
19291933
formatContext.noscriptTagInScope,
19301934
)
19311935
: pushStartTitle(target,props,responseState);

0 commit comments

Comments
 (0)