react: Document that you can't rely on React 16 SSR patching up differences
With SSR being loaded on client side, there are various wrong behaviors if the server’s HTML differs from the client’s HTML.
For a minimal example, I created this repository. Here is a code snippet:
class AppView extends React.Component {
render () {
const isServer = this.props.isServer
const styles = {
server: {
backgroundColor: 'red'
},
client: {
backgroundColor: 'green'
}
}
return (
<div>
{
isServer ?
<div style={styles.server}>isServer</div> :
<div style={styles.client}>isClient</div>
}
</div>
)
}
}
In the example I render a CSS background color of red for the server and green for the client. I force a difference by the server’s and client’s HTML with the isServer property.
With React 15, everything works as expected: the server renders a red background, but the client corrects it to green. With React 16 however the background stays the same, but the text changes as expected.
There are probably other similar behaviors. For example I found out about this bug, because I was conditionally rendering a complete component like so:
return someCondition && <MyComponent />
It becomes even more weird, because if there is additional JSX after that conditional rendering, it would render that additional JSX as if it is inside that MyComponent
return (
<div>
{ someCondition && <MyComponent /> }
<SomeOtherComponent />
</div>
)
becomes
return (
<div>
<MyComponent>
<SomeOtherComponent />
</MyComponent>
</div>
)
if someCondition === true on server side and someCondition === false on client side.
You can see this behavior on my website: http://smmdb.ddns.net/courses Open Chrome Dev Tools and lower the width until you get the mobile view, then reload the page and see how the list is wrapped inside another Component.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Comments: 19 (5 by maintainers)
@sebmarkbage, thanks for your thorough answers here. We’re running into this too and your comments helped us understand what’s going on.
That said, in all honesty, I don’t completely see how this is not considered a bug. It’s very hard and messy to ensure that what’s rendered server-side and client-side are 100% equal. For instance, what if a component displays something time-based and the client clock isn’t millisecond-precise equal to the server clock? (e.g. always). In our case, seemingly random (but consistent) items in a list suddenly get some child div content swapped.
Obviously everything can be worked around by copying all and every piece of global state from the server to the client (and even ensuring that eg the same millisecond time is used in the entire render call), but isn’t that major overkill?
Obviously, by using
(new Date()).getTime()as an input, our components aren’t 100% purely functional, but making React completely break because components aren’t 100% purely functional is quite a stretch 😃 I’m sure there’s other kinds of singleton-ish data than time that exhibit a similar dynamic.(in all honesty I think the screen size example from @Tarnadas is a nice example too)
Ah. The reason this happens is because we try to reuse the structure as much as possible. Because the first item is a div in both cases we try to reuse it. That’s why it retains its styles. If it was a different tag name it would patch it up but it’s risky to rely on this.
You can build a little infra that passes screen size to the server as a separate request but that might be overkill. If you don’t have screen size on the server you will server render the wrong output which could lead to flashes on the content when it finally restores the JS on the client. If your app is resilient to that, then there is a way to model that on the client.
You can render the initial mount with the server render screen size. Then in componentDidMount you can read the real screen size and if it doesn’t match up with your guess, then you rerender by setting state.
@thebuilder
If you want to render something different on the client, you can set state in
componentDidMountand do it there. https://github.com/facebook/react/issues/8017#issuecomment-256351955Btw, if you relied on React to clear out the DOM for you in 15 you can still do that explicitly in 16. Just do
container.innerHTML = ''beforeReactDOM.render. (In 17,ReactDOM.renderwill do that automatically but currently it doesn’t for backwards compat.)I also have a use case in which I intentionally have the server render something slightly different than the client will render, and it’s affected by this change in React 16 as well.
I have a component that renders a feed of items. On the server I render one page of items at a time, with conventional “Prev” and “Next” pagination links. On the client I render an infinitely scrollable virtualized list.
Rendering a conventional paginated list on the server helps improve SEO by giving search bots actual links to follow to subsequent pages and also ensures that clients with JS disabled can still paginate through the list.
There’s no visible flash when a JS-capable client hydrates the list because the only visible difference (pagination links) will typically be below the fold, so this should work out well. However, React 16 reuses certain server-rendered elements without removing their
classNames even though the client-rendered state doesn’t use thoseclassNames, and this breaks the design in weird and unexpected ways.I understand that it’s generally not advisable for server-rendered content to differ from client-rendered content, but this was a conscious choice made with full awareness of the disadvantages. I expected a slight performance penalty, but didn’t expect broken output.
If this scenario really is considered unsupported in React 16 and there are no plans to allow developers to opt into full validation when hydrating server HTML, then it seems like the dev mode warning should be an outright error in order to prevent subtle breakage from slipping into production. This change should probably also be called out prominently in the release notes, since it could unexpectedly break existing code that relies on the old behavior.