Library Upgrade Guide: <head> (e.g. react-helmet) #106
Replies: 2 comments 2 replies
-
One compromise that's good if you want to avoid UA detection:
That way the head is always included in the first React shell but you can stream the rest of the page. |
Beta Was this translation helpful? Give feedback.
-
@sebmarkbage I've been thinking about this a bit lately. This approach might actually make it difficult to migrate to from the existing approach that is explicitly recommended in this post (i.e. waiting until Instead of explicit support for passing in a default As an alternative, allowing that default head value that is provided up front to be a promise that can run concurrently with the rendering of the document would also be acceptable, I think. |
Beta Was this translation helpful? Give feedback.
-
Library Upgrade Guide:
<head>
This is an upgrade guide for libraries like react-helmet, next/head etc. I.e. libraries that collect tags like
<meta>
,<title>
,<link rel="apple-touch-icon">
and<link rel="canonical">
by rendering them in components and then extracting them to the head.I don't include inserting
<link rel="stylesheet">
,<style>
or<script>
into the head for this use case. There are follow up guides for<link ref="stylesheet" />
,<style>
and<script>
loading. Our recommendation is that<head>
libraries don't support this use case and instead direct this problem space to CSS and script specific solutions. You can support ad-hoc inserting random scripts/css this way but not as a general purpose solution. This is mostly already the case.The use case for
<head>
is a 1:1 correspondence with the URL. It's meta data for decorating the URL - for a social media preview, for SEO, for showing a bookmark, or for display in the current browser tab title.Injecting Into the SSR Stream
Typically this technique is done by collecting data while rendering and then combining the result at the end like:
In React 18, renderToString is discouraged and instead the new stream API is preferred which doesn't work with this technique.
Instead, we recommend amending the stream before React writes its shell content.
This ensures that React can start emitting preload tags into the head early, and once it has rendered its shell, you can collect the head and emit it before React writes the rest of the HTML.
For a Web Stream, you can use an intermediate custom ReadableStream. Something like this:
or maybe a TransformStream:
You can only emit what React has rendered so far. However, if something suspended, React might not have rendered the whole tree yet. React will still start emitting HTML for the partial HTML. In that case, the
<head>
you emit at onCompleteShell might not be the final.When to Start the Stream
Unfortunately, most specs specify that
<head>
tags requires them to be first in the HTML stream before any visual content. This is bad since it requires compromising on display performance of the main content, by putting content that's invisible first. Hopefully we can one day ensure that all implementations respect title and meta tags all the way through the document (which most do).Similarly, for SEO purposes it might be important to accurately reflect a 301 or 500 HTTP status code.
You can choose to hold React back from streaming if you want and start streaming later. In general we don't recommend this for best performance, but it can be useful if the User-Agent is a known bot that doesn't support JavaScript. We recommend looking at th user agent header and if it's a bot delay the stream until the
onAllReady
callback. That ensures that you have the full HTML ready all at once - including any overrides to the<head>
.In this case React can't stream preload tags early but that's because if you want to reliably set the status code, you have to wait until you're sure you won't error anymore.
For non-bots we recommend just leaving the
<head>
in whatever partial state was reached when React started rendering. That way you get a best effort and depending on how fast things load and whether you have any overrides - it might be the final. If it's not, then let hydration patch it up.It might be tempting to emit scripts that update the
<head>
into the stream as new heads are discovered. However, that gets complicated since the client might start hydrating and navigating away before the stream is complete. Therefore, we recommend not doing this and instead just always override it when the client component mounts.Another technique is to wait for the first
<Head>
before starting streaming. That way if the first<Head>
is deep in the tree, you won't get the benefit of streaming up until that point but at least the rest of the page is streaming. This is not necessarily best for SEO purposes since it still uses JS for streaming, but can be used as a compromise if you want to support unknown User Agents while compromising on end-user performance.Selective Hydration
On the client React 18 will partially hydrate at the Suspense boundary level. This can cause an interesting effect if your system attempts to merge multiple heads and supports conflict resolution. E.g. if you have a parent provide defaults and a child can override it.
Imagine a structure like this:
React will first hydrate the outer
<title>Site</title>
and triggers its life-cycles/effects. Then later it'll come back around to hydrate the inner content:<title>Site - Page</title>
.We recommend that your library always override whatever was in the original HTML when hydrating for this use case.
However, that can lead to a quirky effect in this scenario. If the server had rendered
<title>Site - Page</title>
then the title can temporarily update toSite
and then later back toSite - Page
while hydrating.To avoid this we recommend just having a single
<Head>
or putting them all in the same Suspense boundary.Preload tags
In the future we'd like to enable React to emit
<link rel="preload">
tags as soon as possible into the stream. As long as you don't need to set the HTTP status code, these can be emitted even before the other head tags as discovered. As a result, it's not necessarily a good idea to delay the whole stream unless the status code is important.In particular, for a logged-in user that can't be a bot or a UA that's not a bot, we recommend starting the stream as soon as possible by calling pipe() immediately.
Future
In a future minor we'd like to add built-in support for
<head>
directly in React. That way you can easily render the whole document using React and avoid a lot of the extra plumbing.However, the best idea we have right now is that it would not merge the content (unlike react-helmet and next/head). Instead it'd be the last
<head>
that wins but we'd recommend only having one on the page at any given time. Instead defaults can be provided through Context from above. We'd also have the ability to specify a default-head at the root that can be used as a fallback if the stream starts early - e.g. to set a default title.Another change is that it would not reset the
<head>
when it's unmounted. Instead it would leave it on the old value. That way if you're navigating between two pages, and the new<head>
doesn't immediately load (e.g. it's suspended), then it doesn't temporarily flash the old value. It is expected that this API would be used together with a router to ensure that you pair one<head>
to one route.This would be backwards compatible since if you SSR
<head>
today it would keep working pretty much the same, and you already can't use it on the client unless you're hydrating SSR:ed content.Beta Was this translation helpful? Give feedback.
All reactions