Using React 19 Metadata handling with React Router

Today we go over how to use React 19 to improve your websites SEO and add metadata to your page. We go over the pitfalls of React 19 and how you can create custom code to handle these pitfalls in react-router.
Transcript
00:00 So with the release of React 19 we got support for document metadata baked in right into React 19. So why does that matter to us? Well there are multiple ways to use metadata in React router right now and today I'm gonna show you how you can use React 19 directly to shape your metadata and render it properly. And how I'm gonna do that is I'm gonna show you the shortcomings of React 19's metadata and how we can avoid it. So here I have my React Hour app running and I've already created a route called product slash one.
00:39 So this route renders the product page and as you can see there's the product route and the product details route. So one of them is slash product, one of them is slash one. And if I click here, I'm going to go back to the original route. If I click here, I'll go back to the details page and the styling is not important for this part. So we're just going to skip over that here in the head.
01:03 If I look at it, you can see that I have one two three four five titles so I have five titles on my page and I have one two three descriptions. So what's going on? Well if I check out the code and if I open my root route, you can see in the root I have base stack as the title and meta as the description. So this is Automatically hoisted by react-19 into the head. But the issue with react-19's hoisting is it just hoists it.
01:35 It doesn't deduplicate it, it doesn't do anything to it. So our product page has the title product page and the meta description that it sets and then we have our product ID where it says the product details and product details routes description and why is this problematic because we end up having like five titles on the page you end up having multiple descriptions and that's not what we want we want a single title a single description and maybe some extra power later on. So let me show you how to do that. So the first thing we need to do is we need to go to our root route. So here what we want to do is we want to construct the metadata somewhere and you have three options here handle meta function and your loaders so my personal preference is to handle this in the meta function and let react auto do it itself but if you want to use react 19 this is how to do it so because handle doesn't get loader data we can't use that so handle is out of the picture you don't want to use meta so the only place we want to do that is the loader So in the loader we can return something like SEO and what we're gonna do here is I've already prepared a helper called create SEO and I've imported this from util slash meta and this expects an SEO return type.
03:03 And what we're gonna do is we're gonna check this out. So if I go to the implementation, they just returns this type, and this type is an optional prefix and a descriptor array. And this is using the React.Java's meta descriptor. So this is provided by React.Java. For now, what you need to understand is when you call create SEO it just gives you type safe way to create your SEO and We are going to use the descriptor fields to create our SEO.
03:33 And what's important here to note is that the meta descriptor array is returned by the meta function as well. So that's what we are doing here. So if I go back to the root here, what we can do now is say descriptors and then title is my demo or rather let's call it home page we need a description so we're gonna do name is description and content is my default description. Alright so let's save this so now if I go here in my dev tools and I close the Chrome dev tools I can see that I'm returning the SEO descriptors from my root loader. All right, perfect.
04:24 Now let's go back to the actual code. All right, so we are returning these from the loader. Now, what we need to do is instead of rendering this we need to render the actual stuff returned by the loader so let's do that right now so const seo is equal to loader data dot SEO and then what we're gonna do is we're gonna delete this and then we're gonna say SEO descriptors dot map so descriptor and then we're gonna do the following if the title is in the descriptor we're going to return a title and that title is gonna be descriptor.title so now we're just narrowing the types and also I forgot the key here so the key is the scripter.title and also here otherwise we return meta we spread the descriptor and we also set the key to be the scripter.name all right and this is now complaining So what we're gonna do is we're gonna say if char set in the scripter return meta char set is the scripter char set and we're gonna close that and we're gonna give it the key of descriptor.charset and then let's continue narrowing so name in descriptor we're gonna return meta descriptor and key is equal to the scripter dot name property and the scripter return meta descriptor key is the scripter dot property otherwise we're gonna say that the descriptor key is the index because we don't want to handle the other cases.
06:25 Or you can narrow it down further as you go. For this demo, I don't want to bore you with these details. So I'm just gonna set it as index for now. So now if I save it and we check our head you can see that the description is set and the title is also set. So perfect now we're rendering our SEO return from the loader instead of hard-coding it in the code.
06:48 So what do we do next? So now we want to use either the child SEO or the parent SEO depending on which one is available and the child one takes precedence. What we are gonna do here is we're gonna create a state, so seo and setseo, and by default the use state is gonna take in lowerdata.seo. Alright, so now we can remove this, and if I save this this is gonna work again but we're not using the set the SEO so what we want to do is we're gonna use effect so whenever something changes and what we're gonna do here is we're gonna bring in matches and we're gonna pop them in the dependency array and what we want to do is whenever the matches change we want to rebuild the SEO. So how are we gonna do that?
07:44 Well the first thing we need to do is we need to find the the match of the leaf routes in the last place in the chain. So how do we do that? So we're gonna do the following so match is equal to matches dot reverse because we want to go from the child upwards and then dot find and I've already prepared a find function and it's called the find match with SEO and if I hover over this you can see that it returns a match with the SEO already set. So now what we can do is, now that we have the match, if match, So if it's found, now this type constrains it even further. What we want to do is, we want to set the SEO and the value here will be match.loaderdata.seo.
08:45 All right, and we save this. And we want to type this as well. So this will be SEO return. And we also want to type constrain the lower data to be defined. And we can just change this to be this.
08:58 So now, and we can also change this to be this. So what are we doing here? We are setting the bottom most SEO of the current routes as the SEO of the page. So if our product slash product id is setting the SEO we're gonna use that, if the product sets it we're gonna use that, if nothing sets it we're gonna use the default root one. So that's what we are doing here.
09:26 And if I save this, and now I go to the head, you can see that this is still being set. And why is that? Because we have not created the SEO for our Leaf routes, so let's do that right now and see if this works. Alright, so if I go to the product route We have our loader here, but we are not returning anything here. So let's add our SEO by creating create SEO.
09:53 And here we can say the descriptors are so the title and that's products page. And then we want the description. And that's going to be another object. So name is description. And then content is products page description.
10:16 And we are going to save this. And then we're going to go to products product ID, and here again, we're gonna say SEO, create SEO, and here we're gonna say the descriptors are, the title is products details page, and then our description description content my products page details and we save this Now if I refresh this and we go to the head we can see that there are three product pages title and also the description matches what it should be. And as you can see this is set in the root for the product page. So this means this works, but there's a small issue of re-rendering the double titles. Well, that's an easy solve.
11:11 All we have to do is delete it from here and go here and delete it from here and we save this and now if I refresh this you can see that the title is set to the products page and the description is set to the products page description and One last thing that's left to do is, instead of using the loader data as default, we're gonna do matches.reverse.find, find match with SEO, otherwise.loaderdata.seo, otherwise loaderdata.seo. And this is gonna make sure that when we refresh the page and if I look at the title here, so if I refresh this, as you can see when I refresh this the products page is the default title so if you refresh the page this works as well and there you have it we have successfully implemented this but as you can see the root is complaining here and we have a bunch of type issues here and we just need to further constrain the type and I would suggest extracting this into a component of its own but what's important to do here is say type of descriptor title is equal to string and basically add this everywhere where it's complaining so instead of here do char set instead of here do name here do property and that's it Now this works again without any typescript issues And we have fully converted to using React 19 to handle our SEO So this is a great way for you to use React 19 if you want But as you can see, it's a lot of manual work So all of this is handled by React 4 out of the box and you can even merge headers from parents easily and do stuff like that.
13:11 But if you want to add custom behavior what you could do is for example In the create SEO use other fields that are not descriptors so for example I already set the prefix to be available and then I say prefix is My app and I'm gonna do this and now if I go here what I can do is I can say that we are setting this and we want the descriptors to be SEO descriptors and we want the descriptors to be the descriptors and then map I'm gonna do D here and if title in D what I want to return is title matches 0.loaderdata.testco.prefix so this will be defined And then we want to append this to the title. So like this, and then we can, and because we know this is defined, we can just do this. Otherwise return the descriptor. And now if I refresh and go here and this is not working, it's undefined. Ah, so the issue is that I'm using reverse instead of toReversed.
14:29 But if I save this now, now it works. So my app prefix is added to every route in our project. So for example, if I go to something else, it changes to my app product details page. I go back to the original route, it changes. So now we have a global prefix working in our app and this is how you add custom behaviors and you can do a lot of other ones but that's really up to you so this is the best way to do react 19 plus reactor metadata And I'm just gonna remove this because it's kind of annoying me and we can just do this.
15:08 And now if you save this, we have a fully working app with React 19 metadata. And that's it. I hope you enjoyed this one. If you're interested in more tips like this, I'm dropping a React Router workshop on November 6th and it's going to be a live event where we code together and I teach you a lot of stuff about React Router and that's going to become a self-paced workshop later on. So if you're interested in that, check out the workshop and feel free to sign up.
15:39 That's it. Thank you for watching and see you in the next one. Bye.
- Play React Server Components with Vite and React-Router
React Server Components with Vite and React-Router
- Play The future of react-router just got a lot brighter
The future of react-router just got a lot brighter
- Play Let's talk about the future of Remix and react-router
Let's talk about the future of Remix and react-router
- Play Understanding the cause of hydration issues in react-router
Understanding the cause of hydration issues in react-router
- Play Server Components (RSC) in react-router are... actually good?
Server Components (RSC) in react-router are... actually good?
- Play Debug React Router Applications with Custom Logs using react-router-devtools
Debug React Router Applications with Custom Logs using react-router-devtools