React Router 6.4 - Getting Started

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
In this video, we're going to take a look at React Router version 6.4. And that's, of course, just a minor new version, so why a separate video? Well, because this minor new version introduces many potentially groundbreaking new features that vastly simplify how you build and write your React apps. To be precise, React Router 6.4 gives you some key features that simplify data fetching and data submission in your React apps. And in this video, you will learn how these new features work and how you can use them and of course, you don't have to use them. You can stick to React Router version 6 to the syntax you know. You can even stick to React Router version 5. You wouldn't be alone if you take a look at the download numbers but I would argue that these new features offered by React Router version 6.4 will be well worth your time. So let's take a look at them. Now, to understand these new features added by React Router version 6.4, I created a brand new demo project. I created it with Vite, which is kind of an alternative to Create React App but that's not too important. It is a standard React app in the end with a couple of components, a couple of components that are used as pages with help of routing. And my routes are defined in just the way you know from React Router version 6. And that's important. This starting project uses React Router version 6.3. So it uses version 6 but not version 6.4. So this is a React app, as you probably know it, and in some of these route components, we're also fetching data, for example. And data fetching typically means that we use useEffect to initialize our request, to send the request, to manage some loading state and potentially some error state. And that, of course, is then also reflected in the UI. And of course, we also might have some data submission in our app, which means we have a form here nested in the NewPostForm component where we then, in the end, prevent the default, manually extract some data, and then also manage some submission state, send a request here with help of some utility functions that are defined in a separate file where I send a default HTTP request with the fetch API. And ultimately, we also manage some error state here, and we might navigate the user away, redirect the user if we're done sending the request or show some error if we had an error. That's all not too fancy. That's all code that should kind of look familiar to you. Now, this approach of handling data submission and data fetching is pretty standard but it, of course, also isn't too easy. It's quite a bit of code you have to write, quite a bit of complexity you have to manage on your own, and especially if your apps get more complex, your code will also get more complex. In addition, of course, a disadvantage of this approach is that we only start fetching data after this component was loaded, instead of right when the navigation to this component starts. And we have to manage the loading state and the error state manually. And that all can be a bit annoying and add a lot of unnecessary complexity. Now, things get much simpler if we install React Router version 6.4 because if we do so, we unlock a bunch of new features that simplify data fetching and submission. And we can start with data fetching. When using React Router version 6.4, we can get rid of all that code here. We can get rid of this code here as well. And get rid of this conditional check here. Our component can be simplified a lot as you can see. So this is the new BlogPostsPage component. But of course, right now, we're not really fetching any data in there. We're not doing anything here. Instead, we're using some posts, which don't exist. This posts thing here doesn't exist anywhere. But we can now add a new function to this component file. We can add a new function, which we can call loader. And I'm saying can because the name is up to you. What's important though is that you export this function because we'll need it in some other place soon. And in this function, we can then return some data that should be available in this component function. That could be an array or an object or some text or a number but it can also be a promise that eventually resolves to some data that should be available here. For example, it could be the result of calling getPosts and getPosts is this function, which I'm importing from api.js. And it's simply a function that generates and sends an HTTP request to fetch some posts, checks if we did fetch those posts successfully. If not, it throws a custom error object and otherwise, it returns a promise that eventually resolves to the JSON data that's a part of the response. So this promise returned by getPosts is, in the end, what's now returned by this loader function. Now, this alone doesn't do anything but this loader function can now be registered on our route definition. On the route that is responsible for this component that should load data, so in this case, this route here, we can add a brand new loader prop. This prop is added by React Router version 6.4. It did not exist before that version. Now, here we now import this loader function, which I export here in BlogPosts.jsx. We import that from this BlogPostsPage and here I'll also assign an alias to it, though that's not required but I'll do it here because we'll have multiple loader functions soon. So here I have the blogPostsLoader imported from this BlogPosts file and it's this blogPostsLoader, which I assign as a value to this loader prop. So in the end, this exported function here is now assigned to this loader prop. And React Router will automatically call this function whenever we navigate to this route. And it will automatically get the data returned by this loader function and make it available in this function component here. To get hold of that automatically made available data, we just have to use a special hook, the useLoaderData hook, which is provided by react-router-dom. This hook gives us access to the loaderData. And that is then simply the data returned by the loader function. If the loader function returns a promise, it's the data to which the promise resolves to. So in my case here, it's in the end the data returned by this response from this request here. So it will be a list of dummy posts returned from that dummy API. And that's exactly what I can pass as a value to the blogPosts prop here because that was what was passed to the blogPosts prop before before using this loader function as well. I just had to manage the whole data fetching and storage process manually back then. So now we're passing loader data to blogPosts and we register the loader. Now there's just one missing step because this new feature is not available when setting up our routes like this. Instead, you can't use BrowserRouter anymore. You can only use that if you're not using these new React Router version 6.4 features. If you do want to use them, you have to use the RouterProvider component instead. This is a component, which you also create here but which is a self-closing component. You don't put any components between its opening or closing tags. Instead, it takes a router prop and to this router prop, you pass a router, which is created with help of the createBrowserRouter function. So here we can create a router outside of the component function. And this router can now be created with help of a couple of JavaScript route definitions. You could pass an array to it and then every object in that array would represent one route. You can add a path property to every route definition and the element property to every route definition like this. But alternatively, you can also define your routes as you did it down there by passing another function to createBrowserRouter and that is the createRoutesFromElements function here. You call this function here and to this createRoutesFromElements function, you pass your route definitions like that. Now we can get rid of that code down there and I'll get back to the route layout in just a second because actually when setting up your routes like this, you must not use the routes element, the routes component here. Instead that must also be a route, singular, and it's simply a parent route that contains all the other routes as child routes. And that's a good thing because this parent route now should actually render the layout that should contain the other routes. So in my case, the RootLayout, like this. And now it's this router that's passed to the router prop of RouterProvider. Now, this root route should also have the root path and the route that had that path previously is now the index route. And index routes are simply the default routes that will be rendered if the parent route path is activated. So if a user visits just our domain slash nothing, this route would be activated. If a user visits /blog, this route would be activated because it's the index route of this route, which does listen to /blog. Now, the concept of nested routes and index routes was already introduced with React Router version 6 and I did, therefore explain child routes in my video I created back then when I talked about updating from React Router version 5 to React Router version 6. Now, in order for RootLayout to support child routes, we should go to RootLayout and we can move it to the pages folder if we want to but more importantly, we should get rid of the children part here and instead use the special Outlet component provided by react-router-dom, which simply marks the place where all those nested child components should be rendered to. So all the components defined inside of this root route, so all these components defined inside of this root routes will be rendered in this place. That's what you define with help of Outlet. And with all of that, by switching to this new router and this new RouterProvider, we unlock these new React Router version 6.4 features and we can use this loader feature. And therefore, with all that done, if we start the development server again, and we visit this page here, you see that if you go to Blog, it loads this page and the blog posts are there. But now they're fetched and made available to this page component automatically behind the scenes by React Router. And you don't have to manually initialize the data fetching. You don't have to use useEffect. You don't have to manage any loading state because the page component is only rendered after the data is there. And therefore, you have much less code to write. By the way, if you still wanna visit the page before the data is there, React Router version 6.4 also offers a solution for that and I'll get back to that later. So that's how you can fetch data with loaders. You can also fetch data with loaders if you have a dynamic path parameter. Like here for the PostDetail page component. This route here has a dynamic path parameter, the ID parameter and of course, we wanna load the specific post for that ID when this route is visited. To make this happen in PostDetail, we can still get rid of all that code here, get rid of that code here and get rid of that code here and then get rid of these unnecessary imports up here. And instead, we can also export a loader function here. By the way, the name is up to you. This is just a convention but the name is totally up to you. And in this loader function, I wanna call getPost, which is another function imported from the api.js file, which actually needs the ID of the post, which I wanna fetch or for which I wanna fetch data. And I can get this with help of some data that's passed automatically to this loader function by React Router because React Router automatically generates and provides an object to this loader function. And this object gets a request object, which contains some data about the page transition but it also gets a params object. And this params object gives me access to my post ID by accessing .id on it. Now, it's .id here because my dynamic path parameter is called ID. With that, I got the postId. I can pass this to getPost and then again return the data returned by getPost, which in this case is another promise so that the data this promise resolves to is made available to the PostDetail page. In there, we can therefore again use the loader data to get our postData, like this. And now we got our postData.title and postData.body, which we're fetching here. With that done, we can go back to our route definitions and here we add the loader prop to the route definition of that route where we do want to fetch the details about a post. And we import that loader function which we export in that PostDetail file. We could give it another alias here like blogPostLoader and assign that as a value to this loader prop down there. And with that, we can visit a single post and view its details here. So that's how we can work with loaders. Now, we don't need to worry about the loading state because the page is only loaded after the data was fetched behind the scenes as I mentioned but what about the error state? Because before I did perform some error handling. Well, we can also do that when using this new approach. We can just do it in a more elegant way. Instead of manually tracking some error state, we can add an errorElement prop to our routes where we are performing some data fetching. And just as we have the regularElement prop, which defines the regular component that should be rendered if everything goes well, we can also define a component or some JSX code that should be rendered if something goes wrong. Like this paragraph where we say an Error occurred! If I now break my code here, and I send the request to a URL that doesn't exist, you will see that I get this fallback output here. So errorElement can be used to control what should be shown if an error occurs whilst loading data. And you don't have to add errorElement to the route where you're doing the loading. You can add it to any parent route as well, and it's then always that parent route that will be replaced by the errorElement. Therefore, you can also add it on the root component here, on the root route, the RootLayout route in my case here. And for example, add some error page component to your pages folder where you're in the end showing the MainNavigation and then some error message here. And use that error page component as the fallback component if an error occurs anywhere in your route definitions. Because errors will bubble up through your route definitions and the first route with an errorElement they reach will then render its errorElement. So that's how we could render this error page if things go wrong. If I break this route here again, you see the content of this error page, which is this an error occurred title in my case. And you can also access any additional error data that might be provided. In my case here, for example, I have an extra message, which I might want to output. This can be done by using a special hook provided by React Router. The useRouteError hook. Just as we had the userLoaderData hook for accessing the data that was fetched by the loader, the useRouteError hook can be used for accessing the error that was thrown. So in my case here, I'm, for example, throwing this error object. It's therefore this error object, which is made available by React Router through the userRouteError hook. And therefore, I could then, for example, also output error.message. So dive into that object and output the message. Therefore after doing that, you would also see this message here. Of course, with that, I'll fix my route but that's how error handling works with that. Now, besides data fetching and error handling, we also, of course, often submit data, like here in NewPost.jsx, and I do this by listening to my form submission in the NewPostForm component, triggering the onSubmit function, which is received through a prop in this case here. And then in there, I prevent the default because by default, the browser which generate an HTTP request and send it to some fictional backend and I then extract data manually and I handle errors and submission status manually and all these things. Now, we can also simplify that and get rid of all these parts here. Get rid of that and get rid of the error and isSubmitting states. Get rid of useState up here. And here for NewPostForm, I no longer pass onSubmit to NewPostForm because in the NewPostForm component, I no longer wanna handle form submission manually. Instead, React Router version 6.4 also helps with that. And it does help with that by giving you a brand new form component, which you should use if you do want to handle form submission with React Router. This Form component is imported from react-router-dom and starts with a capital F. And you should wrap your form fields with that component if you do want React Router support. On that Form component, you add a method, which can be set to get that post but also to patch, put and delete. In my case here, I'll use post. And you can define a action. This defines the path to which a request will be sent. And in my case here, I'll use the path of this route here, /blog/new. So that's the route to which I wanna send a request. But when I say send request, it's important to note that actually, no request will be sent anywhere. This is still client-side code. React Router is still a client-side library. Instead, what React Router will do is it will generate a request object that, for example, contains all the form data but it will not actually send the request to any backend. Instead, it will send the request to an action function defined by you in which you then can forward it to a backend or do whatever you wanna do with it. And that's therefore what I now add to the NewPost.jsx file, which is the file that contains my NewPostForm. So that component that holds the form on which I just worked. By the way, we should also get rid of this error handling code here because we're not handling errors manually here anymore. Now, in this NewPost component, we can now export another new function, and this time, I'll name it action. The name is again up to you but I'm not naming it loader because this is now not about loading data, but instead about performing an action after a form was submitted. This action function here should be registered as an action in your route definitions. And you should register it on the route to which you're sending this request so to say with help of this Form component. So in my case, I'm targeting /blog/new. So it's that route here in my route definitions on which I add another brand new prop, the action prop because it's this action function at which I'll point here, which will be executed whenever a form is submitted and targets this route's path. So here I will import this action function we just created in the NewPost component file. newPostAction would be a fitting name. And I'll assign this as a value to this action prop. With that, this action function will be invoked whenever this form is submitted since this form targets that route to which I just added this action. Therefore, this action function here will be invoked whenever the form is submitted and here we also get an object, an automatically generated object with some data. We again would get some params here if we needed them but we also get a request object. And this time, the request object is important because this automatically generated request object, which hasn't left the browser yet, that's really important, we're still in the browser but this automatically generated request object will actually contain the data that was submitted with the form. And you will be able to extract data by using the names assigned to the form inputs. So the names matter. You don't have to manually extract data from the form with help of refs or two-way binding as you would maybe normally do it. Instead, you can use these name attributes and the automatically generated form submission object so to say, which is part of that request that was automatically generated. To do that here in this action function, I will actually turn it into a async function because on this request object, there is a formData function, a formData method, which we can execute, which actually returns a promise, which I wanna await. We get this because this default request object, which was generated here has this built-in formData method, which gives us access to the data that was part of the form. So here I got my formData. And now we can construct a post here, a post object by giving it a title property and accessing formData.get title. The get method is a method supported by this formData object, which is returned by this formData method here after awaiting it. And these are all default browser APIs by the way. This is all part of the default request object, which is leveraged by React Router. So here I'm getting the title value. And the value I pass to get here is one of the names defined in my form. In this case, this title name. And if I also wanna get the main text entered, I have to use that name, post-text in this case. So here I will add a body field and set this to formData.get post-text. And I'm using body here as an identifier because that's what I'm using in api.js later for validating the input. And that's also what this dummy backend API expects to get. It expects an object with a title and a body property. That's why I'm setting this here with help of that formData that was generated automatically. Now what we can do is we can call savePost. So simply this helper function defined here in api.js and pass this post object to it. Like this. And savePost now also returns a promise, which I'll await because I also wanna wrap it with a try-catch block because actually this could go wrong. If we take a look at savePost, we see that we throw an error if validation fails. And if something goes wrong with sending the request. Therefore, here if we get an error, I wanna check if error.status is equal to 422, which would be the case if we got a validation error because in that case, I wanna do something else in the near future. For the moment, I'll just rethrow the error but this will soon change. Otherwise if it's a different error, I will also just rethrow it. Again, we'll add more logic in here soon. And if we make it past this try-catch block, then we might want to redirect the user because then the form submission was successful. We did send the post to the backend. We might want to redirect the user therefore. Before we did this by calling the navigate function, which is also implicitly provided by React Router. I'm still using this here if we cancel entering data in the form in the end but I don't wanna use it here in the action and actually, we can't use it here. Instead here we should return redirect and redirect is another function imported from React Router. In the end, it generates a response that triggers the browser to navigate us to a different page. And here we can just define the path of the page we wanna go to. So that's how easy it is to redirect the user after triggering such an action. Now to see this in action, I'll just temporarily set is submitting to false here. We'll soon change this again but otherwise, we can't test it right now so I'll set it to false. And if we then save everything and we create a new post, this should work. We see that indeed, we were redirected and behind the scenes, an HTTP request was generated and sent. You can see this again if you enter something here. There you go. You see some HTTP requests being sent here. So that works and again, it's less code for us to write and more work done by React Router. And especially our route component here got much leaner. Now, of course, sometimes things go wrong when submitting data. Right now I'm always rethrowing the error and therefore, of course, the error handling here is redundant but if you are rethrowing an error, if an error is thrown in general by your action, then the same thing will happen as it did for loaders. The closest errorElement will become active. In my case here, that means that if I break this URL, which we used for submitting the post, for example, and I then add a new post, I see this error page thereafter. But sometimes, especially when submitting data in a form, you don't want to redirect the user to an error page. Instead, you might have some validation error and you wanna stay on this page therefore but you wanna show some message to the user or show some error information to the user whilst still being on this page. And this kind of error can be handled by not rethrowing here when we have a validation error, but by instead returning a value here. And the error I return here could be that error which I have, that validation error. If I return here instead of throwing, I stay on this page. I don't redirect away and I don't load the error page. Instead, I stay on this page and we can get access to that returned data by using the useActionData hook. So now with that hook which is also provided by React Router of course, we get hold of that error data or whichever kind of data was returned by the action, and we can use it here in the UI. For example, to check if data is set and if data is set, and has a status, which indicates that it's an error, for example, then we could output a message. We could output that message that's part of the error object that makes up our data. If we then simulate such a validation error by temporarily adding super harsh requirements here in our validation code, you will see that I get this error message here but all my data is still kept around down there. Now, with that, I'll switch back to a more reasonable validation check here but that is how you can return data and use data with an action. Now, when submitting data, you also sometimes wanna find out what the current state of the data submission is because whilst we do redirect after the data was submitted, we don't do anything whilst the data is being submitted. And we might want to disable the buttons of the form whilst the data is traveling to the backend, for example. To do this, I actually already have some code in my NewPostForm component where I set the buttons to be disabled based on this submitting prop which I get. But right now, that prop is always hard coded to false. Now, React navigation gives us another hook called useNavigation, so not useNavigate but instead useNavigation, which is a hook that gives us an object that simply exposes some navigation information. To be precise, we can access a state property on that object, and that state is either idle, loading or submitting. And we can use that to check if it's currently in submitting state, so if our action function is currently doing some work and that then allows us to disable our buttons whilst that is the case. So whilst our action function is doing some work. So useNavigation gives us more information about whether our action or our loader functions are currently doing some work. And with that, if I submit a new post, you will briefly see that the buttons are disabled and contain some different text until we're then finally redirected here. So these are the basic new features added by React Router version 6.4. It's all about data fetching and data submission and it's all about getting the related logic out of your components, and make your components leaner and instead, the goal is to put that logic in a simplified form into these extra action or loader functions. And it's a vastly simplified form because you don't have to worry about error handling and loading spinners or anything like that because React Router manages all these things for you as you saw over the last minutes. It's a different way of thinking about data fetching and data sending but ultimately, it allows you to write less code and leaner components. I like it a lot and I think it greatly simplifies the way of building complex React applications. If you ever worked with Remix, by the way, which is a React framework built by the same people that built React Router, a lot of the things I just showed you will seam familiar to you, by the way, because Remix also embraces this syntax and approach. The key difference is that Remix allows you to build full-stack applications based on React and allows you to add backend code to your React app with ease. Whereas with React Router, we're still on the front end and that's really important. Even inside of these action and loader functions, we're still on the front end. We might be sending requests to a backend here but all this code still executes in the browser. We're not on a server here. It's still browser-side code. Now, we did explore these core features added by React Router version 6.4. React Router version 6.4 also offers some advanced features related to data fetching and submission though. And that's still important. It's still all about data fetching and submission. That's still the key theme. But one useful feature added by React Router version 6.4 is that you can also defer loading data. But what does this mean? Well, let's assume we've got the DeferredBlogPost component here where I'm also fetching blog posts and I'm basically rendering them as I did it before but I'm doing this with help of a function that's actually quite slow. Maybe I'm sending a request to a backend that's pretty slow. Here this is simulated with help of a separate function where I simply pause execution for two seconds before sending the request because the API is quite fast. But of course, you could have a slow API here as well. I'm just using this approach to simulate such a slow API. Now, if you have something like this, you'll notice that whenever you wanna go to this blog page, you click on it and it takes some time until you get there. So I'm clicking now and it takes two seconds until this page is loaded. The reason for that is that as mentioned before, React Router waits with the page navigation until the data for this page was loaded. Now, there are different ways of handling this. You could, for example, try to build some loading bar that's shown until the page is loaded but you might also want to visit the page before the data is there to signal to the user that something's happening. And if that's what you wanna do, you can use another feature offered by React Router. You can import a special function, the defer function and this allows you to defer loading data that belongs to this page component. And you can also defer just parts of the data that's required if you're loading multiple pieces of data. Here I'm just loading one piece of data, one list of posts, but I can defer this as well simply by wrapping this with defer and putting it into an object like this. And then here I'll give this a key of posts, and the value stored for this posts key in this object is the result of calling getSlowPosts. Now, this returns a promise and that promise is stored under posts in this object that's wrapped by defer. Now, by using defer like this, we can use another special component in our page component here. And that's the Await component provided by React Router. The Await component is rendered like this and it has a special resolve prop. To that resolve prop, you pass a pointer to that function, that loading function that's being deferred. So in my case, loaderData.posts. Because loaderData is basically that object, which I'm returning here with help of defer, and that object does have a posts key, and that posts key holds that function that is quite slow. And that is the function for which I'm waiting here by passing a pointer at that function to resolve on that await component. You can also add an errorElement here to specify which element should be shown if loading that data should fail eventually. Error loading blog posts, for example. And now for await to work, you must wrap it with the Suspense component, which is provided by React. You might know that component from code splitting. Well, React Router also uses this Suspense component for showing a fallback until that data for which you're waiting is there. For that, you add a fallback prop to Suspense and then here you could say loading or anything like that. You basically put your loading spinner or your loading fallback code here. Now, you also must define some code that should be executed once the data is there. And that can be done with help of a concept called render props, where you put some dynamic code between the opening and closing tags of Await, and you put a function in here. And that function receives the loadedPosts and that function will be executed by React Router once this function here for loading the posts, this slow function finally finished. And here you can then render this Posts component with its blogPosts prop, which receives these loadedPosts. So that's in the end the content that will eventually be shown once the data was loaded. Now, what's the idea behind using defer with Await and Suspense though? Well, after making all these changes and getting rid of that old Posts component here, if we save everything, you will notice that if you go to that Blog page, you see that Loading text for these two seconds and then the data appears but the rest of the page is already visible. So we already show parts of the page instead of only showing the full page after all the data is there. And in this case, we're only fetching one piece of data, only that list of posts but of course, some pages, like some complex admin dashboards might have multiple data sources and then it can be really useful to show some pieces of data that already have been fetched and not waiting for all the data to be there before showing anything. And by the way, if you do defer multiple pieces of data here, because you can, of course, add as many keys down there as you want to, then you can also control on a per-key basis which data should be deferred and which data should not. You can do this by simply adding await here in front of that function that returns a promise. If you do add await, that tells React Router in the end that it should wait for this function before showing the page. So if we add await here, you see if I go to Home and click on Blog, nothing happens for two seconds and only then the page loads. If I remove await, we tell React Router that we should not wait with loading this page until that function finished but that instead we should instantly load this page and then put this data into this page once it's there. So no await means we don't wait for it, and therefore, we visit this page instantly and only wait for parts of that data. With await, we wait until the data is there, until the page is loaded. So if you have different piece of data here, you can await some of them, the ones which are critical and without which the page should not be loaded at all and you don't await others, which can be loaded with some delay. So defer is another key a bit more advanced feature provided by React Router. Another new feature offered by React Router is the useFetcher hook. This hook can be used to basically manually trigger a form submission or build a form by using fetcher.Form. And the difference you have when using fetcher for submitting the form instead of using that regular form component I showed you earlier is that it will now cause no page transition or anything like that but instead, the request is basically sent behind the scenes and you can still target a specific page here to which is the request is sent. In this case, I created a route that contains no component code at all but just an action and that was registered in my route definitions. Here, by the way, written in this alternative approach using route definition objects. But here I have this newsletter route, which renders no route component but defines an action that should be triggered whenever a request is sent to that route so to say. This action here. And I'm sending that request with help of fetcher behind the scenes. So the core idea is really just that we don't trigger any page transition and the fetcher functionality is therefore ideal for pages where you wanna send requests without switching the page like, for example, here where I can sign up for a newsletter from inside any blog post. I don't wanna leave the page just because I signed up, so therefore here instead, a request is sent behind the scenes with help of fetcher. It's a slightly more advanced pattern but it can be helpful for avoiding unwanted page transitions. And that's basically it. Of course, there's always more you can learn. And therefore, you should definitely also check out the official documentation if you wanna learn all about the brand new features and how you can use and combine them. But this video hopefully got you started and gave you a first overview and idea of what React Router version 6.4 is all about and how it can simplify the process of fetching and sending data.
Info
Channel: Academind
Views: 68,175
Rating: undefined out of 5
Keywords: react, react.js, react router, react router dom, react-router-dom, react-router, react router 6.4, loader, action, data fetching, useeffect, maximilian schwarzmueller, maximilian schwarzmuller, maximilian schwarzmüller
Id: L2kzUg6IzxM
Channel Id: undefined
Length: 44min 30sec (2670 seconds)
Published: Thu Sep 22 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.