Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: Managing Data in a Next.js Finance Application

In this lab, you will add to existing expense and income pages in a Next.js finance application. This will include supporting CRUD operations within the entire stack. By the end of this lab, you will have a deeper understanding of server components, client components, server actions, API route handlers, and dynamic routes.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 1h 30m
Published
Clock icon Jul 02, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the Guided: CRUD Operations for a Next.js Finance Application lab.

    Throughout this lab, you will learn how to add CRUD operations to a Next.js application using both server-side and client-side rendering. You will build React server components (specific to the Next.js framework) and React client components (a format you may already be familiar with, but if not, do not worry you will review it). Additionally, you will incorporate these components into a Next.js layout and learn more about Next.js pages. By the end of this lab, you should be familiar with API route handlers, React server versus client components, CRUD operations, dynamic routes, and more in a Next.js application.

    To start, you have been given the bones of a Next.js finance application. The application has expenses, income, transactions (home), and budget planning pages started and accessible on the running application through a basic navigation bar. When you are finished with this lab, users will be able to see, add, delete, and update transactions and budget agenda.

    A basic understanding of Next.js’s unique file structure, page and layout interface, and App router may be helpful to complete this lab, but is not necessary. It would also benefit you to have some prior and basic experience with React, JavaScript, HTML, and JSX. Details on Next.js paradigms and design patterns around server and client components and API calls for CRUD operations will be given to provide context for your implementations in each step.


    There is a solution directory that you can refer to if you are stuck or want to check your implementations at any time. Keep in mind that the solution provided in this directory is not the only solution, so it is acceptable for you to have a different solution so long as the application functions as intended.

    You can launch your application by entering npm run dev in the Terminal. The results of your code can be seen in the Web Browser tab if you visit http://localhost:3000 or visit the following link: {{localhost:3000}}

  2. Challenge

    React Server Components And Client Components

    React Server Components and Client Components

    Next.js is a full stack framework built on top of React. This means Next.js is capable of hosting React server components in addition to what you know and love, React client components. All components in a Next.js application are React server components by default. So far, all the components in the finance application are React server components since you do not find ’use client’ directive at the top of any component file.

    Server components are around to improve the developer experience by simplifying work processes like:

    1. Page loads! Server components allow page loads to be faster initially because there is no waiting on JavaScript to load. This is because server components are pre rendered at build-time or on revalidation. This will also result in a small client-size JavaScript bundle size since many components are not included on the client-side.

    2. Optimizations! Metadata on server components and layouts in Next.js allows for search engine optimization. This is helpful for locating an application or website through Google. Server components also can assist with Image optimizations.

    3. Simpler code structure! Server components can access backend services like databases directly while still safeguarding sensitive data like API keys and access tokens that are available on the server. This also means you can create external API calls in server components without worrying about sharing too much information about the client.

    However, not all components in Next.js applications can be server components. Since server components are only built at deploy/run time and on revalidation (the process of pulling data in case it has changed), components and pages that require a lot of interactivity and where data changes often are best suited to be client components. This is because you cannot have event listeners like onClick or onChange in a server component. Server components also do not support React hooks like useEffect or useState since these hooks trigger prefetching of data and rerenders. When you attempt to use a hook or event listener in a server component, you will get an error notifying you that they only work in a component marked with ’use client’. You can transform any server component to a client component by placing ’use client’ at the top of the file in Next.js. Just remember that by changing a component to a client component, you cannot have metadata for that component to help with search engine optimization.

  3. Challenge

    Server Component Data Fetching

    React Server Component Data Fetching

    Since a React server component is built at deploy/run time and has access to the server, data fetching can happen in the component itself. Usually, in a React client component, hooks are used to fetch data and render the page afterwards.

    This application will serve for several examples of how Next,js accomplishes data fetching and rendering, so you can plan on the Expenses page remaining a server component for the purpose of this lab. You will convert the Income page to a client component to compare the two pathways for displaying and fetching data in a future step.

    The “database” currently lives locally in app/api. In each subdirectory, you are provided 2 JSON files called mockData.json and originalData.json. The mockData file contains initial data and is meant to represent a "database" for storing data and changes made to it will persist across sessions. If you ever want to "reset" the data in mockData, simply paste originalData into mockData. The transactions directory is where you will find transactions of type income, expense, and savings. The budget-plan directory is where you will find budget plans around certain categories of type income, expense, and savings. You can think of the JSON provided in these files as SQL results when asking for the entire transactions and budget-plan tables.

    In this step, you will fetch expenses in the Expenses page component and display expenses.

    To begin, create an asynchronous function called getExpenses in app/(ui)/expenses/page.jsx that returns only the transactions of type expense.

    Example Answer For Fetching Expenses
    async function getExpenses() {
      const file = await fs.readFile(
        process.cwd() + "/app/lib/transactions/data/mockData.json",
        "utf-8"
      );
      const transactions = JSON.parse(file);
      return transactions.filter((transaction) => transaction.type === "expense");
    }
    

    For the above code, do not forget to add the following import: import { promises as fs } from "fs";. In the real world, you would have a SQL database and could query the database using SQL in the getExpenses method instead of filtering through an array in JavaScript. This is only possible because it is a React server component.

    Next, you want to display the expenses you can now get through your getExpenses method in your ExpensesPage component. To accomplish this, you will need to make the component asynchronous, await your data fetch, and create a simple html table to show the expenses.

    Example Answer For Displaying Expenses
    export default async function ExpensesPage() {
      const expenses = await getExpenses();
      return (
        <div>
          <h1>Expenses</h1>
          <table>
            <tr>
              <th>Date</th>
              <th>Amount</th>
              <th>Category</th>
            </tr>
            {expenses.map((expense) => {
              return (
                <tr>
                  <td>{expense.date}</td>
                  <td>{expense.amount}</td>
                  <td>{expense.category}</td>
                </tr>
              );
            })}
          </table>
        </div>
      );
    }
    
  4. Challenge

    Client Component Data Fetching

    React Client Component Data Fetching

    Next.js still supports React client components as usual, and these type of components are what is suggested when a component is going to be very interactive or the data for the component will be changing often. One might suggest the Expenses and Income pages actually be client components since your end goal would be to have these page support all CRUD behavior.

    In order to compare the two types of components, you will create an API route handler for getting income transactions, convert the Income component to a client component, and display income transactions on the page in this step.

    API Route Handlers are functions that are executed when a user requests an API route, and these are available in your traditional idea of a public backend with API calls. These route handlers deal with incoming HTTP requests for a specific route and respond with the necessary data.

    Unlike typical full stack applications that have a separate API, Next.js has the functionality to host API Route Handers within your app structure. You will utilize this functionality by adding an API route handler for getting income transactions.

    Instructions (1) Export an async function named `GET` that returns all transactions of type `income` in `app/api/transactions/route.js`.
    export async function GET(req) {
      const file = await fs.readFile(
        process.cwd() + "/app/lib/transactions/data/mockData.json",
        "utf-8"
      );
      const transactions = JSON.parse(file);
      const incomeTransactions = transactions.filter(
        (transaction) => transaction.type === "income"
      );
      return new Response(JSON.stringify(incomeTransactions));
    }
    

    For the above code, do not forget to add the following import: import { promises as fs } from "fs";. As mentioned before, if you had a SQL database, you would be performing data queries here.

    With this method there is an API route handler available at http://localhost/{port}/api/transactions where {port} is the same port that your frontend is running on. You can test this by visiting the url above in the Web Browser and you will see the income transaction JSON.

    Next, you will want to convert the IncomePage component to a client component so you can use hooks to get the income transaction data available for you at the route created above. To start the conversion, use the ’use client’ directive at the beginning of app/(ui)/income/page.jsx. This tells the Next.js compiler that this page is a react client component.

    Now, you will write an asynchronous function to get data from the API route handler in app/(ui)/income/page.jsx.

    Example Answer For Fetching Income
    async function getIncome() {
      const res = await fetch("http://localhost:3000/api/transactions");
      const incomeJSON = await res.json();
      return incomeJSON;
    }
    

    Lastly, you will display the income transactions served at http://localhost/{port}/api/transactions on the Income page.

    Example Answer For Displaying Income Since you cannot await the return of the data fetch, you will use a state to maintain the income items. This will allow us to set the items when they are received. As a reminder, asynchronous components are not allowed in client components which is why you are using a hook.
    export default function IncomePage() {
      const [incomeItems, setIncomeItems] = useState([]);
      useEffect(() => {
        getIncome().then((income) => {
          setIncomeItems(income);
        });
      });
    
      return (
        <div>
          <h1>Income</h1>
          <table>
            <thead>
              <tr>
                <th>Date</th>
                <th>Amount</th>
                <th>Category</th>
                <th>Notes</th>
              </tr>
            </thead>
            <tbody>
              {incomeItems.map((incomeTransaction) => {
                return (
                  <tr key={incomeTransaction.id}>
                    <td>{incomeTransaction.date}</td>
                    <td>{incomeTransaction.amount}</td>
                    <td>{incomeTransaction.category}</td>
                    <td>{incomeTransaction.notes}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      );
    }
    

    Do not forget to add the following import: import { useState, useEffect } from "react";.

    — ## Refactor Transaction End Point

    Above, you created a transaction API route handler that returns income transactions. The suggestion is to refactor this end point to take url search parameters to filter to a specific type of transaction (instead of always returning only income transactions). This will create a more versatile end point that aligns closers with RESTful practices.

    In app/api/transactions/route.js’s GET method, pull off search params from the request url and get type off of them. Instead of filtering transactions by a hard coded string, “income”, compare to the search parameters type.

    Example Answer to Filtering by Search Param Type
    import transactions from "../../lib/transactions/data/mockData";
    import { NextResponse } from "next/server";
    
    export async function GET(req) {
      const { searchParams } = new URL(req.url);
      console.log(searchParams.get("type"));
      const incomeTransactions = transactions.filter((transaction) =>
        searchParams.get("type")
          ? transaction.type === searchParams.get("type")
          : true
      );
      return NextResponse.json(incomeTransactions);
    }
    

    You can test this by playing with the http://localhost/{port}/api/transactions (where {port} is the same port that your frontend is running on) and visiting the url iterations in the Web Browser to see different transaction JSON. For example, you can visit http://localhost/{port}/api/transactions, http://localhost/{port}/api/transactions?type=income, http://localhost/{port}/api/transactions?type=expense and http://localhost/{port}/api/transactions?type=none

    Update the IncomePage component to reach out to http://localhost:{port}/api/transactions?type=income for data. Remember your port is most like 3000, but it is wherever the Next.js application is running locally.

    Example Answer For New Fetch Address on Income Page
    async function getIncome() {
      const res = await fetch("http://localhost:3000/api/transactions?type=income");
      const incomeJSON = await res.json();
      return incomeJSON;
    }
    
    --- ## Loading Page

    Now that you have a data fetch that happens on the client side, you can add a loading page to delineate that data is being fetched. This is also useful for come server components, but not any of the ones in the finance application yet.

    A Loading component already exists for you in app/(ui)/components/loading.jsx. In your IncomePage component add a state to manage when data is loading, and if that state indicates you have not received data, render the Loading component.

    Example of Loading State in IncomePage Component
    "use client";
    import { useState, useEffect } from "react";
    import Loading from "../components/loading";
    
    async function getIncome() {
      const res = await fetch("http://localhost:3000/api/transactions?type=income");
      const incomeJSON = await res.json();
      return incomeJSON;
    }
    
    export default function IncomePage() {
      const [incomeItems, setIncomeItems] = useState([]);
      const [loading, setLoading] = useState(true);
      useEffect(() => {
        getIncome().then((income) => {
          setIncomeItems(income);
          setLoading(false);
        });
      });
    
      if (loading) {
        return <Loading />;
      }
    
    …
    }
    

    You should briefly see the Loading component before the Income table appears now. In a future step, you will see how loading.jsx in the app directory will automatically show a Loading component without managing states in React server components, but it is necessary in client components like IncomePage.

  5. Challenge

    Creating and Displaying New Transactions

    Creating and Displaying New Transactions

    You should now have a finance application that will read transactions from a “database” and display two different types on an Expenses page and Income page. By the end of this step, you will also be able to create transactions from these pages and display new transactions as they are created.

    Creating Transactions Server Action

    You are going to begin by creating a server action that will be used to create a transaction from a form later. Server actions are asynchronous functions executed on the server. They are defined with the React ”use server” directive at either the top of a function or a file to mark all exports of the file as server actions. Both client and server components can call server actions, however client components can only use module level server actions (server actions exported from a separate file).

    Finish implementing the createTransaction server action in app/lib/actions.js. This action will be responsible for adding a transaction to the “database”. In your case, this will look like creating a new transaction from formData and using fs to write to /app/lib/transactions/data/mockData.json. The method already fetches all the transactions for you. You may also notice the function accepts a transactionType as an argument, this will come in handy later and you can ignore it for now.

    Example Answer For Create Transaction Server Action
    export async function createTransaction(transactionType, formData) {
      const transactions = await getTransactions();
      const transaction = {
        id: transactions.length + 1,
        date: formData.get("date"),
        amount: formData.get("amount"),
        category: formData.get("category"),
        notes: formData.get("notes"),
        type: transactionType ? transactionType : formData.get("type"),
      };
      transactions.push(transaction);
      try {
        await fs.writeFile(
          process.cwd() + "/app/lib/transactions/data/mockData.json",
          JSON.stringify(transactions)
        );
        console.log("New transaction successfully added!");
      } catch (error) {
        console.log(`Error adding new transaction: ${error}`);
        throw new Error("Failed to add transaction");
      }
    }
    

    Working to Display New Expense Items

    After creating the server action that will create a new expense item, you will render a form component on the Expense page and call the action. The form component you will use already exists in app/components/server-action-server-component-add-transaction-form. If you examine the HTML, you will see the <form> already is calling your server action through the action attribute. Import this component in your Expense page and call it after the table. On this page, the transactionType will be ”expense” and the transactionPath will be ”/expenses”.

    Example Answer For Using the ServerActionServerComponentAddTransactionForm
    import AddTransactionForm from "../components/server-action-server-component-add-transaction-form";
    
    …
    
    export default async function ExpensesPage() {
      const expenses = await getExpenses();
      return (
        <div>
          <h1>Expenses</h1>
          <table>
            …
          </table>
          <AddTransactionForm
            transactionType={"expense"}
            transactionPath={"/expenses"}
          />
        </div>
      );
    }
    

    Now you should see a form to create expense items when you visit https:localhost/{port}/expenses in the Web Browser tab. However, you may notice if you fill out the fields and submit the form, you do not see your new expense item appear.

    This is because as a server component, the Expense page does not rerender unless its cached data “expires”. Server components, that are statically rendered, only cache data in a Content Delivery Network during build time or revalidation. Revalidation can be used as a tool to basically label the cache as “expired”, leading to a new data fetch. In Next.js, you can revalidate data on demand by revalidating a path.

    Add the following code in the createTransaction server action after writing new data to your “database”. This code will revalidate your expenses and transactions (home) paths. You will also need to add this import at the top of the file: import { revalidatePath } from "next/cache";.

    if (transactionPath === "/expenses" || transactionPath === "/") {
        revalidatePath(transactionPath);
      }
    

    Visit https:localhost/{port}/expenses in the Web Browser tab again. When you submit the form now, you should see the page refresh and be able to locate your new expense.

    Working to Display New Income Items

    One disadvantage of server components, like the Expense page, is that they have to go through a hard refresh to revalidate and cache new data. However, with a client component you can use states to manage your data and refresh individual components as those states change. The Income page is a client component that is already using state to manage income items. In this step, you will leverage a form component very similar to the last one to create new income items and display them without a hard refresh.

    In addition to the ServerActionServerComponentAddTransactionForm used in the last step, a client version of the same component exists in app/components/server-action-client-component-add-transaction-form. Since ServerActionClientComponentAddTransactionForm is a client component it can use React hooks. If you compare ServerActionClientComponentTransactionForm and ServerActionServerComponentAddTransactionForm you will notice that the client component has a setTransactions prop and its form action awaits the creation of a new transaction and uses setTrasactions to update the transaction list. To be frank, setTransactions is designed to be a setter from the useState React hook.

    Now, use ServerActionServerComponentAddTransactionForm in your Income page component.

    Example Answer For Using the ServerActionClientComponentAddTransactionForm
    import AddTransactionForm from "../components/server-action-client-component-add-transaction-form";
    
    …
    
    export default async function IncomePage() {
      const [incomeItems, setIncomeItems] = useState([]);
    
      …
      
      return (
        <div>
          <h1>Expenses</h1>
          <table>
            …
          </table>
          <AddTransactionForm
            transactionType={"expense"}
            transactionPath={"/expenses"}
            setTransactions={setIncomeItems}
          />
        </div>
      );
    }
    

    Now you should see a form to create income items when you visit https:localhost/{port}/income in the Web Browser tab. When you fill out the form you should notice that an error occurs indicating that incomeItems is no longer an array. To fix this error, you must return transactions filtered to only your transactionType (or all transactions if a type is not specified) from the createTransaction server action now. Once you add this return statement, you should see your income list update without a hard refresh.

  6. Challenge

    Deleting Transactions

    Deleting Transactions

    Your finance application will now display transactions and create new transactions. By the end of this step, you will be able to delete transactions from the application.

    Deleting Transactions Server Action

    Finish implementing the deleteTransaction server action in app/lib/actions.js. This action will be responsible for deleting a transaction to the “database”. This action takes an id, transactionPath, and transactionType. The list of transactions is already available for you, you need to find the transaction with id, delete it, and write the new transaction list to the mockData.json file.

    Like in the previous step, you will use this server action in the IncomePage client component and ExpensesPage server component. To make it so the pages reflect the appropriate transactions in the server components, revalidate the expenses and home paths after deleting. To allow client components to update state data after calling this server action, return the transactions filtered based on their respective transaction type.

    Example Answer For Delete Transaction Server Action
    export async function deleteTransaction(id, transactionPath, transactionType) {
      const transactions = await getTransactions();
      const dataIndex = transactions.findIndex((item) => item.id == id);
    
      if (dataIndex == -1) {
        console.log(`Transaction with ${id} not found`);
        return transactions;
      }
    
      transactions.splice(dataIndex, 1);
    
      try {
        await fs.writeFile(
          process.cwd() + "/app/lib/transactions/data/mockData.json",
          JSON.stringify(transactions)
        );
        console.log(`Transaction with id ${id} successfully deleted`);
      } catch (error) {
        console.log(`Error deleting transaction: ${error}`);
        throw new Error("Failed to delete transaction");
      }
    
      if (transactionPath === "/expenses" || transactionPath === "/") {
        revalidatePath(transactionPath);
      }
      return transactions.filter((transaction) =>
        transactionType ? transaction.type === transactionType : true
      );
    }
    

    Add a Delete Button to Expenses Table

    In the ExpensePage component, create another column in the table to display delete buttons. Every row will have a delete button wrapped in a form and the column should have a blank header. The form action will be the deleteTransaction server action binded to the row’s expense id, the transactionPath “/expenses”, and the transactionType ”expense”.

    Example Answer For Delete Button Column in Expenses
    import { deleteTransaction } from "@/app/lib/actions";
    
    …
    
    export default async function ExpensesPage() {
      const expenses = await getExpenses();
      return (
        <div>
          <h1>Expenses</h1>
          <table>
            <thead>
              <tr>
                <th>Date</th>
                <th>Amount</th>
                <th>Category</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {expenses.map((expense) => {
                const deleteExpenseById = deleteTransaction.bind(
                  null,
                  expense.id,
                  "/expenses",
                  "expense"
                );
                return (
                  <tr>
                    <td>{expense.date}</td>
                    <td>{expense.amount}</td>
                    <td>{expense.category}</td>
                    <td>
                      <form action={deleteExpenseById}>
                        <button type="submit">Delete</button>
                      </form>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          <AddTransactionForm
            transactionType={"expense"}
            transactionPath={"/expenses"}
          />
        </div>
      );
    }
    

    Visit https:localhost/{port}/expenses in the Web Browser and delete an expense to test out your new code.

    Add a Delete Button to Income Table

    In the IncomePage component, create another column in the table to display delete buttons. Every row will have a delete button wrapped in a form and the column should have a blank header.

    Unlike the Expenses page. The delete income transaction form action for the Income page will be an asynchronous function that calls deleteTransaction with the income transaction id, transactionPath “/income”, and transactionType ”income”. In this case, you do not have to bind the server action to any parameters because you can call it directly.

    As in the previous step, also update your income transaction list inside of your form action’s asynchronous function.

    Example Answer For Delete Button Column in Expenses
    import { deleteTransaction } from "@/app/lib/actions";
    
    …
    
    export default async function IncomePage() {
      const [incomeItems, setIncomeItems] = useState([]);
    
      …
      
     return (
        <div>
          <h1>Income</h1>
          <table>
            <thead>
              <tr>
                <th>Date</th>
                <th>Amount</th>
                <th>Category</th>
                <th>Notes</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {incomeItems.map((incomeTransaction) => {
                return (
                  <tr key={incomeTransaction.id}>
                    <td>{incomeTransaction.date}</td>
                    <td>{incomeTransaction.amount}</td>
                    <td>{incomeTransaction.category}</td>
                    <td>{incomeTransaction.notes}</td>
                    <td>
                      <form
                        action={async () => {
                          const transactions = await deleteTransaction(
                            incomeTransaction.id,
                            "/income",
                            "income"
                          );
                          setIncomeItems(transactions);
                        }}
                      >
                        <button type="submit">Delete</button>
                      </form>
                    </td>
                  </tr>
                );
             })}
            </tbody>
          </table>
          <AddTransactionForm
            transactionPath={"/income"}
            transactionType={"income"}
            setTransactions={setIncomeItems}
          />
        </div>
      );
    }
    

    Visit https:localhost/{port}/income in the Web Browser and delete an expense to test out your new code.

  7. Challenge

    Updating a Transaction

    Editing an Expense

    The last thing you will add to the finance application in this course is the ability to edit an expense.

    Editing an Expense Server Action

    Finish implementing the updateExpense server action in app/lib/actions.js. This action will be responsible for saving any expense item updates to the “database”. This action is going to be called from a form so it accepts formData as a parameter. Take note of the id parameter as well for finding the expense in the “database”.

    Remember to revalidate the /expense/[id] path relevant to the updated expense. This will be helpful when you add a form to the Expense page next.

    Example Answer For Update Expense Server Action
    export async function updateExpense(id, formData) {
      const transactions = await getTransactions();
      const dataIndex = transactions.findIndex((item) => item.id == id);
    
      if (dataIndex == -1) {
        console.log(`Expense with ${id} not found`);
        return transactions;
      }
      const expense = transactions[dataIndex];
    
      const inputData = {
        date: formData.get("date"),
        amount: formData.get("amount"),
        category: formData.get("category"),
        notes: formData.get("notes"),
      };
    
      transactions[dataIndex].date = inputData.date;
      transactions[dataIndex].amount = inputData.amount;
      transactions[dataIndex].category = inputData.category;
      transactions[dataIndex].notes = inputData.notes;
    
      try {
        await fs.writeFile(
          process.cwd() + "/app/lib/transactions/data/mockData.json",
          JSON.stringify(transactions)
        );
    
        console.log("Expense successfully updated!");
      } catch (error) {
        console.log(`Error saving expense item}`);
        throw new Error("Failed to update expense item");
      }
    
      revalidatePath(`/expenses/${id}`);
    }
    

    Link Expenses on the Expenses Page

    On the Expenses page, link each expense to its corresponding expense/[id] route. Add a Details column to the table that contains links to all of the individual expenses using Next.js’s Link component.

    Example Answer For Details Column in Expenses
    import { deleteTransaction } from "@/app/lib/actions";
    
    …
    
    export default async function ExpensesPage() {
      const expenses = await getExpenses();
      return (
        <div>
          <h1>Expenses</h1>
          <table>
            <thead>
              <tr>
                <th>Date</th>
                <th>Amount</th>
                <th>Category</th>
                <th></th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {expenses.map((expense) => {
                const deleteExpenseById = deleteTransaction.bind(
                  null,
                  expense.id,
                  "/expenses",
                  "expense"
                );
                return (
                  <tr>
                    <td>{expense.date}</td>
                    <td>{expense.amount}</td>
                    <td>{expense.category}</td>
         <td>
                      <Link href={`/expenses/${expense.id}`}>Details</Link>
                    </td>
                    <td>
                      <form action={deleteExpenseById}>
                        <button type="submit">Delete</button>
                      </form>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          <AddTransactionForm
            transactionType={"expense"}
            transactionPath={"/expenses"}
          />
        </div>
      );
    }
    

    Form to Edit an Expense on Expense Page

    Create a form in HTML to edit an expense in app/(ui)/expenses/[id]/page.jsx. This form should call the server action update expense. Remember to bind the expense id to the server action.

    Example Answer For Expense Form
    export default async function Expense({ params }) {
      const expense = await getExpense(params.id);
      const updateExpenseById = updateExpense.bind(null, expense.id);
      return (
        <div>
          <form action={updateExpenseById}>
            <input name="date" type="date" defaultValue={expense.date} required />
    
            <input
              name="amount"
              type="number"
              step="0.01"
              defaultValue={expense.amount}
              required
            />
    
            <input
              name="category"
              type="text"
              defaultValue={expense.category}
              required
            />
    
            <input name="notes" type="text" defaultValue={expense.notes} />
    
            <button type="submit">Update Expense</button>
          </form>
        </div>
      );
    }
    

    For reference, individual or dynamic routes are denoted in Next.js by a folder name enclosed in square brackets. This is what makes it possible to replace id with a specific expense id available and you can access it in the server or client component on the params prop.

    Hooray! You now can edit expense items in the finance application.

  8. Challenge

    Looking Forward

    In this lab, you flushed out the Expenses and Income pages with data. This required creating Next.js server actions and route handlers to perform data operations, writing HTML and JSX to show data in tables, and learning how to execute server code in client and server components.

    By now, you should be familiar with the difference between server and client components as well as the advantages and disadvantages of each type of component. You also should have knowlegde about Next.js server actions, API route handlers, and how to visit nested routes using Link.

    The finance application is coming along, and in the Guided: Daisy UI and Tailwind CSS to Style a Next.js Finance Application you will take off from where this lab left off to create a cohesive design for the application. The upcoming lab will also cover Next.js Streaming, Image and Font optimization, static vs dynamic rendering, and accessibility.

Jaecee is an associate author at Pluralsight helping to develop Hands-On content. Jaecee's background in Software Development and Data Management and Analysis. Jaecee holds a graduate degree from the University of Utah in Computer Science. She works on new content here at Pluralsight and is constantly learning.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.