facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
227.59k stars 46.43k forks source link

React 18 requests similar component trees for the useId #22733

Open theKashey opened 2 years ago

theKashey commented 2 years ago

Origins

Historically SSR was requiring some extra components to create a special "server" environment. Usually the ServerApplication is expected to be wrapped with different collectors and providers in order to power code splitting, style extraction, media queries, and many other things, some part of which don't have to be used on the client side, or even cannot exists at all.

ClientSide in turn, might contain some elements not required for the Server

The problem

According to my experiments for the proper use of useId one does not need ideally matching component trees - any number of "wrappers" are allowed, and only having "more than one child" is breaking id generation, however it does not cause any hydration id and cannot be detected without a context-aware test.

const ServerProvider = ({ children }) => (
  <context.Provider value={"server"}>
    <SugarComponent>{children}</SugarComponent>
  </context.Provider>
);

export const ServerApp = () => {
  return (
    <ServerProvider>
      {/* this one is breaking */}
      {/* <SugarComponent /> */} 👈 having this one will break id generation
      <SugarComponent>
        <App /> 👈 client will render only this
      </SugarComponent>
    </ServerProvider>
  );

The question

What level of similarity is really required? What actually matters - the path(so internals of siblings do not matter), or everything "before this point"(probably not due to Selective Hydration)?

How one can understand are component trees are similar enough, or one should not try to do that, comparing the expected behavior (matching Ids) without relying on implementation details of useId (currently one has to)

gaearon commented 2 years ago

Just to set expectations -- We're heads down preparing for the conf so the response may be delayed but thanks for a good writeup.

maraisr commented 2 years ago

Which conf is that one @gaearon?

eps1lon commented 2 years ago

Which conf is that one @gaearon?

@maraisr React Conf 2021

dominic-p commented 2 years ago

I think I'm running into the same thing here.

On the server side, I need to wrap my app with a shell like this:

<html>
   <body>
     <div id="root"></div> <!-- this is what gets hydrated on the client side -->
   </body>
</html>

This works great until I start using useId(). Now I get hydration mismatch errors because the ids don't match. I can "fix" it using Noop components like they did with Next.js, but that's super brittle and confusing.

Is this considered a bug or expected behavior? Is there a better workaround that I'm just not aware of?

theKashey commented 2 years ago

useId is based on Tree structure. There is no other workaround.

dominic-p commented 2 years ago

Thanks for the confirmation @theKashey. Looking at the SSR demo it seems like the expected pattern is to hydrate the entire document on the client side and not just the app root. I haven't tried this myself, but maybe it could work?

https://github.com/facebook/react/blob/17806594cc28284fe195f918e8d77de3516848ec/fixtures/ssr2/src/index.js#L12

That said, it looks like they are wrapping the app root in a data provider on the server only:

https://github.com/facebook/react/blob/17806594cc28284fe195f918e8d77de3516848ec/fixtures/ssr2/server/render.js#L41-L43

So, I'm guessing they will have hydration mismatch errors if anything in <App /> uses useId. I haven't tested it myself though.

theKashey commented 2 years ago

It's actually pretty simple thing - useId is affected only by sibling presence in the parent components.

Let's assume we have App using useId

<App/>
// is the same as 
<Provider>
 <AnotherProvider>
  <App/>
 </AnotherProvider>
</Provider>

because there is no "difference in sibling" in parents. And the change in parents does not matter.

Given another example

<>
 <Stuff/>
 <App/>
</>
// is the same as
<>
 <Stuff>
   <SomeOtherStuff/>
 </Stuff>
 <App/>
</>

Because there is change, but not among "siblings in parents" - change is in "another tree branch", which will have issues using useId

gaearon commented 2 years ago

That said, it looks like they are wrapping the app root in a data provider on the server only

I'd say that's a mistake in the example.

On the server side, I need to wrap my app with a shell like this

The canonical solutions are either to add this wrapper to the client (and hydrate the whole document) or not use React to generate this wrapper on the server either.

dominic-p commented 2 years ago

@theKashey thanks for the clarification. I didn't realize that it had to do with the siblings.

@gaearon thanks for information on the suggested solutions. I'll have to rethink quite a bit about how I approach SSR, but at least now I know what I should be aiming for.

brophdawg11 commented 1 year ago

Ah - this is exactly what I was battling yesterday! I think this thread explains the behavior I was seeing and maybe there's room for a small docs update or clarity. I was confused by what I read in the docs versus the behavior I was seeing. The current docs don't mention how useId generates it's value, but the beta docs mention:

Inside React, useId is generated from the “parent path” of the calling component. This is why, if the client and the server tree are the same, the “parent path” will match up regardless of rendering order.

If it really does come down to both the parents and their siblings then I would suggest updating the docs to be a bit clearer. It currently reads to me like only the path to the current component matters, not the siblings along that path.

Here's a simplified example showing the parent sibling impact on useId: https://stackblitz.com/edit/node-afgxnd