Rendering on a first party page in which we don't control or want to alter the styles
In its current shape, React Native Web is not ideal in this case :
react-native-web automatically injects __react-native-style on the document using React, not necessarily the one from the render targetWhat I'd really like would be:
react-native-web APII see that the current way of injecting is performed as a hook on StyleSheet.create, which makes it fairly complicated to add these options.
If you consider this issue, I'd suggest:
mountNode.ownerDocument (to work with frames simply)ReactDOM.unstable_renderSubtreeIntoContainer to inject the style automatically if the mountNode.ownerDocument doesn't have it yet.shouldInjectCSSReset: boolean = true option to appParameters for other cases (e.g. rendering in a shadow root)getCSSReset function to AppRegistery for manual injection (e.g. inject it in a shadow root)Thanks a lot.
Why do you need these options? Allowing anyone to inject global styles is a problem because now components cannot depend on a consistent global environment setup by the framework.
Let's say my JS bundle loads on page A, and I want isolation from the styles in this page to avoid any bad surprise. So I have to render my components in a source-less iframe iframe B.
Right now, React Native Web injects the global styles in A because the JS bundle gets A's document, these styles can have side effects on the A UI integrity, and are missing in B. I'd like to be able to get an up-to-date reset to inject myself in these frames.
The components can depend on the consistent environment by default, I just like it to be tweakable in order to make it work for bit-more uncommon cases.
React Native Web injects the global styles in A because the JS bundle gets A's document, these styles can have side effects on the A UI integrity
Are you rendering into a document that has components rendered by another library? RN's style reset is very minimal (a subset of what is in most resets) and mainly targets pseudo-elements; it seems unlikely to cause problems for an existing document.
So I have to render my components in a source-less iframe iframe B.
Are you rendering a tree into a single iframe, or rendering multiple iframes? TBH, I think you can just clone the stylesheet RN injects and inject that clone into whatever iframes you are creating.
The thing is: I don't have any control on the page my React copy runs, any CSS change might breaks things. And I'd rather not have to insert, let the page render incorrectly for a few moments, the remote it.
Just in order to make it "simpler" to work around, is there any chance that:
// StyleSheet
module.exports = {
// …
__insertStyleSheet: (id) => {
// check if the server rendered the style sheet
styleElement = document.getElementById(id);
// if not, inject the style sheet
if (!styleElement) {
document.head.insertAdjacentHTML(
'afterbegin',
`<style id="${STYLE_SHEET_ID}">${defaultStyleSheet}</style>`
);
}
},
create(styles) {
if (shouldInsertStyleSheet) {
module.exports.__insertStyleSheet(STYLE_SHEET_ID);
shouldInsertStyleSheet = false;
}
// …
}
}
so that we can do:
import StyleSheet from "react-native/dist/apis/StyleSheet"
StyleSheet.__insertStyleSheet = noop
import { getDefaultStyleSheet } from "react-native/dist/apis/StyleSheet/css"
That's a bit hacky, but at least gives the key to solve the issue.
The thing is: I don't have any control on the page my React copy runs, any CSS change might breaks things. And I'd rather not have to insert, let the page render incorrectly for a few moments, the remote it.
I don't understand what you're trying to do or why you think RN's stylesheet is going to break things.
I'm trying to render third-party widgets which use RNW (Ã la disqus). RNW stylesheet can break the page (for instance just the font-family: sans-serif in the reset can break the first-party styles).
I'll look into how to support the into-iframe rendering. If you can show me an example of how the font-family reset can break a site, I can look into addressing that too.
This is something I would be interested in too, as I'm looking to inject a RNW app inside a larger React Web app and I want to minimize and control the global CSS that's injected into the page. Feels like RNW should take responsibility for its own resets, but allow control of them in the case where it is injected into a pre-existing document that already has one declared.
Is that a hypothetical concern or something happening in reality?
This is definitely happening in reality in my example setup. I just got a "Hello World" running where I'm mounting a RNW app to a div that is nested inside the main React js app. I noticed that a <style> tag is injected into the head of the page (since there's only one obviously and there can't be more than one html tag on the page.
Out of the gate, this doesn't appear to affect styling on anything in the master app CSS, but I can see that it _may_ potentially affect things as more code is built-out, so its something to be aware of and I'll update this as I make more progress. :)
this doesn't appear to affect styling on anything in the master app CSS
ok, that means it's a hypothetical concern.
as you can see here there are hardly any global styles, none of them likely to negatively impact an app. the stylesheet is injected before any other stylesheets, so if there were a style edge-case you can override the relevant reset style in your app code
Yeah, I think this should be fine for my use case. I'll continue to monitor it, but it looks likely that it may not interfere. Thanks!
I currently use a fork that makes it ok for my concerns (working in source-less iframes in order to isolate styles):
What I did is:
StyleManager#mainSheets a Map<Window, StyleSheet>StyleManager#addRoot to register a window and inject a stylesheetStyleManager#removeRoot to unregister a windowStyleManager#setDeclarations iterate over mainSheets to apply changesStyleRegistry#addRoot to register a window (calls its StyleManager's)StyleRegistry#removeRoot to unregister a window (calls its StyleManager's)ReactNative.render & AppRegistry.runApplication call StyleRegistry#addRoot with node.ownerDocument.defaultView as parameterReactNative.unmountComponentAtNode & AppRegistry.unmountApplicationComponentAtRootTag call StyleRegistry#removeRoot with node.ownerDocument.defaultView as parameterReactOwner)@necolas Would you be likely to accept a PR with these changes?
Put a PR up and I'll take a look! Thanks
@bloodyowl are you still planning to share a PR for this? Also when are you publishing your photo editor thing? ;)
@necolas I have it almost done by wrapping ReactDOM.render in RNW, I just need to add a mechanism to add the stylesheet if RNW doesn't handle the root render (it doesn't work with storybook right now because it uses plain ReactDOM.render(reactElement, rootNode).
and the photo editor will arrive soon 😄
would it seem reasonable to you if I add a ReactNativeRoot component?
it would be added automatically in ReactNative.render and renderApplication, and it would require people to use it directly when directly rendering through ReactDOM or as an external component child.
<ReactNativeRoot>
<MyComponent />
</ReactNativeRoot>
This component would register its root window in order to inject the stylesheets and run modality in the right window object. The only downside is that it would require a two-phase initial rendering (pseudo code):
class ReactNativeRoot extends React.Component {
state = { hasRegistered: !canUseDOM }
componentDidMount() {
registerRoot(this.container.ownerDocument.defaultView)
this.setState({ hasRegistered: true })
}
render() {
if(!this.state.hasRegistered) {
return <noscript ref={c => this.container = c} />
}
return React.Children.only(this.props.children)
}
}
Hey, do you have code I can look at for this? Is there a branch you have somewhere on GitHub?
Stumbled upon a similar issue while I was trying to integrate RNW into an app that is rendered inside of an iframe. Would it be hard to use the rootTag.ownerDocument when creating the WebStyleSheet?
Not sure who depends on ReactNativeStyleResolver but I'd be happy to dig and learn more about the codebase and prepare a PR if you think that this is feasible. I imagine that a non-singleton approach is doable and apps that run in the same document can still share styles.
TBH, I think you can just clone the stylesheet RN injects and inject that clone into whatever iframes you are creating
I ended up doing this.
const rnwStyleSheet = document.getElementById('react-native-stylesheet')
if (rnwStyleSheet) {
const destSheet = document.createElement('style')
frameDocument.head.appendChild(destSheet)
;[].slice.call(rnwStyleSheet.sheet.rules).forEach(rule => {
destSheet.sheet.insertRule(rule.cssText, destSheet.sheet.length)
})
rnwStyleSheet.sheet.insertRule = (rule, index) => {
destSheet.sheet.insertRule(rule, index)
return insertRule.call(rnwStyleSheet.sheet, rule, index)
}
}
const rnwModality = document.getElementById('react-native-modality')
if (rnwModality) {
frameDocument.head.appendChild(rnwModality)
}
@necolas can you think of any other API or internal business logic that might not work properly when rendering in iframes?
Side node: to be clear the problem is not RNW's styles breaking things but rather the host page styles breaking my RNW app.
Thanks for the example code @giuseppeg! Some tweaks/fixes:
// Call createElement inside the target document. (Not sure if necessary, but seems more correct to me)
const destSheet = iframe.contentDocument.createElement('style')
// Add missing .rules here
destSheet.sheet.insertRule(rule.cssText, destSheet.sheet.rules.length)
// Get the insertRule function from the prototype
return CSSStyleSheet.prototype.insertRule.call(rnwStyleSheet.sheet, rule, index)
Even after cloning the CSS, we found all the UI elements were squashed up at the top of the iframe. I eventually realised the app's root element needed to be 100% tall. You can force this after rendering:
AppRegistry.runApplication('App', { rootTag: iframe.contentDocument.body });
iframe.contentDocument.body.firstChild.style.height = '100vh';
Or you can wrap your app's container in a tall flexbox, so the root of your app will grow into it:
<iframe {...props}>
<div style={{ display: 'flex', height: '100vh' }}>
<YourRNApp />
</div>
</iframe>
@joeytwiddle cool. While this fixes the issues with rendering styles inside of an iframe, I think that the framework as a whole might still misbehave because there are plenty of call points that reference the parent window globals.
Most helpful comment
I currently use a fork that makes it ok for my concerns (working in source-less iframes in order to isolate styles):
What I did is:
StyleManager#mainSheetsaMap<Window, StyleSheet>StyleManager#addRootto register a window and inject a stylesheetStyleManager#removeRootto unregister a windowStyleManager#setDeclarationsiterate overmainSheetsto apply changesStyleRegistry#addRootto register a window (calls itsStyleManager's)StyleRegistry#removeRootto unregister a window (calls itsStyleManager's)ReactNative.render&AppRegistry.runApplicationcallStyleRegistry#addRootwithnode.ownerDocument.defaultViewas parameterReactNative.unmountComponentAtNode&AppRegistry.unmountApplicationComponentAtRootTagcallStyleRegistry#removeRootwithnode.ownerDocument.defaultViewas parameterReactOwner)@necolas Would you be likely to accept a PR with these changes?