Replies: 20 comments 67 replies
-
Thanks for summing this up! Remix is advertising that users "own the entry points" and I love this! By better supporting render/hydration entries other then A potential way to achieve this would be to officially expose data used by |
Beta Was this translation helpful? Give feedback.
-
Thank you for drafting this proposal @clgeoio 🙌🏼 FYI, I added a |
Beta Was this translation helpful? Give feedback.
-
This isn't just a problem of in-app browsers/extensions or React 18 exclusively. This also relates to any external scripts which inject content to the page. For example — in the current privacy landscape, almost all sites implement some kind of cookie consent script. And I'm currently observing this problem when implementing Quantcast cookie consent on React 17. This script needs to be in head so that the consent api is available to any other resources which depend on it, which means it runs before the hydration. It also inserts the consent dialog to the body as soon as it's loaded. But then when hydration runs the consent dialog gets overridden with client hydration. The best workaround (apart from modifying and maintaining the script itself) is to load it in |
Beta Was this translation helpful? Give feedback.
-
I've got a solution for the hydration issues entirely in userland. |
Beta Was this translation helpful? Give feedback.
-
I took the approach from @kiliman and streamlined it into |
Beta Was this translation helpful? Give feedback.
-
This was discussed in Roadmap #5, around 49'32 @mjackson to respond to this proposal. |
Beta Was this translation helpful? Give feedback.
-
This issue is also present on the official Shopify site. Install the Grammarly extension, throttle your CPU (if needed) and you'll see hydration errors in the console. |
Beta Was this translation helpful? Give feedback.
-
https://twitter.com/joshcstory/status/1622748680305639429
Looks like the React Team at Vercel are working on this. |
Beta Was this translation helpful? Give feedback.
-
Can you folks post what different browser extensions are doing to the DOM that cause hydration errors? Like the "before DOM" and "after DOM" of the browser extension so we can get some good test cases to the React team? |
Beta Was this translation helpful? Give feedback.
-
Here's an examples of before and after that I've rustled together with some extensions that I know are troublesome. For the "Before DOM" I've disabled Javascript and set the throttling of my browser to be very slow.
I've tested Checkmate Savings - Manifest 3, Grammarly - Manifest 2 and LastPass - Manifest 2 An example of a very basic Remix page with Checkmate active.
After DOM:
|
Beta Was this translation helpful? Give feedback.
-
Just to add to the use case of browser extensions, we're maintaining a micro frontend architecture where we keep a separate system for e.g. the header, the footer, but also for some shared related things like e.g. shared css. We use a central NGINX and "virtual includes" to stitch the services together, so we have to render a tag like this in |
Beta Was this translation helpful? Give feedback.
-
It's pretty difficult to predict changes in head since they can be created from any part of the system (3rd parties injecting things like cookie consent/legal, users browser extensions, more and more 3rd parties xD). The race condition between hydration and head changes is tricky. Deferring 3rd parties to after hydration can be a solution (and probably the best one for some cases), but does not solve the browser extensions issue. At the same time deferring required 3rd parties will behave like waterfall. Exporting I believe that render I agree that this is a react issue but it impacts a lot any system using remix. |
Beta Was this translation helpful? Give feedback.
-
Looks like its partially fixed in React 18.3.0-next-fccf3a9fb-20230213 (at least for styled-components) |
Beta Was this translation helpful? Give feedback.
-
For anyone struggling to get this working with MUI/Chakra or other Emotion-based component frameworks, you need to pull the styles out of the entire rendered app, then inject it separately from the rest of the header. The only thing I haven't figured out is how to remove the server rendered header on the client side (i.e. this step). The interaction between attempting to remove the server-rendered header and the useEffect code in Head() (which also is trying to flush and inject style tags into the header) is giving me an error. I just live with the duplicated server-rendered header for now without any real issues. Resources I followed to get my Chakra setup:
UPDATE: see this comment for a solution that works with React 18 renderToPipeableStream() // entry.server.tsx
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
): Response {
const cache = createEmotionCache();
const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);
const html = renderToString(
<CacheProvider value={cache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
);
const emotionChunks = extractCriticalToChunks(html);
const emotionCss = constructStyleTagsFromChunks(emotionChunks);
// swap out default component with <Head>
const defaultRoot = remixContext.routeModules.root;
remixContext.routeModules.root = {
...defaultRoot,
default: Head,
};
const head = renderToString(<RemixServer context={remixContext} url={request.url} />);
// restore the default root component
remixContext.routeModules.root = defaultRoot;
const body = renderToString(
<CacheProvider value={cache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
);
responseHeaders.set("Content-Type", "text/html");
return new Response(
`<!DOCTYPE html><html lang="en"><head><!--start head-->${head}<!--end head-->${emotionCss}</head><body><div id="root">${body}</div></body></html>`,
{
status: responseStatusCode,
headers: responseHeaders,
}
);
} // entry.client.tsx
interface ClientCacheProviderProps {
children: React.ReactNode;
}
function ClientCacheProvider({ children }: ClientCacheProviderProps): JSX.Element {
const [cache, setCache] = useState(defaultCache);
function reset(): void {
setCache(createEmotionCache());
}
return (
<ClientStyleContext.Provider value={{ reset }}>
<CacheProvider value={cache}>{children}</CacheProvider>
</ClientStyleContext.Provider>
);
}
function hydrate(): void {
startTransition(() => {
hydrateRoot(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Part of hack for hydrate error https://github.com/kiliman/remix-hydration-fix
document.getElementById("root")!,
<StrictMode>
<ClientCacheProvider>
<RemixBrowser />
</ClientCacheProvider>
</StrictMode>
);
});
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Guard for Safari
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1);
} // root.tsx
export const Head = withEmotionCache((_props, emotionCache) => {
const clientStyleData = useContext(ClientStyleContext);
// Only executed on client
useEffect(() => {
// re-link sheet container
emotionCache.sheet.container = document.head;
// re-inject tags
const tags = emotionCache.sheet.tags;
emotionCache.sheet.flush();
tags.forEach((tag) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any -- Copied from https://chakra-ui.com/getting-started/remix-guide
(emotionCache.sheet as any)._insertTag(tag);
});
// reset cache to reapply global styles
clientStyleData?.reset();
// eslint-disable-next-line react-hooks/exhaustive-deps -- Copied from https://chakra-ui.com/getting-started/remix-guide#2-provider-setup
}, []);
return (
<>
<Meta />
<Links />
</>
);
});
interface DocumentProps {
children: JSX.Element;
}
function Document({ children }: DocumentProps): JSX.Element {
const location = useLocation();
useEffect(() => {
track("Page View", { path: location.pathname });
}, [location]);
return (
<>
<ClientOnly>{(): ReactPortal => createPortal(<Head />, document.head)}</ClientOnly>
<ChakraProvider>{children}</ChakraProvider>
<ScrollRestoration />
<Scripts />
<LiveReload />
</>
);
}
function Root(): JSX.Element {
return (
<Document>
<Outlet />
</Document>
);
} |
Beta Was this translation helpful? Give feedback.
-
facebook/react#24430 (comment) |
Beta Was this translation helpful? Give feedback.
-
According to @gaearon's message in facebook/react#24430 (comment), we shouldn't hydrate |
Beta Was this translation helpful? Give feedback.
-
For those who encounter this problem, I advise you to install the canary versions of react and react-dom. Canary version in my case solves all hydration problems
|
Beta Was this translation helpful? Give feedback.
-
Hey all, I am going to close this discussion for now - it seems that the React team are well aware of the issue and are working toward a suitable solution. |
Beta Was this translation helpful? Give feedback.
-
Guys, don't upgrade to canary version. it will hurt the page performance. |
Beta Was this translation helpful? Give feedback.
-
@WesleyYue
Do you have any clue what might be causing this?
|
Beta Was this translation helpful? Give feedback.
-
There's been a few issues posted that relate to browser extensions breaking the React 18
hydrateRoot
because Remix hydrates the whole document, rather than a div. This issue is discussed in the Roadmap Planning 3.This has been considered more a React problem than Remix, however, this issue affects more than just desktop browsers with extensions enabled and allowing users to solve this issue will stop the buck from being passed around.
I'm not sure if React will close this issue, but we shall see!
The issue impacts Remix sites that are loaded in a non-standard browser, such as TikTok's in-app browser as it injects custom information on page load. This opens the issue surface up widely as now it's not only desktop users, but mobile users too. Apps with in-app browsers include Facebook, Instagram and Slack.
From iOS 15 and onwards, users can install extensions on iOS Safari, although not as common as desktop extensions at the moment it does open the problem space to more iOS devices that are not using in-app browsers.
Although React falls back to doing a client-side render, packages like styled-components may not recover as easily and can result in obvious styling or behavioural issues. There have also been issues with defer'd promises never resolving.
A workaround that has been posted is to downgrade to React 17, who's
hydrate
method seems to be clear of this issue. Doing so is fine, but it means that developers won't be able to usedefer
or any of the new React 18 tools.So, with that in mind my proposal is somewhat similar to the suggestion made in the Roadmap Planning video where Remix could be hydrated in two parts.
Hopefully it's not too fiddly to deal with when considering streaming.
I took a stab at implementing something like this, but it lacks the two
hydrateRoot
s and as such<Meta>
and<Links>
don't really do anything on client navigations.Relevant hydration issues:
0, 1, 2, 3, 4, 5
Beta Was this translation helpful? Give feedback.
All reactions