ProNextJS
    Level up with State Management with Next.js App Router

    I respect your privacy. Unsubscribe at any time.

    This is a free tutorial

    In exchange for your email address, you'll get full access to this and other free ProNextJS tutorials.

    Why? First and foremost, your inbox allows us to directly communicate about the latest ProNextJS material. This includes free tutorials, NextJS Tips, and periodic update about trends, tools, and NextJS happenings that I'm excited about.

    In addition to the piles of free NextJS content, you'll get the earliest access and best discounts to the paid courses when they launch.

    There won't be any spam, and every email you get will have an unsubscribe link.

    If this sounds like a fair trade, let's go!

    Loading...

    Transcript

    Okay, well, I hope that wasn't too difficult. Let's jump into the code and see how I solve this problem. Now, the easiest part of this really starts at the store. You just need to define a new slice for our reviews. So I need to bring in the review type.

    And we need to define our slice. So we're going to have our review state. It's either going to have an array of reviews or it's going to have null. And we're going to start off at null. Then we're going to create our review slice. We're going to give it the name reviews. We're going to give it an initial state of our initial reviews. Again, just null. And then we're going to give it a new action called set reviews.

    That's just going to take the array of reviews and set the state in the store to our initial reviews. All right, let's go take that review slice and add it to our store. Now we go down here to our root state.

    So now we see that our Redux store, which is maintained at the layout level, contains both the cart slice, awesome, and our review slice. And that's going to make it interesting as we get to the point where we need to initialize our reviews.

    OK, I think the last thing we need to do is export some actions. So we'll export set reviews as well as a selector to use the reviews. As I say, this is probably the easiest part. So now let's go into the product page.

    So now we're looking at the product detail page. It's a React server component that takes an ID. And the first thing it does is it gets the important data. It gets the product that we're interested in. It gets a list of products we want to show in the related products.

    And it does both those asynchronously, which is a really nice thing about a React server component. Now, the data that's really interesting to us is, of course, product.reviews. And we have to figure out a way to get the product reviews into the store.

    Now, why couldn't we do something like this? We just dispatch just like we did in the store provider, but this time with the product reviews using that awesome set reviews action creator that we just built. Here's the problem. We don't know where the store is.

    The store is over here in the client component and is passed down via context. You can't use context in a React server component. And the store is not defined as a global, so we can't just access the store as a global. And even if we could access the store, React server components only run on the server.

    So there's not going to be any way for the React server component to do the initialization on the client the same way it does here. So we need a client component to go and do the initialization. Lucky for us, we have two different client components that access the reviews. But now, actually, let's make this a little bit simpler on ourselves.

    And for the moment, let's just forget about average rating. Let's just deal with reviews. All right, now let's go over into our reviews. And first, let's get use store from React Redux. That's going to give us that store that we can't get in the page.

    Now let's get the store, but you need to give it the type of the store, so that's why we need root state. So let's go bring in root state. So far, so good. So let's just dispatch that set reviews. So far, so good.

    So we've got our store. Let's just go dispatch set reviews. But of course, we know this is going to be dynamic, so we also want to use the reviews that's in the store. So really, what we have here are the initial reviews.

    So let's send our initial reviews, and then we'll go and use that use reviews hook to get the current reviews. So bring in user reviews, and we'll get our reviews from that.

    All right, let's give it a try. That seemed to work. Let's go check the SSR, and it looks like the reviews are in the SSR output, so pretty good. And let's actually go from page to page and see what happens.

    So we jump from page to page. That actually seems to work. All right, let's add a new review, and I'll set it to one and submit review. Okay, that's kind of weird. So I hit submit review, and nothing seemed to happen. So let me hit refresh. Okay, so my data actually got in there, which is great,

    but it didn't actually update. So what actually happened there? So what happened was when we called setReviewText and setReviewRating to just reset the review text and review rating,

    we re-rendered the reviews component, which called that dispatch and set the store to our initial reviews.

    Uh-oh. Okay, so how about we use like useEffect, and we'll just wrap this in a useEffect. Now we know that's only going to get run once. Let's see.

    All right, I hit refresh, and I noticed that the reviews actually don't come up right away. They actually kind of just kind of pop like that, and if we look in the HTML,

    we actually don't see our reviews anymore. Uh-oh. Huh. So what's happening here? So what's happening here is that the useEffect is only ever triggered on the client after the first render.

    So we're not getting any server-side rendered output because the server either has null or an empty array of products or whatever the initial kind of starter state of your reviews was. So not great. Can't use useEffect. What else can we do? Well, we can take a look at the store and see if it has reviews,

    and if it already has reviews, then we don't dispatch anything, right? That makes sense. So we'll look at the store. We'll call this handy getState function. That'll get us our state.

    We'll take a look at reviews, and we'll say if we don't have reviews, then we'll dispatch our set of the initial reviews. So far, so good. Okay. Hit save. Now if I refresh, looks good.

    There's none of that jumpy thing that we had with useEffect, so we are getting the result in the SSR, but if I go from route to route, you can actually see that as I go from route to route, the product changes, the price changes, whatever, get all that good stuff, but the reviews don't change. So why is that?

    Well, remember, our Redux store is effectively global since we defined it at the layout level with our store provider. That means as you go from route to route to route, that state is actually maintained from route to route,

    even though the page component, the RSC is getting rerun, the reviews component is getting rerun. Everything's getting rerun, but we look at the global state of that store, and we say, well, are there reviews?

    If there are, yes, then don't do that dispatch. So we have to figure out a way to track if we are getting re-rendered for a new route that is outside the state of the store. To do that, I'm just going to use a simple useRef,

    and we'll have it track if we are initialized or not. Because reviews is going to get rebuilt every time, we're going to get a new initialized with a new current every time we go from route to route, and it's always going to start at false. Now, let's check that.

    And so we're not initialized. We're going to set the reviews using that dispatch of the initial reviews. Of course, we have to say that we are initialized, so we have to set the current to true.

    Okay, so far, so good. So let's go back over to Arc, try again. Hit submit review.

    And it actually did work, and if I go from page to page, that actually works, too. But we don't actually dispatch the set reviews after we've gone and submit a review.

    But the issue is, if we look down here at our ad review action, we don't actually dispatch the set review, so let's go and do that. So we need to bring in use dispatch, and then use dispatch to give ourselves a dispatch, and we've got set reviews already.

    So all we need to really do is just do dispatch set reviews with the output of the action. Okay, let's give it a try. Hit refresh. Hello again. Perfect. Awesome.

    Hit refresh. Let's just double check the SSR, and we have our reviews in there, so we are good to go. Now, one last thing, we've got to go and implement on our average rating. So let's just bring average rating back and see what happens. So just go and uncomment out average rating.

    All right, now that we've brought our average rating back, let's go and implement on average rating to make sure that it gets the reviews from the store and not from its initial reviews.

    So we use these reviews. We'll bring that in from the store. Let's save. We'll refresh, and there's something weird going on here.

    There's like a little popping. Okay, let's see if there's anything in the console. Okay. Well, nothing in the console, but I don't like that popping. So what's happening here?

    Well, what's happening here is it's looking at reviews, and it's initially getting null for the reviews, even though it should be set because it's getting from the store, but here's what's happening. So both of these components, Add to Cart and Reviews, are peers of each other.

    They're in the same hierarchy in the JSX, and we don't really know what the render order is. In fact, actually, Reviews is probably getting rendered second, but according to the React documentation, you should never depend on the render order of your components.

    So how do we fix this? Well, my contention is that you could use the same exact initialization code in both Reviews and Average Rating.

    Why? Because product.reviews is the same reference in both places, so you'll just be setting the reference once, and it doesn't really matter which one runs in which order. Let's give it a try.

    So we'll go in and take our initialization code from here, the order Average Rating, paste it in there, and now let's fix any issues.

    And finally, we need View Store. Hit Save. Let's give it a go.

    Hit Refresh, and now it's solid as a rock. In fact, actually, let's go over to our SSR Output and make sure that our 4.5 is out there. Find 4.5. There we go. Average Rating of 4.5. Perfect.

    So that means that we're actually calculating the average rating on the SSR as well as outputting the product reviews. You might be freaking out saying, whoa, don't repeat yourself. You're putting the same initialization code in two different components. That's not great.

    Okay, fine. What you could do instead is have a client component, possibly up here at the top of the JSX, that would be an initializer component that would take the product reviews and then do that initialization.

    And because it's at the top of the hierarchy, we're going to assume that it's going to get rendered first. Probably a decent assumption, but that would be one way to avoid this replication of this initialization code.

    All right, well, I hope you enjoyed getting into all of the different variations around this initialization of the store and how to do it in a way that works for both server-side rendering as well as changing between routes as well as updating the data in place.

    Those are the kind of checks that you'll need to make in your application as you implement on patterns like this. But if you want to avoid some of that, I got to tell you, the Zustand version that we're going to do next is actually a lot cleaner.

    So join me in the next exercise as we look at how to reimplement all of this using Zustand.