remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.34k stars 2.47k forks source link

Firefox - prop mismatch between server and client #4679

Closed jmlee2k closed 4 months ago

jmlee2k commented 1 year ago

What version of Remix are you using?

1.7.6

Steps to Reproduce

https://codesandbox.io/s/remix-prop-bug-g1hn4p?file=/app/routes/index.tsx

Expected Behavior

props should match on client and server

Actual Behavior

Warning: Prop "disabled" did not match. Server: "null" Client: "true"

cemcakirlar commented 1 year ago

For this particular case I have an idea. This looks like a JSON.stringify issue where remix loader data is stringified. You have "undefined" keys in your data. {disabled: undefined}, JSON.stringify makes them {disabled:null}, since "undefined" is not allowed in JSON objects. It is not a value, but null is a value. Your client mounts the component with the disabled property = true in he first render (most probably). Solution may be to make loader know in advance that the prop should be true but not undefined.

Edit: I just saw your sandbox. You dont use loader here but the issue is most probably some kind of "react server rendering of props with undefined values" thing.

jmlee2k commented 1 year ago

@cakirlarc - yeah, I came to the same conclusion, but the weird part is that it changes depending on the browser and state of the cache - it works in chrome all the time, and works in firefox with a clear cache.

It seems like a timing issue to me, but I'm not familiar enough with Remix or SSR in general to have anything more than a hunch.

brophdawg11 commented 1 year ago

This looks like some sort of Firefox issue with maintaining client side state on soft reloads. I can reproduce the same error in a super basic React SSR setup. The issue seems to be a soft reload after the useEffect has fired to enable the button. I changed the timeout to 2.5 seconds in my example - soft reloads within that 2.5 second window do not have the error. Once the button enables, the hydration issue arises on soft reloads.

Add the following to an index.mjs file and run node index.mjs.

import http from "node:http";
import * as React from "react";
import * as ReactDOMServer from "react-dom/server";

function App() {
  let [disabled, setDisabled] = React.useState(true);

  React.useEffect(() => {
    setTimeout(() => setDisabled(false), 2500);
  }, []);

  return React.createElement(
    "html",
    null,
    React.createElement(
      "head",
      null,
      React.createElement(
        "style",
        null,
        "button { border: 2px solid green; } " +
          "button[disabled] { border: 2px solid red; }"
      )
    ),
    React.createElement(
      "body",
      null,
      React.createElement("button", { disabled: disabled }, "Hello Button"),
      React.createElement("script", {
        crossOrigin: "true",
        src: "https://unpkg.com/react@18.2.0/umd/react.development.js",
      }),
      React.createElement("script", {
        crossOrigin: "true",
        src: "https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js",
      }),
      React.createElement("script", {
        async: "async",
        dangerouslySetInnerHTML: {
          __html:
            App.toString() +
            "\n" +
            "ReactDOM.hydrateRoot(document, React.createElement(App));",
        },
      })
    )
  );
}

let server = http.createServer((req, res) => {
  let doc = ReactDOMServer.renderToString(React.createElement(App));
  res.setHeader("Content-Type", "text/html");
  res.write("<!DOCTYPE html>" + doc);
  res.end();
});

server.listen(3000, () => {
  console.log("server listening on http://localhost:3000");
});

I need to log off for the day but I think this likely needs to get logged on the Firefox bug tracker, or maybe search through the React repo a bit and see if it's been encountered there before.

brophdawg11 commented 1 year ago

I logged https://bugzilla.mozilla.org/show_bug.cgi?id=1847798 to see if this is a Firefox bug

mogadanez commented 5 months ago

I have similar error when add Google AdSense snippet

<script async
                    src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=c-xxxxxxxxxx"
                    crossOrigin="anonymous"></script>
Prop `async` did not match. Server: "null" Client: "true"
brophdawg11 commented 4 months ago

I'm going to close this out as there's nothing Remix can really do about it.

Furthermore, the bug I filed 9 months ago was just marked as a duplicate of another bug opened 13 years ago 😨 ... so I'm no longer confident we'll see any traction.

In reading through the other bug I did find that this has come up enough that the difference in behavior from Firefox and all other browsers has made it's way onto MDN input docs and can apparently be controlled in firefox using the autocomplete attribute (search for firefox on the page if the hash link doesn't work).