Course

Server Actions - Update

So far, we've learned how to write and implement the first two major CRUD operations: Create and Read. In this lesson, we'll learn how to implement the Update operation.

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.

Update

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.

Hint
monkey
  • The Product ID - to identify the Product you want to update.
  • The Data we want to update the product with

Let's implement the with it's arguments and some basic error handling:

products.ts;

// use what we need to apply an update
// to decide what arguments we need:
export async function updateProduct(productId: string, data: Partial<Product>) {
  try {
    // TODO: Implement the updateProduct server action
  } catch (error) {
    console.error(error);
    return null;
  }
}
Insight

Why do we use ?

We use to allow us to update only the fields we want to update.

We're telling TypeScript that the object may only contain some of the fields of the model.

Mongoose provides a method called that we can use to update a document in the database.

The method takes three arguments:

  1. The of the document you want to update.
  2. The you want to update the document with.
  3. An options object that allows you to configure the update operation.

Let's use this method to update the Product with the given with the given .

products.ts;

export async function updateProduct(productId: string, data: Partial<Product>) {
  // Make sure we connect to the database
  await dbConnect();
  try {
    const updatedProduct = await Product.findByIdAndUpdate(productId, data, {
      new: true,
    });

    return updatedProduct._id.toString();
  } catch (error) {
    console.error(error);
    return null;
  }
}

What's the option?

The option tells Mongoose to return the updated document after the update operation is complete.

If you don't provide this option, Mongoose will return the document as it was before the update operation.

Integrating the Update Product Server Action

Now that we've implemented 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 findProductById((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(id);
  const { reviews, averageRating } = await getReviewsAndRating(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

import Product from "@/lib/models/product";
...
export default function AddProduct({
  edit,
  id,
  product,
}: {
  edit?: boolean;
  id?: string;
  product?: Product;
}) {
  ...
}

Great! Now we're passing 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 || []);

What's happening here?

We're using the prop to pre-fill the form fields with the product data when the method is .

If the prop is not provided, we'll use the default values for the form fields.

Now that we've pre-filled the form with the product data, let's implement the update operation.

First, we'll import the server action in the component.

//
import { updateProduct } from "@/lib/actions/products";

Next, we'll implement the function to call the server action when the form is submitted.

We need to determine if the form is being used to create a new product or update an existing product. So we can use the prop to determine this.

//
const handleSubmit = async (e: FormEvent) => {
  e.preventDefault();
  try {
    // if the method is 'edit', update the product
    if (edit && product && id) {
      const productID = await updateProduct(id, {
        name,
        price,
        description,
        category,
        images,
      });
      router.push(`/product/view/${productID}`);
    } else {
      // else, create a new product
      const productID = await createProduct({
        name,
        price,
        description,
        category,
        images,
      });
      router.push(`/product/view/${productID}`);
    }
  } catch (error) {
    // show some toast or alert to the user
    console.error("Error creating product:", error);
  }
};

What's happening here?

We're using the prop to determine if the form is being used to create a new product or update an existing product.

If the method is , we call the server action with the product ID and the updated product data.

If the method is not , we call the server action to create a new product.

Conclusion

In this lesson, we learned how to implement the Update operation in our application.

We created a new server action called that updates a Product in the database.

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

We updated the component to pre-fill the form with the product data when the method is .

We updated the function to call the server action when the form is submitted.

In the next lesson, we'll learn how to implement the Delete operation.

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

Server Actions - Delete