Handling Reviews with React Context
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'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.