Course

Building A Server Action: Update a Product

So far, we've learned how to write and implement the first two major CRUD operations: Create and Read. We've created a new Product, and learned to read the data and it's related models from the database.

In this lesson, we'll learn to create an update operation. We'll create a new server action that updates a Product in the database.

Creating a New Server Action - Update a Product

Let's open our file once more and create a new server action called .

products.ts;

export async function updateProduct() {
  try {
    // todo: implement the updateProduct server action
  } catch (error) {
    return null;
  }
}

Think about what you need to update a Product. You need the Product ID to identify the Product you want to update. You also need the new data that you want to update the Product with.

The Prisma Client provides a method called that you can use to update a record in the database. The method is very similar to create. In fact, it will take the same data object that you used to create a new record.

The only difference is that we'll need a clause to identify the record that we want to update.

So the update is a bit of a combination of the create and read operations. You need to find the record first, then update the data.

Let's implement the server action.

export async function updateProduct(id: number, product: CreateProductInput) {
  try {
    const updatedProduct = await prisma.product.update({
      // where statement, just like reading a record
      where: { id },
      // data object is the exact same as 'create'.
      data: {
        name: product.name,
        description: product.description,
        price: product.price,
        category: product.category,
        images: {
          // delete previous images and create new ones
          deleteMany: {},
          create: product.images?.map((url) => ({ url })),
        },
      },
    });
    return updatedProduct;
  } catch (error) {
    return null;
  }
}
Insight

The statement is used to delete all the images associated with the Product before creating new ones. This is a pattern that can be used when updating a record that has a one-to-many relationship.

In this case, we're deleting all the images associated with the Product and creating new ones. This is a simple way to update the images associated with a Product.

If you want to update the images without deleting the old ones, you can remove the statement, or specify precisely which images you want to delete by providing more information:

images: {
  deleteMany: {
    id: {
      in: [1, 2, 3] // specify the IDs of the images you want to delete
    }
  },
},

In the server action, we're using the method to update the Product in the database. We're passing the Product ID as the clause to identify the Product we want to update.

Integrating the Update Product Server Action

Now that we've created the server action, let's integrate it into our application.

Let's start in the file. This is the file that chooses which component to render based on the path, and the file that reads the Product data from the database.

We'll take a close look at what's happening here:

export default async function Page({ params }: { params: { path: string[] } }) {

  // figure out the first and second part of the URL
  const method = params.path[0];
  const id = params.path[1];

  // If the method is 'new', render the AddProduct component
  if (method === "new") {
    return <AddProduct />;
  }
  // Figure out which component to render based on the method
  if (method === "edit") {
    return <AddProduct edit id={id}/>;
  }
  if (method === "delete") {
    return <DeleteProduct id={id} />;
  }

  // get the product by ID
  const product = await getProductById(parseInt(id));

  // If the product is not found, return a message
  if (!product) {
    return <div>Product not found</div>;
  }

}

Currently this is the way this component works:

  1. It reads the method and ID from the URL.i.e. will have and
  2. If the method is , it renders the component.
  3. If the method is , it renders the component with the prop set to .
  4. If the method is , it renders the component.
  5. If the method is none of the above, it reads the Product by ID and renders the component.

We'll need the product's information to update it. So we'll need to pass the product data to the component when the method is .

Which means we need to update the component to accept the data as a prop, and we need to render that component AFTER we fetch the data.

Let's update the file to pass the product and correct the order:

page.tsx
export default async function Page({ params }: { params: { path: string[] } }) {
  const method = params.path[0];
  const id = params.path[1];

  if (method === "new") {
    return <AddProduct />;
  }

  const product = await getProductById(parseInt(id));

  if (!product) {
    return <div>Product not found</div>;
  }

  if (method === "edit") {
    return <AddProduct edit id={id} product={product} />;
  }
  if (method === "delete") {
    return <DeleteProduct id={id} />;
  }
...
}

Let's open the component used to display the Update form. In our case, it's the same as the component.

Let's reopen the file and it to use the product being passed into it now.

AddProduct.tsx;

// define the type
export interface ProductEditProps extends Product {
  id: number;
  reviews: Review[];
  images: Image[];
}
// include the product data as a prop
export default function AddProduct({
  edit,
  id,
  product,
}: {
  edit?: boolean;
  id?: string;
  product?: ProductEditProps;
}) {}

Great! Now we're padding in the product. Our goal now is to make sure that the form is pre-filled with the product data when the method is .

Let's update the form states to pre-fill the data when the method is .

//

const [name, setName] = useState(product?.name || "");
const [price, setPrice] = useState(product?.price || 0);
const [description, setDescription] = useState(product?.description || "");
const [category, setCategory] = useState(product?.category || "");

const [images, setImages] = useState<string[]>(
  product?.images.map((i) => i.url) || [],
);

For each value in our state that used to simply have a default value, we're now using the product data if it exists.

Now, let's update the form submission to use the server action when the method is .

AddProduct.tsx;

const handleSubmit = async (e: FormEvent) => {
  e.preventDefault();
  // if the method is 'edit', update the product
  if (edit && product) {
    await updateProduct(product.id, {
      name,
      price,
      description,
      category,
      images,
    });
  } else {
    // else, create a new product
    await createProduct({
      name,
      price,
      description,
      category,
      images,
    });
  }
};

Redirecting

When the product is updated, we want to redirect the user back to the Product page. We can do this by using the hook from Next.js.

Let's import the hook and use it to redirect the user back to the Product page after the product is updated.

AddProduct.tsx

// import useRouter
import { useRouter } from "next/navigation";
...
export default function AddProduct({
  edit,
  id,
  product,
}: {
  edit?: boolean;
  id?: string;
  product?: ProductEditProps;
}) {
  // create a router instance
  const router = useRouter();
  ...
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (edit && product) {
      const updatedProduct = await updateProduct(product.id, {
        name,
        price,
        description,
        category,
        images,
      });
      if(updatedProduct){
        // redirect the user back to the product page
        router.push(`/product/view/${updatedProduct.id}`);
      }
    } else {
      await createProduct({
        name,
        price,
        description,
        category,
        images,
      });
      if (newProduct) {
        router.push(`/product/view/${newProduct.id}`);
      }
    }
  };
  ...
}

Conclusion

In this lesson, we learned how to create an update operation using the Prisma Client. We created a new server action called that updates a Product in the database.

We integrated the server action into our application by passing the product data to the component when the method is .

0 Comments

"Please login to view comments"

glass-bbok

Join the Conversation!

Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.

Upgrade your account
tick-guideNext Lesson

Building A Server Action: Delete a Product