Isomorphic rendering sporadically renders attributes in a different order #6451

Closed
FuzzySockets opened this Issue Apr 8, 2016 · 48 comments

Projects

None yet
@FuzzySockets

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) a status update..." name="status" value=
(server) a status update..." value="" name="statu

I'm using react 15.0.0

@mridgway
mridgway commented Apr 8, 2016

This is most likely due to v8 using the incorrect order in Object.assign. I recommend that you polyfill Object.assign = require('object-assign'); in your server code.

@FuzzySockets

@mridgway No such luck...

Ran npm install --save object-assign
And added Object.assign = require('object-assign') to my entry file, to the module responsible for rendering the server-side react, and in both places just to confirm it's not working.

I'm using chrome Version 49.0.2623.87 (64-bit), and updating chrome now to see if that makes a difference.

@zpao
Member
zpao commented Apr 8, 2016

If you have Object.assign in your environment already I don't think that will work because object-assign will use Object.assign if it exists so you're really just saying Object.assign = Object.assign.

@FuzzySockets

@zpao Thank you sir...

Object.assign = null;
Object.assign = require('object-assign');

did the trick!

@syranide
Contributor
syranide commented Apr 8, 2016

@zpao Hmm, wasn't this one of the original reasons for the polyfill? I seem to remember this exact discussion.

@zpao
Member
zpao commented Apr 8, 2016

It does sound familiar but I didn't think that was part of the original discussion. I recall that we were just going to use Object.assign directly and then there was an issue with the spec (it would throw when getting null, which would have been really bad with how we were cloning props). Firefox had already implemented that old version of the spec so for a short period Facebook was actually completely broken in Firefox Nightly.

FWIW, I think the problem that's being exposed here could have been encountered before, especially if you ever used spread in JSX (or even regular object spread) and use Babel. Both of these get compiled to using Object.assign (essentially). The difference though is probably frequency, instead of just hitting it for those cases, it's happening for normal use of props.

I'm not sure the best way to handle this. I really want us to be able to use native code and not end up relying on our own dated polyfill. See #6376 for that discussion. We assumed it was perfectly safe, particularly due to the fact that we effectively have been using native Object.assign for facebook.com for at least a year (we did some module replacement internally). We don't do much server rendering so did not properly consider this case (and honestly completely forgot that v8 still has this bug)

@sebmarkbage
Member

Seems like this would be a problem Babel spread too. Typically this is the kind of issue a polyfill should test for before assuming compatibility of the native impl.

@jimfb
Contributor
jimfb commented Apr 8, 2016

I wish we checked for validity by walking the DOM rather than calculating a checksum for markup. It's more about us not being spec-compatible, rather than a V8 bug.

@sebmarkbage
Member

No, V8 is not spec compliant. The enumeration order is defined by spec and fixed in newer V8s.

There are other APIs like Intl number formatting that does allow for different output. That's a separate issue and even if we walked the DOM that wouldn't automatically solve that issue.

@jimfb
Contributor
jimfb commented Apr 8, 2016

Ah, ok, they fixed the spec :). We're depending on previously undefined behavior. But the behavior is still undefined if we're targeting anything below ES6, so one could argue that we should solve this within React anyway.

@buzinas
buzinas commented Apr 12, 2016

Only pasting the text from the other issue here:

I'm developing an universal app with React, and after updating to v15.0.1, I started getting this error:

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) ut class="md-input" name="txtCadmus" pat
(server) ut class="md-input" pattern="[0-9]*" nam

It seems to be some bug on V8 Object.assign reordering or something like that, but I'm not completely sure.

Tried to change a bunch of stuff (e.g: force both server and client to use a polyfill, or using Babel transform object-assign plugin), but nothing solved the problem.

Then, I uninstalled React 15.0.1 and installed 0.14.8 again, and the problem was gone.

Btw, it's pretty simple to simulate the issue:

Code

ReactDOM.render(
  <input name="my-input" className="my-class" type="number" pattern="[0-9]*" />, document.querySelector('#main')
);

console.log(document.querySelector('#main').innerHTML)

console.log(ReactDOMServer.renderToString(
  <input name="my-input" className="my-class" type="number" pattern="[0-9]*" />
));

React 15 not working

15 0 1

React 0.14.8 working as expected

0 14 8

@zpao
Member
zpao commented Apr 12, 2016

Yes, V8 has a bug in their Object.assign implementation. I've heard the bug might have been fixed but that's really unlikely to end up in Node v4.x || 5.x.

