Skip to content
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

Isomorphic rendering sporadically renders attributes in a different order #6451

Closed
nebulou5 opened this issue Apr 8, 2016 · 48 comments
Closed

Comments

@nebulou5
Copy link

nebulou5 commented Apr 8, 2016

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
Copy link
Contributor

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.

@nebulou5
Copy link
Author

nebulou5 commented Apr 8, 2016

@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
Copy link
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.

@nebulou5
Copy link
Author

nebulou5 commented Apr 8, 2016

@zpao Thank you sir...

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

did the trick!

@syranide
Copy link
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
Copy link
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
Copy link
Collaborator

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
Copy link
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
Copy link
Collaborator

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
Copy link
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
Copy link

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
Copy link
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
Copy link

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
Copy link
Collaborator

^ cc @spicyj

@zpao
Copy link
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.

@sophiebits
Copy link
Collaborator

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.

@sophiebits
Copy link
Collaborator

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
Copy link

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
Copy link
Contributor

jimfb commented Apr 19, 2016

@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.

@nebulou5
Copy link
Author

nebulou5 commented Apr 19, 2016

@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
Copy link

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 :(

@nebulou5
Copy link
Author

@buzinas In your entry, add:

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

@gaearon
Copy link
Collaborator

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
Copy link

buzinas commented Apr 19, 2016

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


PS: It works! 💃

@gaearon
Copy link
Collaborator

gaearon commented Apr 19, 2016

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

sophiebits added a commit to sophiebits/object-assign that referenced this issue Apr 28, 2016
Incorrect property ordering is causing us some problems downstream at
facebook/react#6451.
@sophiebits
Copy link
Collaborator

sophiebits commented May 2, 2016

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
Copy link

@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.

@sophiebits
Copy link
Collaborator

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

@eliseumds
Copy link

object-assign@4.1.0 works :)

@crossman
Copy link

crossman 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

@gaearon
Copy link
Collaborator

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
Copy link

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
Copy link

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.

@joshuaandrewhoffman
Copy link

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)

@sophiebits
Copy link
Collaborator

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
Copy link

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
Copy link
Contributor

aweary commented Aug 30, 2016

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

@wchargin
Copy link

@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
Copy link
Contributor

mridgway commented Aug 30, 2016

@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.

@sophiebits
Copy link
Collaborator

sophiebits commented Aug 30, 2016

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
Copy link

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.

@sophiebits
Copy link
Collaborator

Got it. Nice sleuthing.

wchargin added a commit to wchargin/wchargin.github.io that referenced this issue Sep 5, 2016
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.
wchargin added a commit to wchargin/wchargin.github.io that referenced this issue Sep 5, 2016
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.
@abotchen
Copy link

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests