The Ultimate Guide to Server Actions in NextJs 13 with Error Handling & Validation Using Zod

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video we're going to talk about server actions which is a new way to mutate data on your server now at the time of this recording which is June 2023 several actions are still in Alpha release I'm going to update this video if next JS releases any major updates on server actions but we're going to learn this anyways I've condensed everything that we're going to go over in a little note here so I remember everything that I want to talk about so let's get started now let's first Define what server actions are as I mentioned they're in Alpha release and they're built on top of react actions I'm going to explain what react actions are in a second but before we get there several actions are used to do server-side data mutation so when you want to update your data on the service side that's called the mutation we can use server actions now they're going to reduce lines like JavaScript and progressively enhance your forms now if you're using server actions with a server component they do not require any client-side JavaScript for example to submit your forms to your backend and mutate your data and progressively enhance your forms it's kind of the same thing which your forms are going to actually work they will be interactive people can submit data without JavaScript and then react is going to actually enhance it on the client side progressively without blocking the interactivity of your form when you're using server actions Now actions in react or experimental feature again and basically they would allow you to run asynchronous code in response to a user interaction so a user interaction can be submitting a form in response to that event you can run an asynchronous code Now actions are defined through the action prop of an element we're going to see more ways to actually create and invoke actions other than the action prop itself but this is the most prominent use of your actions now instead of passing a URL to your HTML form this is how the action prop or attribute works on an HTML form react allows you to pass in a function to that same action attribute now with using actions you can also have optimistic updates or optimistic UI thereby you would just show the update that's intended to be triggered by your server action without waiting for the response of your server action so that's optimistic you either you do the update immediately before waiting for the action to actually finish updating that in your backend now let's look at how we actually create server actions to start you have to turn this experimental flag for Server actions inside of your next config.js because as I mentioned this is a still a experimental feature in react and also in xjs it's in Alpha release so you have to set the experimental flag for it first and once you've done that you can Define server actions in two different ways one is inside of a server component that actually uses that server action or you can Define your server actions in a separate file for example in actions.js so therefore you can reuse the actions that you've defined there in different components now let's look at an example of using server actions with a server component now inside of this example server component that I have add to cart over here we have defined this asynchronous function Now actions are asynchronous functions that you would pass into the action prop of A4 we're going to see different ways that you can actually invoke actions but for now the simplest way is to use the action prop of a form element I hope you can see this let me just make this a bit bigger so the first way is to just pass your actions to the action prop of a form now this is an asynchronous function it receives the form data it uses this use server directive in the body of the function this instructs next.js that this function is meant to only run on the server and we're going to follow the similar path if you're using a separate file as well we're going to see it in a second and then here in this example we are reading the cookies because you can also access the cookies inside your server component and we're getting some data and saving it to our database as an example now this function just so you know the re arguments that you pass to This Server action and if you are returning anything from it needs to be serializable because you're actually submitting this data from the client to the server so it's crossing the boundary between the server and client it needs to travel over the network it needs to be convertible to string the arguments you pass with also anything that you return from your server actions now let's look at an example of using server actions with a client component now you cannot Define server actions inside client component you can only import them to client components so for this example we're using a separate file for example this underscore action.js inside of our app directory we're going to put this use server directive up top and once you do this any other function that you're exporting from this file is going to be treated as a server action so you don't have to repeat this inside the function body you can just put it once up top and it will be applied to every single function that you export from this actions why is that the idea is you define all of your actions in just one file and then you can access them or reuse them throughout your application now this underscore naming convention in xjs just so you know makes this private and it just takes it or offsets it out of your route segments so we have defined this same ad item function or server action over here we then imported it inside of our client component so this client component lives inside our file components it's using the use client directive up top to instruct NXT is that this is a client component and we are importing this because that's the only way you can get server actions inside of your client components so you can pass them as properties too we're going to get there um but the point is you cannot Define interactions inside client component you can just import them and we're just importing that and passing it to the action prop of our form over here so this is using getting a server component and also in a client component now I'll note here that I have that even though importing server actions is a recommended way sometimes you would need to or you may want to use contextual data like this ID inside of your server action as you can see here I'm defining the server action inside of my server component so I have this example server component and by the way these examples are directly from the docs I've just condensed what I've learned from the docs over here so I don't take any credit for this is actually an example from the documentation definitely recommend to check it out kudos to the team for putting so much effort into creating the nice documentation here now as you can see here we are defining This Server action and then inside of it I'm accessing this ID which is passed to the server component and then I'm passing this update item which is this server action as a prop to my client component now inside of my client component I'm receiving this function which is a server action and I'm using it to pass it to a client-side action and this is another pattern that you can use Now actions are basically asynchronous functions you can have client actions or you can have server actions server actions are functions that run on the server but they can be called from the client side client actions as you can see here are asynchronous function so I'm passing in a client action to this form action and inside of that client action I'm actually calling this update item which was a server action that was passed as a prop to my client component which was created inside of my server component so you might think I just said you cannot create server actions inside client components remember this is not a server action because it does not use this use server that's how you would say this is a server action if it has this use server directive inside the function body or if it is defined in a separate file like the example we just see here we have this use server directive up top of the file which affects all the functions that are exported from here so here down we only have a regular client site action which is not a server action so we can Define it inside of a client component just like that and inside of it we're calling This Server action that was passed into this client component as a prop and we created that server action inside of our server component now in the next section we're going to talk about how how do you actually invoke this created server actions the first example that we've seen was the action prop on a form element now the second way that you could invoke your actions is the form action prop that you can actually use them on a button on an input with type submit or with an input of Type image now if you don't know what input Type image is I didn't know either I looked it up it's just like an input type submit or it's just like a button but instead of a text it just shows an image on the button uh I never came across this but good to know that you can actually use form actions on these things and this is when or it's useful when you want to actually have different actions inside of a form so if I kind of quickly show here inside of this form you can see I have a main action which is this handle submit this is my main action and then I have a secondary action let's say an upvote button for lack of a better example and this this form action actually takes precedence over this form action so it takes precedence over the owner of this button and just submits or invokes this upvote action so you can have two different actions happening on the same form now you can have let's say a like button or an upward downward button or a delete button here tied into a specific server action and then you can still have your main action for the form then the form is actually submitted so that was the second way we could invoke server actions and the Third Way is actually using start transition now start transition is a function you get back from the use transition hook and use transition hook allows you to update State without blocking the UI so it's a new hook in react and it will be useful if you want to use server actions outside of forms buttons and inputs okay so let's say here I have that same add item actions defined in a separate file called underscore actions and inside of my client-side component I have a button it's not inside of a form and I don't want to use the form action there is no form over here I just have a button and I want to invoke this add item server action in response to a click of the button then I can use a use transition hook which gives me this start transition function back to the start transition you have to pass in a callback which is which is going to be invoked you can perform any asynchronous task that you want there which is going to then perform that task without blocking the rendering of the UI and then mutate your data to on the server side now one thing you may notice here is this revalidate path function that we're calling inside of our add item server action and this is a way to manually Purge the next JS cache the HTTP cache and then refetch and re-render this specific server components at this specific path for example here we are just marking all of our product pages with this Dynamic ID to be revalidated we have the revalidate path and revalidate Tac both of which are just manual functions that you can use to purge next.js cache to revalidate a specific path or a specific tag inside of your cache and that was the third way to invoke your server actions to perform a data mutation or server mutation now there is this section in the documentation that explains that if you're not doing server mutation and that means you're not calling redirect revalidate path or revalidate tag after you have updated your data because typically when you're updating your data you would want to revalidate that path or the tag and refetch and re-render that specific segment to reflect that mutation now if you're not doing that redirect or revalidation you're technically you're not doing a server mutation you're just performing a server action and server actions are just functions that run on the server that you can call them from client-side if that's the case you can actually pass your server action as a regular function down to your client components so here inside of my page there is this increment server action file created with this use server it's a server action and we're just passing it as a prop to our like button which is a client-side component that just invokes that increment in response to an on click so there is no form there is no start transition because we are not actually doing a server mutation which means that we are not redirecting or revalidating your path after we can just pass this server action functions as props down to client components and invoke them from the client side for example in response uh to an event now Progressive enhancement is we touched on it briefly in the beginning it all means that your forms are going to function without JavaScript so if you're using server actions as I mentioned inside server components it actually doesn't need any client-side JavaScript because server components don't actually ship any JavaScript to the client side so your forms would work your users can submit data without any client-side JavaScript and if you're using server actions inside client components they would still work so that your form would still be interactive but once the action is invoked it will be placed in a queue until the form is hydrated so until react hydrates the form and then that queued action will actually go ahead and execute so in both cases you're going to have interactive forms without the need of JavaScript with the server components you just have the added benefit of not actually needing any JavaScript with the client components or using server actions inside client components you can still have an interactive form without JavaScript but the action or the execution will be queued until react actually had the chance to hydrate your forms now the on-demand revalidation is using the revalidate path and revalidate tag from next cache as I mentioned to actually Purge the cache for a specific path or for a specific tag inside of our cache index.js Naval Edition well this data that you are sending to your server actions is data that's coming from the browser it's coming from the client side and you should never trust the data that's coming from the client side it's basically the form data passed into your server actions so you should verify validate and sanitize this data you can use any type of validation that you want inside of your server actions or before you invoke your server actions and there is a pattern shown inside the documentation whereby you would only call your server action if the data coming to your server action with the use of this with validate function actually passes your verification so you sanitize it you verify it and only if it passes then you would call the server action with it otherwise you just throw an error now we're going to talk about error handling and I'm going to actually implement it when we jump to the code but for now we just know we have to validate the data coming in from the client side now you can also use cookies and headers inside your server actions from the next headers you can get the cookies as you can see here we are reading the cookies and in the second function we are actually setting the cookies pretty straightforward stuff other enhancements I just grouped all the other hooks over here there's this use optimistic hook that enables you to do optimistic UI or optimistic updates which literally means that it would just immediately update the UI based on the desired action or the desired outcome of your server actions without having to wait for your server action to do that async update and then the response to come back and there's also this use form status hook which you can use it with your form actions which is going to give you the pending property so when your forms is actually submitting or the server action is in process you'll get a pending status to maybe disable the form or the buttons to submit this is again a client-side hook that you could use with form actions I'm going to just quickly talk about this actions we mentioned these are a react feature which allows you to do an asynchronous task in response to user events that's react actions in short server functions are functions that run on the server but you can call them from the client server actions or just server functions that are passed in as an action to a form now server mutations as I touched on it are server actions I mutate your data and then call redirect revalidate path or revalidate tag now let's jump into the code and see all this in action so we're going to continue building on top of where we left off in the previous videos for folks who haven't seen those videos or don't remember what we actually did we created this guestbook page where we were fetching some data from our backend in this case was mongodb we are fetching the entries that people have left on our page and then we were showing them down here we also had this guest book entry form where we had this form with an input of a name and a message and then the way we were handling this form submission was the traditional react way of handling form submission whereby we have this handle submit attached to this on submit event handler on our form we also created an API endpoint using route handlers in nexjs13 or inside the app router we had an API endpoint this is the layer that actually talks to our database and then performs that insertion into our collection and then returns the data back or returns a success message or error message back over here we also implemented this use transition hook because until the server actions are actually stable you will get this start transition function from the use transition hook and then up until this point this is just regular react things we are preventing the default Behavior we're getting the form data out of the event simple data validation here we're going to replace this later on we are having a local loading state to see if you're loading which is when we're actually submitting this post request to our backend API and then once we're done if we don't have any errors we're resetting the form we're setting the loading to false and we're using that start transition function we get we got back from this hook to actually refresh this specific route or the path so therefore we refetch the data and re-render the server component to actually then show the results of that newly created message we actually discussed this specific page being a dynamic Page by using this Force Dynamic so therefore we can see that message actually popping up at the end if we don't export this Dynamic from this page it will be by default a static page which means the get data or the data fetching only runs at build time statically once and it never actually updates the cache so we needed to make this a dynamic page that actually refetches the data every time at request time so therefore inside of our form when we're calling this router.refresh it is actually refetching this data which then includes that newly created message down there if you're actually getting this is pending from use transition and creating this is mutating by looking at loading State and is pending that comes back from view strand decision and if you're mutating we're going to disable the form or maybe disable the button now let's see how we can update all of this to use server actions first off we no longer need that intermediate API layer to talk to our database we can just Define server actions that are functions that run on the back end where we can just talk to our database and we can call the server actions from our client side in this case from R4 so let's actually start by creating our first action I'm going to go inside the app directory create a new file called underscore actions and inside of it what I'm going to do is I'm going to export an async function called add entry that's going to receive some data and it's going to do the following is going to get the name and the message this is the form data submitted from our form in the client side I'm going to call object from entries and I'm going to pass this data and we're going to do that same error same validation check we're going to replace this with proper validation later on but for now if we don't have a name or we don't have a message we're going to throw a new error to maybe say invalid input and if we do have the name and the message we're going to actually do the same thing we were doing inside of our API endpoint so in this route.js that you can see I created an API folder inside of the app and a guestbook endpoint with this route.js I'm creating an endpoint inside of it forget about this get we're actually working with this post you have to export named functions from this route handlers that are going to map to http verbs so for a post request we are exporting a function named post and here what we were doing was to get the name and message out of the body and once we had those things we were going to call this create guestbook entry which is a database function we created earlier which just gets the name and message and gets a handle to Our Guest guestbook collection in mongodb and calls the insert one function on it and inserts a new document in our guestbook collection so we're going to basically do the same thing that we are doing inside of our API route but inside of our server actions and that's why we no longer need the API endpoint to just handle the submission of this form so I'm just going to copy this over to the server action and here I'm just going to say if I encounter any error again let's just throw a new error with that error now remember the convention here is to actually include this use server directive up top so if you are defining server actions you have to either have this use directive inside the function body or if you're defining all of your actions in a separate file you can just put it once up top here and then this would be applied to all the functions that you export from this file so that you can reuse these actions throughout your application so let me save this and go back to our form over here so what I'm going to do is that instead of on submit what I'm going to do is I'm going to pass in an action and here I'm going to just import that ad entry we created inside of our actions let me just comment this things out I'm going to also comment this Imports out we no longer needs these Hooks and actually I'm going to also comment the use client out so I'm just dealing with a server component over here as you can see that uses the server action path to the action prop of this form now we're going to get an error because we have to actually enable this experimental flag inside of our next config.js so if you have to pass in the experimental flag and server actions to true I'm going to actually go ahead and restart the dev server now if I refresh the page now I actually have another error this is mutating that we were using before so let me just X this out and if I refresh the page now we get our form back let's just review what we have done we have created this action inside of a separate file we just use server up top this is going to get the form data passed to it because we're passing This Server action to the action prop of our form using react actions the form data inside are going to be passed to this function that's the form data that we're receiving here I'm just creating an object from the entries inside of this form data getting the name and the message out of it and the name then the message are going to correspond to this add name values you're putting on this input so your inputs should have a name property or attribute so I have an input with name of name and input with the name of message these two are these same things I'm getting here if I don't have name or message I'm going to throw an error and if I do have the name and message I'm going to create a new entry inside my database and if I encounter any error again I'm going to throw this error now let's actually go ahead and try and see if I can create a new message maybe here with server actions so this encounter that error let's see what the error is now the create guestbook entry is not defined well I copy pasted this so let me just go ahead and actually import this over here let's just refresh the page and if I go ahead again and create a message from server actions and add nothing actually happened here I click the ad nothing happened now if I pull up my mongodb compass you can see that this last message was actually created in our database which means that the server action actually gone ahead and performed that data mutation in our backend but we haven't revalidated the cache for this page we have and refetch the data to re-render the server component to see that actual last message we have over here for this we can use the functions I mentioned in the beginning they revalidate path which is going to revalidate this path we're going to path pass a string to the path that we want to be revalidated these are manual functions from next cache that allows you to just manually Purge the cache for just a specific path so therefore as I did that you could see this last message actually popped up over here now if I just refresh this page and create a new message say with re-validate path now you can see it just pops up at the end here because now once this form action actually invokes This Server mutation VR actually revalidating this path at the end of it which just re-runs This Server component and This Server component is this guestbook as part of it we're going to refetch this get data function to get our new entries and then re-rendering the server component over here now as you can see this form actually doesn't wipe the content out even though it re-renders this list down here we can use a little trick here on our form without turning this into a client component we're going to turn it into equine component user ref to reset the form later on when we are actually validating the data and handling the errors but for now you can just pass in a key and then to this key if you're just pass in a random number seems to be doing the trick over here by actually causing to this form to be re-rendered when we are actually revalidating this path so now if I go ahead and say with key prop this is going to pop down here and it's actually going to wipe the content of this file out now let's talk about error handling in our server mutation or server actions right now all we're doing is to check to see if we have a name or a message submitted with our form and if not we're just throwing this error so let me actually go ahead and create a new entry that doesn't have any message so as you can see this is actually throwing an error which is called by error boundary from our app this is the error boundary that we created inside of our app this error.js as we discussed in the error boundary section this is just capturing this error inside and then showing something went wrong from the app now let's actually go ahead and create another boundary inside of our guest book so I'm going to create an error.jsx over here let me just copy some code so you don't have to watch me type over here all I'm doing is to create a new error boundary inside of this guestbook segment this route segment so it's captured here instead of getting caught in the error boundary belonging to the app and inside of it I'm actually showing that same error message but I'm using this reset function if a function that the error boundary actually exposes it's a way to actually reset the state inside of our error boundary which allows the user to retry whatever it is that they're trying to do if they're fetching the data or in this case submitting a form it just allows them to do it again so let's see this in action if I now create that same entry without any message now this time the error boundary from our inside of our guestbook is actually getting this and if I try again or hit this button it actually resets this path resets the state inside of our boundary which allows me to just use this again now if you notice we have this overlay of the error in the development this is not going to be shown in production as I can show you here if I just open up my server and stop the dev server and actually run a build and if I now start the production server by running pmpm start actually opening up a new page why not and going to the guest book if I now create a message that doesn't create an entry that doesn't have a message you can see that overlay no longer shows because that's for development for you as a developer to actually see what the error is the users on the production bundle or when you are actually deploying your app to production won't see that overlay but still we were able to catch the air inside of our error boundary instead of this gas segment and with this reset function that the Euro boundary exposes we can just reset the form back as you can see we down here on the server we can actually see the logs of the airstill but the user can just go ahead and maybe this time they actually provide a message and if they add everything works so this error that we're throwing from a server actions it's not actually crashing our whole application and with this reset function we can actually reset the form allowing them to actually do something now this error that we are currently throwing is not actually telling them any message that's useful they don't know what went wrong or if they had it to provide something else here so let's actually look at implementing a more robust error handling going back to the action that we have over here I'm going to actually verify or validate the data that the user is sending because at the end of the day you have to remember this is the data coming from the client side from the browser and should never trust any data coming in from the client from the browser so you should validate this data I'm going to use which is a data validation library that works well with typescript if you're creating your apps with typescript let me just make this a bit bigger you can see for the installation you can just install it you can use it with JavaScript as well if you're using it with typescript which it's where it actually shines because it can infer the type of your types from your schemas instead of you having to duplicate creation of the schema and the type but in this example we're just using uh for this schema validation we're not actually inferring any types from it all you need to do is to install zot so let's Circle just go ahead and stop the server and add Zod here and what you need to do is once you've installed it you can just import Z from zot and with the use of DC you can Define new schemas for example this my schema which is a string or we can define an object which has a username of string we can include different limitations or very applications or validations to this string and then once you have this schema created you can parse any object against that schema to see if it actually passes your verification or it's actually mapped to that same schema or the same shape that you have created for your schema over here down here as I mentioned you can use this in Fair from zot to actually get the type or create a new type from this same schema you created up here so you're not recreating these types over here and it actually has these parts safe or safe parse method where it doesn't throw an error the the parse function throws an error if what you are trying to parse doesn't actually map to your schema it throws under the safe Parts doesn't throw an error actually gives you an object back it has a success or error property on it which you can use and test and see what went wrong or the message of the specific error messages of why actually it failed uh the validation this actually implement this in our code I'm going to restart the dev server over here I'm going to create a schema inside my lib folder so I'm going to create a new file I'm going to call this schema.js and all I'm going to do is to actually create some schemas so let me just bring in here I'm getting the Z from zot I'm creating a guest entry schema by calling the object function on the Z I'm saying this entry should have a name which is a string should be minimum of one character and if they didn't pass this name I'm including a specific error message that says name is required and a message I can do the same thing by passing in another object here and say message is going to be that message is required okay let's now save this going back to our actions we want to bring in that schema that we just created I'm going to call the save parse on it and I'm going to pass in this name and message we got out of our form data to our safe parse function now as you saw in the documentation this is going to give me an error back if there is an error so I'm going to call this an error I'm going to rename these two actually Zod error because I have another error down here and then I'm going to check and see if I have any error from actually validating our data I'm going to actually return an object with the error property on it and then I'm going to pass in this Zod error and then I'm going to call the format function on this zoter where it turns the error that you actually got into an object where there is going to be Keys mapping to the name of values you have actually defined inside of your schema now let's actually go back to our form and use these error messages that are coming from our server actions so if you have to turn this into a client component because we want to use the state to actually show these error messages to the client so I'm going to comment this back in let me just also get rid of the is sections that we no longer need so it's nice and clean so here instead of passing this add entry directly to this form action I'm going to actually create a client-side action I'm just going to name this action and this is going to receive the form data and all we want to do here is just calling this add entry data passing that same data so I'm going to just get this action and pass it to the action prop here I'm just going to also get rid of this key we're going to reset the forms using a user because now if you're a client component okay let me just expand on this function a little bit more if you remember in the beginning we talked about this pattern of using client actions with server actions this is a client action that we're creating because it does not have the use server a directive inside the function body is just a client action which is just an asynchronous function that will be invoked in response to user interactivity this is using react actions now you can create and compose client actions with server actions the difference is that server actions are functions that run on the server you can call them from client and client actions are functions that just run on the client in response to any user interactivity here so if you're passing this action to this action and the reason why I'm doing this is that now that I'm calling this ad entry from inside of my actions this is actually possibly sometimes may return an error so I'm going to have to read the response that comes back from this server action to actually check and see if I have any error on it and if I do I want to show some local error state so the user can get useful messages and understanding what went wrong so what I'm going to do here I'm going to check to see if I have an error and if so maybe I want to set some local error state so let me just Define some State here I'm going to use react user State hook to create some validation error state I'm going to start off with null and if we do get an error or what I'm going to do is to just set this validation error we created and I'm going to pass this result dot error to it and if we don't have any errors well I'm going to just set this validation error back to null to wipe out any previously encountered error and then we'd have to also reset the form now the proper way to reset the form here is to actually use the use ref hook so I'm going to create a form ref by using the use ref Hook from react let me actually also import that up top here and I'm going to have to pass in this ref to my form element so therefore down here I can just come in here and say okay let's get a reference to our form and call the reset method on it let me just save this out actually I do have a typo here so it's form ref and that's actually also initialize this to null so now that we have this validation error which is going to be an object coming from Zod that actually has keys mapping to the same schema that we have sent it so it's going to have name and message on it I'm going to create this error messages down here so I'm going to check to see if the validation error actually exists and if the name property on it exists and if so I'm going to just render a P tag over here that actually shows the error message that we got here so by calling validation error we're going to access the name on it there is be going to be an underscore errors property which is going to be an array so I'm going to have to call the join method on it I'm going to join them with a comma so we can show all the error messages let me actually also pass in a class here I'm going to say text as small and text red 400 me save this up and with the same logic let me actually also go ahead and add some error messages to our input message and the only thing different here is that we're checking the message property on our validation error object if the message field has any error on it we're going to show that specific error messages down here and now if I go back to our application now if I go ahead and create a new entry without a message we should be able to get a specific message back inside of our client component this time that allows us to hold some local state if I now go ahead and actually do create a message over here submit that message down here and then resets the form back to the initial State and similarly if I provide a message without a name I'm going to see that specific message that we passed in or provided inside of our schema to zot is going to show up there then once I provide my name that actually is going to also go ahead and the message is going to be created that's a wrap for this lesson folks we talked about server actions which is a new way to do server mutations it reduces the amount of JavaScript you need to send to the client side it actually works without any JavaScript on the client side if you're using it primarily inside server components and even if you're using it with client components your form is still interactive but that action is going to be queued until react actually hydrates that component and then performs that action now this also eliminates the need to have an intermediary API layer traditionally in react when we wanted to handle form submissions we will create an API layer of your form would submit the data to that API layer the API would then talk to your database and then respond back to your client side with the use of server actions you can just perform that same operation directly with exposing a function that runs on the server but you can call it on the client in response to user interactions if you have any questions hit me up in the comments if you're interested in learning next.js this was an actual lesson from my course there's a link in the description the course will launch this week check it out let me know if you have any questions and I'll see you in the next one bye
Info
Channel: Hamed Bahram
Views: 3,759
Rating: undefined out of 5
Keywords:
Id: sdKFEo6978U
Channel Id: undefined
Length: 40min 43sec (2443 seconds)
Published: Sun Jun 25 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.