theKashey / react-imported-component

✂️📦Bundler-independent solution for SSR-friendly code-splitting
MIT License
663 stars 39 forks source link

Warning: Did not expect server HTML to contain a <div> in <div>. #12

Closed lukebarton closed 6 years ago

lukebarton commented 6 years ago

I get this error when I use importedComponent and hydrate() to do SSR hydration. Both the SSR html and hydrated html appear identical and I do not get any error if I import the component normally.

const element = document.getElementById("app");
const app = (
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
);

// In production, we want to hydrate instead of render
// because of the server-rendering
if (window.___REACT_DEFERRED_COMPONENT_MARKS) {
  // rehydrate the bundle marks
  rehydrateMarks().then(() => {
    ReactDOM.hydrate(app, element);
  });
} else {
  ReactDOM.render(app, element);
}
const Another = importedComponent(() => import("./components/Another"));
// import Another from "./components/Another";

export default function App() {
  return (
    <div>
      <Helmet defaultTitle="Hello World!">
        <meta charSet="utf-8" />
        <link rel="icon" href={favicon} type="image/x-icon" />
      </Helmet>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/another" component={Another} />
        <Redirect to="/" />
      </Switch>
    </div>
  );
}

export default function generateHtml(markup, state) {
  // Get the serer-rendering values for the <head />
  const helmet = Helmet.renderStatic();

  const $template = cheerio.load(HTML_TEMPLATE);
  $template("head").append(
    helmet.title.toString() + helmet.meta.toString() + helmet.link.toString()
  );
  $template("head").append(
    `<script type="text/javascript">window.__PRELOADED_STATE__ = ${JSON.stringify(
      state
    ).replace(/</g, "\\u003c")};</script>`
  );
  $template("head").append(printDrainHydrateMarks());
  $template("#app").html(markup);

  return $template.html();
}
theKashey commented 6 years ago

What is inside window.___REACT_DEFERRED_COMPONENT_MARKS? Did it track loaded components properly? (babel plugin is required for this operation)

You can try to emulate babel plugin, like I did in tests

  function importedWrapper(marker, name, realImport) {
    return realImport;
  }

 const Another =  importedComponent(() => importedWrapper('components/Another', 'mark1', import("./components/Another"));
lukebarton commented 6 years ago

It's value looks something like ['-15frra52'] (not exact, not on laptop at present)

theKashey commented 6 years ago

Any example to reproduce? Could you add some random data-rnd attributes to random divs to figure out which one React don't like.

lukebarton commented 6 years ago

https://www.dropbox.com/s/ygg5bdtu6psozry/smsc.zip?dl=1

$ yarn install && webpack
$ node dist/server/index.js

Should get the server running

Then visit http://localhost:1234/another

theKashey commented 6 years ago

This is related to React 16.3 I've just updated dependencies and get the same.

theKashey commented 6 years ago

Ok. There is 2 bugs here.

  1. rename imported.js to imported.ts. You have to apply the same babel plugin to it. Just renaming will work well.
  2. I've found a problem with my regular expression, it does not properly match.
theKashey commented 6 years ago

I've fixed the actual problem, but this warning is still exists. And I am not sure why. HTML before and after hydration is the same.

lukebarton commented 6 years ago

The HTML (for me) has always been the same before and after. That's why I raised this issue.

Are you sure the things you have changed are actually related, if the warning still persists?

theKashey commented 6 years ago

There was a flickering of async component, I saw it, and I debug it - application was not waiting for Another to be loaded. This is the thing I've fixed. But still getting the error, regardless of Another position, but only when it exists.

I have to dig deeper.

theKashey commented 6 years ago

I've found an issue. React 16.3 deprecated componentWillMount, and this changed the way Loader work. Will fix tomorrow. It is much harder to write enzyme test to catch it, that to fix it.

theKashey commented 6 years ago

The problem was hydrate itself. If component was preloaded it will be reused on Loader creation, but the actual value will be set using setState. This is ok for enzyme or frontend, as long setState inside contructor or lifeCycle method will lead to the right state used in render.

In case of hydrate, look like this is bound to 16.3, "the current state" will be used on render. And it was just empty, resulting wrong rehydration. Wrong but invisible for the eye, as long correct state was already state, and changes will be applied before browser redraw.

theKashey commented 6 years ago

v4.3.1

kleinspire commented 5 years ago

@theKashey I'm getting the exact same behavior on react 16.8.6 and react-imported-component 5.5.3

Did you fix this or is it something I can safely ignore as long as there is no apparent mismatch between server rendered and hydrated markup?

Thank you

theKashey commented 5 years ago

This message from react is just about unexpected difference between server and client rendered code. Like - just don't match, by any reason.

Please look at your HTML coming from server, and the result HTML in the browser. That's the difference. Then we might think about "why".

If you have some example I could play with - just sent me a link, and I would handle the rest.

kleinspire commented 5 years ago

Well I have a monorepo and the concerning package is difficult to separate from the rest.

I can easily see what the server sends, but what is the proper way to extract the final js-hydrated html? I copied outerHtml from the render/hydration root div and the server response into diff tool and there was no difference.

kleinspire commented 5 years ago

I think I'm doing something fundamentally wrong with react-imported-components. When I export component to be loaded by react-imported-component:

const Test = () => 'test';

export default Test;

and re-export it as a route component:

import importComponent from 'react-imported-component';

const Test = importComponent(() => import(/* webpackChunkName: "test-component" */ './TestComponent'), {});

const TestRoute = {
  component: <Test />,
};

export default TestRoute;

App component used with hydrate:

import { Grommet } from 'grommet';

const App = ({children}) => (<Grommet theme={customTheme} full>{children}</Grommet>);

export default App;

client.js

import ReactDOM from 'react-dom';
import { rehydrateMarks } from 'react-imported-component';
import route from './TestRoute';
import App from './App';

const app = <App>{route.component}</App>;

rehydrateMarks().then(ReactDOM.hydrate(app, document.getElementById('root'));

I Get server rendered html:

<div id="root"><style data-styled="cqQlmF" data-styled-version="4.3.2" data-styled-streamed="true">
/* sc-component-id: sc-global-552078406 */
/* styled-components styles... */
</style><div class="StyledGrommet-sc-19lkkz7-0 cqQlmF">test</div></div>

Final html after rehydrateMarks().then(ReactDOM.hydrate()):

<div id="root"><div class="StyledGrommet-sc-19lkkz7-0 cqQlmF">test</div></div>

And console output:

Warning: Did not expect server HTML to contain the text node "test" in <div>.

Client App and ssr live in 2 separate packages. When App is built with webpack, it builds a version for browser (code splitting enabled) and a version with target: 'node' (a single bundle file without code splitting) for inclusion by ssr which sends the rendered html down to the client as a stream together with interleaveWithNodeStream from styled-components.

theKashey commented 5 years ago

You shall compare content of “root” element just before hydration, and just after. I am not sure that’s the problem, as long as “test” should be expected to be inside the only rendered div. Could you share the problematic example, or just final bundles to play with?

kleinspire commented 5 years ago

I replaced

rehydrateMarks().then(ReactDOM.hydrate(app, document.getElementById('root'));

with:

rehydrateMarks().then(() => {
  ReactDOM.hydrate(app, document.getElementById('root'));
});

The warning is now gone and most importantly the flicker when hydrating, is gone as well.

Thank you :)

theKashey commented 5 years ago

😅of course, you were calling hydrate too early.