ProNextJS
    Loading
    solution

    Handling Reviews with React Context

    Jack HerringtonJack Herrington

    Let's start by creating the ReviewsContext component at app/products/[id]/ReviewsContext.tsx.

    Creating a ReviewsContext Component

    ReviewsContext will be a client component because it needs to use context. Like before, we're going to bring in createContext and useState to manage our state, but this time we're going to bring in the Review type:

    // inside app/products/[id]/ReviewsContext.tsx
    
    "use client";
    import React, { createContext, useState } from "react";
    import { type Review } from "@/api/types";
    

    We'll create our useReviewsState hook, which will take the initial reviews like we did with the initialCartState before:

    const useReviewsState = (initialReview: Review[]) =>
    	useState<Review[]>(initialReview);
    

    Next, we'll create the ReviewsContext which will use the output of the useReviewsState hook:

    export const ReviewsContext = createContext<ReturnType<
    	typeof useReviewsState
    > | null>(null);
    

    For accessing this context, we'll define another custom hook called useReviews. If it doesn't find a context, then it will return an error.

    export const useReviews = () => {
    	const reviews = React.useContext(ReviewsContext);
    	if (!reviews) {
    		throw new Error("useReview must be used within a ReviewProvider");
    	}
    	return reviews;
    };
    

    Finally, we'll create a ReviewsProvider that takes the initial reviews and uses the useReviewsState hook to initialize some state that we'll then pass down to any children using the ReviewsContextProvider:

    const ReviewsProvider = ({
    	reviews: initialReviews,
    	children,
    }: {
    	reviews: Review[];
    	children: React.ReactNode;
    }) => {
    	const [reviews, setReviews] = useReviewsState(initialReviews);
    
    	return (
    		<ReviewsContext.Provider value={[reviews, setReviews]}>
    			{children}
    		</ReviewsContext.Provider>
    	);
    };
    
    export default ReviewsProvider;
    

    Now we can bring the ReviewsProvider into our page.

    Updating the Page to use ReviewsContext

    Inside of page.tsx, we'll instantiate the ReviewsProvider with the product reviews inside of the ProductDetail return:

    export default async function ProductDetail({
      const addReviewAction = async (text: string, rating: number) => {
        return reviews || [];
      };
    
      return (
        <ReviewsProvider reviews={product.reviews}>
          ...
    

    It's important to note that the only thing going to the client is the reviews, and none of the immutable data.

    With ReviewsContext in place, we can remove the reviews prop from our Reviews and AverageRating components since they will now get this information from context:

    <Reviews addReviewAction={addReviewAction} />
    ...
    <AverageRating />
    

    Updating the AverageRating Component

    Over in AverageRating.tsx, we need to update the component to get the reviews from context.

    To do this, we'll import the useReviews hook and remove the reviews prop in favor of getting the reviews from the hook:

    "use client";
    import { useReviews } from "./ReviewsContext";
    
    export default function AverageRating() {
      const [reviews] = useReviews();
      ...
    

    We'll follow a similar process for the Reviews component.

    Updating the Reviews Component

    Inside of Reviews.tsx, import the useReviews hook and remove the reviews prop.

    This time, we'll bring in reviews and setReviews from the hook:

    export default function Reviews({
      addReviewAction,
    }: {
      addReviewAction: (text: string, rating: number) => Promise<Review[]>;
    }) {
      const [reviews, setReviews] = useReviews();
      ...
    

    Now in the form for adding a review, we'll use setReviews to update our reviews based on the output of calling addReviewAction with the text and rating:

    ...
    <form
      onSubmit={async (evt) => {
        evt.preventDefault();
        setReviews(await addReviewAction(reviewText, reviewRating));
        setReviewText("");
        setReviewRating(5);
      }}
    >
    

    Checking Our Work

    Back in the browser, when we navigate between routes, we can see that the reviews are changing from product to product.

    We are also able to submit a new review and see the average rating update.

    For example, adding a rating of 1 for the Wizard T-Shirt will lower the average rating to 3.3.

    As one more check, let's check the the server output to make sure it includes reviews. This is important for SEO considerations.

    When viewing the page source in the browser, searching for a a bit of the review text allows us to confirm that reviews are being rendered on the server:

    <div class="mt-1 text-sm other-classes">
    	I really like this t-shirt, the wizard design is unique and the fabric is
    	soft. The only downside is that it&#x27;s a bit too tight around the neck.
    </div>
    

    We've successfully integrated state management for reviews into our ecommerce app using React's Context API.

    Next, we'll explore how to re-implement this app using Redux for both client-side and server-side rendering.

    Transcript

    All right, the first thing I'm going to do is I'm going to go create a ReviewsContext component, and it's going to be a client component because it needs to use context. Just like before, we're going to bring in CreateContext and UseState to manage our state, but this time we're going to bring in Review as opposed to the cart which we brought in before.

    First up, we're going to define our UseReviewsState hook, and much like our UseCartState hook, we are going to take the initial reviews like we did with the initial cart before. So next up, we're going to create our context. Our context will have the output of that UseReviewsState hook.

    Then we'll create a custom hook called UseReviews that we'll use to get that context. And if we didn't find a context, then we'll just throw an error saying, hey, you need to add a context. And then finally, we'll create a ReviewsProvider which takes those initial reviews and uses that UseReviewsState hook to initialize some state that we'll then

    pass down to any children using that ReviewsContextProvider. All right, now let's go bring it into our page. And then right at the top of our JSX, we will instantiate it with the product reviews. So we'll just give it just the reviews. That's

    really important. The only thing going out to the client in this model is the reviews, not any of the immutable data. And we're doing that by only passing the reviews as the property. Now we can get rid of some things. We can go down here to our reviews and get rid of the reviews

    because we're going to get those via context. We're also going to remove them from the average ratings. Now let's go do the average ratings first since it's simpler. We'll bring in UseReviews. We won't get our reviews as a property, but we'll get them from the hook. Easy peasy.

    And we'll get rid of this type. We'll do something similar in our reviews component. We'll bring in the UseReviews. We'll get rid of the reviews as a prop. And then we'll get reviews as well as set reviews from that hook. Now let's use that set reviews by going down here. And then we can set reviews, the output of our ad review action. That's going

    to give us our new set of reviews. So let's take a look. So now as we navigate along from route to route, let's make sure that those reviews change. Yep, those reviews are changing from route to route. That's great. So now we really like this wizard shirt. So we'll say Great Wizard

    Shirt. Except we don't think so. We'll call it one. We'll hit Submit Review. Now we notice the average rating has dropped down to 3.3. So we didn't like it that much, but I guess we did because we said it's great, whatever. And we've gone and we've added that review. So that's looking

    awesome. And one more final check. Let's make sure that the output of the server includes our reviews. We want that because of search engine optimization. We want to make sure the reviews are in the server output. So we'll take a little bit of the review, go into the page source. All right, we've got our

    page source up. Let's go take a look. We have our wizard design is unique. So that means that our reviews are being output during server-side rendering, which is great for search engine optimization. So that actually concludes our React implementation of our e-commerce app. Next up, we

    are going to re-implement this using Redux and using it safely on both the server and the client.