
No Comments Yet
Be the first to share your thoughts and start the conversation.
Be the first to share your thoughts and start the conversation.
Complete source code available till this point of lesson is available at
How did you manage to remove the blur property and reach here?
Upgrading gives you access to quizzes so you can test your knowledge, track progress, and improve your skills.
By logging in, you'll unlock full access to this and other free tutorials on JSM Pro.
Why? Logging in lets us personalize your learning experience, track your progress, and keep you in the loop with new workshops, coding tips, and platform updates.
You'll also be the first to know about upcoming launches, events, and exclusive discounts.
No spam—just helpful content to level up your skills.
If that sounds fair, go ahead and log in to continue →
Enter your name and email to get instant access
In this lesson, we focus on implementing a views feature for a question details page, similar to how video views are tracked on platforms like YouTube. The goal is to increment the view count every time a user visits the question page, providing valuable data for both user experience and future recommendations. Several approaches for triggering this action are discussed, along with the underlying mechanics of server and client interactions in the process.
RevalidatePath
is suggested to ensure up-to-date information is displayed without manual refreshes.00:00:02 And we are ready to implement the views feature.
00:00:05 This is quite a big one and something that you not often see implemented in courses or tutorials.
00:00:10 Basically, whenever a user visits a question details page, we want to record that visit by incrementing the view count of the question by one.
00:00:19 This works similarly to YouTube, where opening a video increases the video's view count by one.
00:00:25 There are many ways to implement this feature, but the core concept remains the same.
00:00:30 When a user visits the page, either when it loads for the first time or after a request is completed, the view count is incremented by one to reflect the
00:00:39 user has accessed that page.
00:00:40 This is not only useful for showing the total number of views, but also for recommendation algorithms later on, because if you know what a user is viewing,
00:00:48 you will know what they want to view next.
00:00:51 So let's first implement an action that increases the view count of questions by one.
00:00:55 Head over into question.action.ts.
00:00:59 Let's collapse the current question right here.
00:01:02 And let's create a new one right at the bottom.
00:01:04 Export async function, increment views.
00:01:09 It'll be a function like any other that accepts params.
00:01:13 of a type increment views perhaps of course this is a type which we are yet to declare and it'll return a promise of a type action response specifically
00:01:25 it'll have a views of a type number perfect first let's head over into the action.d.ts to create a type I'll create a new interface of increment views params,
00:01:39 and it'll simply accept a question ID of a type string, of a specific question we're viewing.
00:01:44 After that, we have to create validations.
00:01:47 So let's head over into validations.ts.
00:01:51 And right at the bottom, let's export const a new validation called increment views schema, which is equal to z.object that'll accept a question ID of
00:02:03 a type z.string.min1 message question ID is required.
00:02:08 Then right within this code block, we can call our reusable action by saying const validation result is equal to await action call And then to it,
00:02:21 we want to pass the params as well as the schema of increment views schema coming from validations.
00:02:29 As usual, we want to check if a validation result is an instance of error and if it is, we return a handle error with the validation result as error response.
00:02:41 If it's not an error, then we can extract the question ID from the params like this.
00:02:47 You can see that our params are complaining a bit, saying that property params does not exist on type error.
00:02:53 but we know for a fact that validation result is not an error, because here we're checking for the instance of error, and if it is,
00:03:00 we wouldn't be able to reach this block of code anyway.
00:03:03 So for now, I'll leave it as it is, but later on, we'll very quickly fix this warning.
00:03:07 I think it also happens in a few other places, as you can see by these red lines right here, so fixing it won't be a problem.
00:03:13 But for now, since we know we're good, let's open up a try and catch block, In the catch, we can just return the handle error as error response.
00:03:22 And in the try, we need to fetch the question by the question ID.
00:03:26 By saying const question is equal to await question dot find by ID.
00:03:33 And to it, we can pass the question ID.
00:03:36 If a question doesn't exist, we can simply throw a new error, question not found.
00:03:42 Else, we can simply say question.views plus equal to one.
00:03:46 It is as simple as that.
00:03:48 And once you change that original question, you want to run the await question.save to save that change into the database.
00:03:56 And then we can return a success of true and data is the new number of views.
00:04:01 Cool, we have the action that should do its thing, but now a more important question here is when will we call that action?
00:04:08 And for this, I'll show you a couple of different approaches, because each one has its pros and cons, and I'll make sure you understand which one is the
00:04:17 best and why.
00:04:18 So for the first approach, we have to change the whole question details page into a client component or create a separate client component and use it directly
00:04:29 inside the question details page.
00:04:30 To see it in action, let's create a temporary client component called view.
00:04:35 We can do it within app, root, questions, and let's call it view.tsx.
00:04:42 Let's run RAFCE.
00:04:44 And let's immediately turn it into a client component by giving it a use client directive.
00:04:50 Now, this component will accept a question ID as a prop.
00:04:55 We already know that it is just a regular string.
00:04:57 So question ID of a type string, and it won't actually return anything.
00:05:02 So we don't want to see it on the screen.
00:05:04 We just want to make sure that it is there.
00:05:06 And then while it is there, we can create a new function, which it will call from within itself.
00:05:12 const handle increment is equal to an asynchronous function.
00:05:18 And we can call it as soon as this client component mounts by using the use effect.
00:05:25 and declaring it with an empty dependency array, like this.
00:05:31 And we can simply call the handleIncrement as soon as it loads.
00:05:36 So what will this handleIncrement do?
00:05:38 Well, let's simply call the action we created.
00:05:41 const result is equal to await increment views, which is coming from question actions.
00:05:50 And to it, we pass an object with a question ID, because that's what it expects right here.
00:05:55 Under params, a question ID.
00:05:58 Then we can have an if statement and check if result.success is true.
00:06:04 In that case, we can simply show a toast coming from hooks use toast.
00:06:11 with a title of something like success, and we can also add a description of views incremented.
00:06:18 Else, if it's not a success, we can show another type of toast, this time with a title of error, description of, maybe we have access to the error message here,
00:06:28 so we can say something like result.error?message, and we can also show a variant of destructive.
00:06:38 There we go.
00:06:39 So in a nutshell, this should be it.
00:06:41 Once we actually import this view component, components have to start with a capital letter, it'll then call this useEffect,
00:06:49 which will call this function, which will call the action, which will increment the number of question views.
00:06:56 So let's put it to the test by calling it within our question details.
00:07:01 So that's the questions ID page.
00:07:03 We can call it anywhere on the screen right here, maybe right here at the top.
00:07:08 by simply saying view coming from dot dot slash view.
00:07:13 And we need to pass to it a question ID equal to ID because it's coming directly from the params.
00:07:21 And if you refresh, you see success, views incremented.
00:07:25 And if you refresh one more time, you'll see the number of views as one right here.
00:07:30 Let me zoom that in so you can see it a bit better.
00:07:33 Number of eyes seen, one.
00:07:35 Now, why is it that you have to refresh one more time to be able to see the updated amount of views?
00:07:41 Why can't we see the views being updated the moment we visit the page?
00:07:45 The reason lies in how Next.js and server actions are working here.
00:07:49 Let me explain the flow of the application.
00:07:51 First, we have the initial page load.
00:07:54 That happens when a user visits the question details page, and the server then renders the page with the current view count.
00:08:01 This is because the page is a server component, so it's getting executed on the server.
00:08:05 Then, we implement the view count, only after the page is loaded, and a server action is called to increment it in the database.
00:08:13 This server action is called from the client side, meaning that only after the page has been rendered, the DOM has been created,
00:08:20 and a client called is made through the useEffect hook.
00:08:24 But we have the issue with the stale data.
00:08:26 Since the page was rendered and served to the client before the view count was incremented, that means that the user doesn't see the updated view count immediately.
00:08:34 Thus, we get to the delayed update, which means that the user would only be able to see the updated view count if they navigate it away and then they return
00:08:43 to the page or if they reload it.
00:08:45 Kind of defeating the purpose, right?
00:08:47 So what is the solution to this?
00:08:49 How do we see real-time updates?
00:08:51 Well, it's super simple.
00:08:53 Use something known as a RevalidatePath.
00:08:56 We can do that by heading back over to our question actions, scrolling down, and then right after saving the question, we can call RevalidatePath and pass
00:09:08 the path we want to revalidate.
00:09:09 It's going to come from routes, coming from constants, dot question, and question ID, which is the question details page we're currently on.
00:09:18 The revalidate path function allows us to revalidate the data for a specific path, ensuring that the user sees the most up-to-date information without
00:09:26 needing to navigate away or refresh the page.
00:09:29 So now if I go back, you can see that this question has three views, but if I click on it one more time, Immediately, we'll get five.
00:09:37 But why five?
00:09:39 Because we have a set of two eyes?
00:09:41 Well, no, that's not it.
00:09:42 It's just another issue that we have to fix.
00:09:45 You'll notice that every time you reload, The number of views will be increased by two instead of one.
00:09:51 There we go.
00:09:52 But this only happens in development mode.
00:09:55 In production, on the deployed site, you won't face this issue of a use effect firing twice.
00:10:00 Trust me, I read it on React Docs.
00:10:03 It says right here that my use effect runs twice when a component mounts.
00:10:07 When strict mode is on, in development, React runs setup and cleanup one extra time before the actual setup.
00:10:16 This is a stress test that verifies your effect's logic is implemented correctly.
00:10:21 Again, this isn't really a problem.
00:10:23 It happens only in development, but still, it's a bit inconvenient, isn't it?
00:10:27 Alongside that, we can also note down the time it takes to complete this request.
00:10:31 To do that, open up your Developer Tools.
00:10:34 head over to Network, and instead of selecting all, simply click Fetch and XHR Requests.
00:10:41 Keep the Inspect tab open, and open the details page of any of these questions.
00:10:45 If you do that, you'll see some requests being logged.
00:10:48 There's a main GET request for the entire page, as well as two POST requests due to use effect in development to update the view count.
00:10:56 firing our question action two times.
00:10:59 No down the timing of each one.
00:11:00 The main get function is here and takes about 200 milliseconds.
00:11:05 And then there's two post requests, which happen one after another, taking about 400 milliseconds each.
00:11:11 So obviously, Creating this client component and calling it that way is not the approach we want to go for.
00:11:18 So in the next lesson, we'll explore some better solutions.
00:11:22 But I want to leave this up to you as a challenge.
00:11:24 Take some time and think about it for a second.
00:11:27 What would be the best way to update the number of views or in other words, make a server action call within our page?
00:11:35 while also not blocking the UI, not having duplicate requests made, and just in general, do it after the entire UI gets loaded because we don't need to
00:11:45 see it immediately to waste that valuable network bandwidth.
00:11:49 As a challenge, think about it for a bit and then head over to Discord and let me know the solution you came up with.
00:11:55 In the next lesson, I'll show you mine.
00:11:57 Oh, and let's not forget to make a commit by saying increment question details.
00:12:06 views will at least implemented in one way.
00:12:11 Now is your turn to show me your way and then I'll show you mine.