@buzinas
buzinas commented Apr 13, 2016

Diving into the code, it seems that it has nothing to do with the V8 Object.assign implementation.

If you open jsBin in Firefox, and paste the code above, React v0.14 will work as expected, while React v15.0 will mess the attributes.

It seems that the problem is because until v15, React created the elements using innerHTML, so the order was maintained. Now that it uses document.createElement, the browsers change the attributes ordering, causing those diffs.

I can see two ways of solving this problem:
1- change the ordering attributes in renderToString to be exactly in the same order that browsers render (but I'm not sure this is the good way, since browsers may behave differently).
2- change the checksum algorithm in a way that it doesn't matter what are the attributes ordering, checking only if they exist and are the same (probably the cost can be high in big apps, but better this than inject an entirely new markup).

@sebmarkbage
Member

^ cc @spicyj

@zpao
Member
zpao commented Apr 13, 2016

change the checksum algorithm in a way that it doesn't matter what are the attributes ordering, checking only if they exist and are the same (probably the cost can be high in big apps, but better this than inject an entirely new markup)

Ideally that is what we would do but I think you are definitely right and it would be expensive. Server rendering is already relatively expensive / slow, so you might lose the advantages you would otherwise get.

@spicyj
Member
spicyj commented Apr 13, 2016

When reviving server-rendered markup, we generate markup on the client using the exact same codepath and generate the checksum based on that – so any changes to the createElement mode won't affect checksums and are unrelated.

@spicyj
Member
spicyj commented Apr 13, 2016

It sounds like there are two related bugs in V8 that mess with the property enumeration order (thanks @mridgway for digging on these):

https://bugs.chromium.org/p/v8/issues/detail?id=4118
https://bugs.chromium.org/p/v8/issues/detail?id=3056

Sounds like the best solution is to send a PR to https://github.com/sindresorhus/object-assign that tests for these cases and doesn't use Object.assign if so.

@buzinas
buzinas commented Apr 19, 2016

@spicyj The only thing that I can't understand is why with React 0.14.8, everything works smoothly, and when upgrading to React 15.0.1, the isomorphic rendering stops working. It doesn't seem to be something related to Object.assign.

@jimfb
Contributor
jimfb commented Apr 19, 2016 edited

@buzinas In React 0.14.8, we had an internal polyfill for Object.assign. In React 15.0, we started using the native Object.assign (if/when available) instead of the polyfill. Thus the problem when the native implementation of Object.assign is buggy.

@FuzzySockets
FuzzySockets commented Apr 19, 2016 edited

@buzinas When I polyfilled Object.assign with the object-assign npm package, things started working again. Not sure what they changed in 15.0 that causes it to break with the default Object.assign though.

Edit: Spoke milliseconds too soon @jimfb now I know why 😆

@buzinas
buzinas commented Apr 19, 2016

@FuzzySockets @jimfb I'm probably doing something wrong, then. Because I'm forcing the Object.assign polyfill, but my app keep warning about the checksum :(

@FuzzySockets

@buzinas In your entry, add:

Object.assign = null;
Object.assign = require('object-assign');
@gaearon
Member
gaearon commented Apr 19, 2016

object-assign npm package is the polyfill React uses now. The problem is that it first tries the native implementation which is buggy in some cases.

#6451 (comment) works because it deletes the native implementation first, so the polyfill doesn’t use it.

@buzinas
buzinas commented Apr 19, 2016 edited

Now everything makes sense :P
Thanks everyone, I'll give this a try!


PS: It works! 💃

@gaearon
Member
gaearon commented Apr 19, 2016

Not saying it’s an official recommendation though. Ideally object-assign should feature test broken native versions.

@mridgway

object.assign package has the proper order check.

@buzinas
buzinas commented Apr 20, 2016 edited

@gaearon You're right. That's what es6-promise does, for example.

But, for now, I'm OK with this workaround, since it's much better than giving up server rendering.

@FuzzySockets

Does this only occur in certain versions of v8?

@FuzzySockets

@mridgway Thanks for that one

@chandlerprall
chandlerprall commented Apr 20, 2016 edited

@FuzzySockets it's supposedly been fixed in v8 since December (it appears the current version of Chrome doesn't have the issue), but it's unlikely to make its way into node until v6.

@spicyj spicyj added a commit to spicyj/object-assign that referenced this issue Apr 23, 2016
@spicyj spicyj Add feature test against buggy V8 versions
Incorrect property ordering is causing us some problems downstream at
facebook/react#6451.
c973456
@spicyj spicyj added a commit to spicyj/object-assign that referenced this issue Apr 28, 2016
@spicyj spicyj Add feature test against buggy V8 versions
Incorrect property ordering is causing us some problems downstream at
facebook/react#6451.
4364790
@spicyj
Member
spicyj commented May 2, 2016 edited

I just released a new version of object-assign (4.1.0) that includes a feature test for these V8 bugs. Can someone reinstall and try with the new version and verify that this is fixed?

@chandlerprall

@spicyj seems to have resolved the issue for me! was getting the mismatched checksum very consistently (~80%) and haven't seen it once since re-installing all modules.

It appears I can even remove Object.assign = require('object-assign'); from my startup script as something else I'm requiring pulls in object-assign already. Though this may not be the case for everyone.

@spicyj
Member
spicyj commented May 2, 2016

Sounds great. I'll close this; hopefully it works for everyone else too.

@spicyj spicyj closed this May 2, 2016
@th0r th0r referenced this issue in gcanti/tcomb-form-templates-bootstrap May 3, 2016
Closed

Support React 15 #7

@eliseumds

object-assign@4.1.0 works :)

@anbhole anbhole referenced this issue in react-bootstrap/react-bootstrap May 12, 2016
Closed

controlId can break server side rendering #1891

@crossman
crossman commented May 13, 2016 edited

I'm using object-assign@4.1.0 and still getting this. It only started once I upgraded to react 15

@gaearon
Member
gaearon commented May 13, 2016

I'm using object-assign@4.1.0 and still getting this. It only started once I upgraded to react 15

Please provide a project reproducing the issue. Also please make sure you don’t have an older object-assign somewhere deeper in the tree, e.g. inside node_modules/react/node_modules.

@crossman

Yeah I did check the tree because that was my first thought, I'll see if I can find some time put together a sample project on Monday.

@crossman

On digging further it turns out object-assign@4.1.0 did solve it for all other pages. I was running into this and didn't realize it was a different issue. Please disregard the noise.

@afc163 afc163 referenced this issue in react-component/checkbox Jun 7, 2016
Merged

Input props remove assign #14

@joshuaandrewhoffman

Apologies for the naive question, but where in my project does this object.assign switcheroo need to occur? Lot of comments saying I need to do it, but I haven't a clue what file to do it in.

(I may have other issues besides just the placement. Typescript seems angry at the period in Object.assign when I try to import it; I get "= expected". I'm also throwing on having a bad path to object-assign, but I think I can probably overcome those problems)

@spicyj
Member
spicyj commented Jun 10, 2016

Assuming you're using npm – if you npm ls and see object-assign@4.0.1 then simply deleting node_modules and reinstalling will probably fix it. You'll want 4.1.0. If you're using the browser build of React, use react-15.1.0.js or later.

@wchargin

I'm hitting something similar, where using an object spread is reordering my props. In particular, I have the following symptoms—with

const p1 = this.props;
const p2 = {...p1};
const s1 = Object.keys(p1).join(", ");
const s2 = Object.keys(p2).join(", ");
if (s1 !== s2) {
    console.log(s1);
    console.log(s2);
    console.log(Object.assign === require("object-assign"));
    console.log();
}

I get

className, onClick, aria-pressed, aria-role, children
onClick, children, aria-pressed, aria-role, className
true

which really seems like a bug to me. Frustratingly, I can reproduce this 100% within this component but can't seem to reproduce it elsewhere.

I do note that the Babelified code uses var p2 = _extends({}, p1);, where Babel defines _extends at the module level to be Object.assign || function …, and that if I add console.log('' + eval('_extends')) I get function assign() { [native code] }.

I'm on object-assign@4.1.0 and react{,-dom}@15.3.1, node@4.2.4, npm@3.10.6. Does this point toward a babel issue? babel-core@6.14.0, babel-plugin-transform-object-rest-spread@6.8.0.

Adding Object.assign = null; Object.assign = require("object-assign"); to the very top of my webpack config does not help. I run webpack with babel-node ./node_modules/.bin/webpack-dev-server --config config/webpack.config.dev.js.

Grateful for any advice.

@aweary
Collaborator
aweary commented Aug 30, 2016

@wchargin would you be able to share a small test case reproducing this?

@wchargin

@Aweary Here's the smallest I can manage: https://github.com/wchargin/object-spread-repro-case. The only dependencies are Babel, React, and Webpack.

As noted at the top of demo.js, the bug goes away if you force object-assign to be polyfilled when you run the generated bundle. So it looks like the polyfill isn't getting injected properly somehow?

You should see the following:

$ git clone git@github.com:wchargin/object-spread-repro-case.git
Cloning into 'object-spread-repro-case'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 14 (delta 3), reused 14 (delta 3), pack-reused 0
Receiving objects: 100% (14/14), done.
Resolving deltas: 100% (3/3), done.
Checking connectivity... done.
$ cd object-spread-repro-case/
$ [ -d node_modules ] && rm -rf node_modules
$ npm install >/dev/null 2>/dev/null
$ npm run build

> @ build /home/wchargin/git/object-spread-repro-case
> rm -rf dist && webpack

Hash: 1fd7ef5334c0d28ceca1
Version: webpack 1.13.2
Time: 1312ms
    Asset    Size  Chunks             Chunk Names
bundle.js  706 kB       0  [emitted]  main
    + 167 hidden modules
$ npm run demo

> @ demo /home/wchargin/git/object-spread-repro-case
> node demo.js

First render...
className, onClick, aria-pressed, aria-role, children
className, onClick, aria-pressed, aria-role, children

className, onClick, aria-pressed, aria-role, children
className, onClick, aria-pressed, aria-role, children


Second render...
className, onClick, aria-pressed, aria-role, children
onClick, children, aria-pressed, aria-role, className
^^^ Reproduced the bug!

className, onClick, aria-pressed, aria-role, children
onClick, children, aria-pressed, aria-role, className
^^^ Reproduced the bug!


$ node --version && npm --version
v4.2.4
3.10.6
$ npm ls | grep object-assign
$ npm ls | grep object-assign
│ └── object-assign@4.1.0
$ 
@mridgway
mridgway commented Aug 30, 2016 edited

@wchargin Most likely you need to move the Object.assign override before all other requires. If a component that uses spread is required before the override, it will use the default Object.assign which is incorrect as of node v4.x. So yes, demo.js is a good place for it.

@spicyj
Member
spicyj commented Aug 30, 2016 edited

Yeah, best to include Object.assign = require('object-assign'); or similar in the entry point of your app if you use Node 4.x.

@wchargin

Thanks, @mridgway @spicyj. I'd considered that that might be the problem, and tried to do this as

// index.js
const isServerRendering = typeof document === "undefined";

if (isServerRendering) {
    Object.assign = null;
    Object.assign = require("object-assign");
}

import initializeClient from './client';
if (!isServerRendering) {
    initializeClient();
}

export {default} from './server';

but this didn't work. What I failed to realize was that Babel hoists imports including export-froms, so this had to become

const isServerRendering = typeof document === "undefined";

if (isServerRendering) {
    Object.assign = null;
    Object.assign = require("object-assign");
}

if (!isServerRendering) {
    require("./client").default();
}

module.exports = {
    default: require("./server").default,
};

All appears to be peaceful now.

TL;DR for people passing by later: read the generated code to make sure your polyfill really is happening when you think it is.

@spicyj
Member
spicyj commented Aug 30, 2016

Got it. Nice sleuthing.

@wchargin wchargin added a commit to wchargin/wchargin.github.io that referenced this issue Sep 5, 2016
@wchargin wchargin Use an Object.assign polyfill
Summary:
I was running into facebook/react#6451 even though I'm on React 15.3.1
and `npm ls | grep object-assign` shows only `object-assign@4.1.0`. A
bit of debugging shows that the polyfill wasn't being applied early
enough in the node bundle that's used for server-side rendering; we
have to be sure to include it before any React components are used, so
we might as well make it the very first thing!

Test Plan:
Load localhost:8080/skills and see that there aren't any SSR errors.
4ceaf2c
@wchargin wchargin added a commit to wchargin/wchargin.github.io that referenced this issue Sep 5, 2016
@wchargin wchargin Use an Object.assign polyfill
Summary:
I was running into facebook/react#6451 even though I'm on React 15.3.1
and `npm ls | grep object-assign` shows only `object-assign@4.1.0`. A
bit of debugging shows that the polyfill wasn't being applied early
enough in the node bundle that's used for server-side rendering; we
have to be sure to include it before any React components are used, so
we might as well make it the very first thing!

Test Plan:
Load localhost:8080/skills and see that there aren't any SSR errors.
333a574
@dhalbrook dhalbrook referenced this issue in JedWatson/react-select Oct 26, 2016
Closed

Isomorphic render issue on 1.0rc2 #1325

@abotchen

Server side and client side should use match

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment