The Problem Nextjs Solves

Daniel SimonsonDaniel Simonson
4 min read

React was great for making fast applications that allowed for interactivity. Static site generators like Gatsby, Jekyll etc. were good at making applications with quick load times and snappy SEO ratings. You could do one or the other but not both.

That was the problem Next.js solved, merging the benefits of single page applications with statically generated websites. A best of both worlds result.

Seamlessly, Next.js accomplished this by embedding client side code into server side data fetching, changing the way applications are written and, even more importantly, changed the user experience for the better.

Today I will explain a real implementation with Server Actions and a client form component working together.

The client part of this process comes in the form of a <form>. The key to making this work is using <form> element's intrinsic ability to pass methods to server side code with the action prop, invoking Server Actions.

Although the form element is working on the client side and has the 'use client' flag, it passes the method through the action attribute ('action=clientAction' as seen below) and ends up running that code on the server side with the 'use server' flag.

In the clientAction method the formData passed in is used as an argument for the handleTransaction Server Action ( handleTransaction(formData) ).

"use client";

import { useRef, JSX, SVGProps } from "react";
import handleTransaction from "@/app/actions/handleTransaction";
import { toast } from "react-toastify";
import { Label } from "@/components/ui/label";
import ToggleMode from "./ToggleMode";
import { motion } from "framer-motion";

export const Transaction = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const clientAction = async (formData: FormData) => {
    const { data, error } = await handleTransaction(formData);
    if (error) {
      toast.error(error);
    } else {
      toast.success("Transaction Created");
      formRef.current?.reset();
    }
  };

  return (
    <>
      <div className="bg-primary rounded-lg border p-6 w-full max-w-md flex flex-col gap-6">
        <div className="flex items-center justify-between">
          <h1 className="text-sm mr-2 md:text-2xl md:mr-0 font-bold">
            Budget Tracker
          </h1>
          <ToggleMode />
        </div>
      </div>
      <div className=" grid gap-4 w-full">
        <div className=" grid gap-2">
          <Label
            className="text-center py-4 text-2xl text-primary mt-7"
            htmlFor="transaction"
          ></Label>
          <form className="" ref={formRef} action={clientAction}>
            <div className="flex flex-col md:flex-row md:justify-around items-center gap-2">
              <input
                type="text"
                id="text"
                name="text"
                placeholder="Enter description..."
                className="border border-black rounded-md py-2 bg-input"
              />
              <input
                type="number"
                name="amount"
                id="amount"
                placeholder="Enter amount..."
                step="0.01"
                className="border border-black rounded-md py-2 bg-input"
              />
              <select
                id="type"
                name="type"
                className="border border-black rounded-md py-3 px-2 bg-input"
                required
              >
                <option value="expense">Expense</option>
                <option value="income">Income</option>
              </select>
              <motion.button
                whileHover={{ scale: 1.1 }}
                whileTap={{ scale: 0.9 }}
                className="cursor-pointer bg-primary text-black font-bold dark:text-white text-lg
              rounded-xl block mt-4 mr-0 mb-3 py-3 w-1/3 max-w-80 "
              >
                Add
              </motion.button>
            </div>
          </form>
        </div>
      </div>
    </>
  );
};

Now, I will examine the Server Action handleTransaction.

The key part in this 'use server' code is the action's ability to pass in the formData and then 'get' the name attribute's value for all the inputs in the form as seen below with the syntax formData.get(...)

const textValue = formData.get("text");
const amountValue = formData.get("amount");
const typeValue = formData.get("type");

See name attributes below.



           ************* inputs ********************

              <input
                type="text"
                id="text"
                name="text"
                placeholder="Enter description..."
                className="border border-black rounded-md py-2 bg-input"
              />

              <input
                type="number"
                name="amount"
                id="amount"
                placeholder="Enter amount..."
                step="0.01"
                className="border border-black rounded-md py-2 bg-input"
              />

               <select
                id="type"
                name="type"
                className="border border-black rounded-md py-3 px-2 bg-input"
                required
              >
                <option value="expense">Expense</option>
                <option value="income">Income</option>
              </select>

Then, if the userId exists, I convert the data into a negative expense or a positive income value and created a new record in the database.

const { userId } = auth();

  if (!userId) {
    return { error: "User not found" };
  }

  if (typeValue === "income") {
    newAmount = `${amountValue}`;
  } else if (typeValue === "expense") {
    newAmount = `-${amountValue}`;
  }
  const text: string = textValue.toString();
  const amount: number = parseFloat(newAmount.toString());

  try {
    const transactionData: TransactionData = await prisma.transaction.create({
      data: {
        text,
        amount,
        userId,
      },
    });

Finally, because I have updated the database, I purge the data cache and re-fetch the latest data with Next.js revalidatePath option, re-rendering with the new data.

  revalidatePath("/");
    return { data: transactionData };
  } catch (error) {
    return { error: "Transaction not added" };
  }

The entire Server action code is displayed below.

"use server";

import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/db";
import { revalidatePath } from "next/cache";

interface TransactionData {
  text: string;
  amount: number;
}

interface TransactionOutcome {
  data?: TransactionData;
  error?: string;
}
const handleTransaction = async (
  formData: FormData
): Promise<TransactionOutcome> => {
  const textValue = formData.get("text");
  const amountValue = formData.get("amount");
  const typeValue = formData.get("type");
  let newAmount = "";

  if (!textValue || textValue === "" || !amountValue) {
    return { error: "Text or amount is missing" };
  }
  if (!typeValue || typeValue === "") {
    return { error: "Expense or Income has not been identified" };
  }

  const { userId } = auth();

  if (!userId) {
    return { error: "User not found" };
  }

  if (typeValue === "income") {
    newAmount = `${amountValue}`;
  } else if (typeValue === "expense") {
    newAmount = `-${amountValue}`;
  }
  const text: string = textValue.toString();
  const amount: number = parseFloat(newAmount.toString());

  try {
    const transactionData: TransactionData = await prisma.transaction.create({
      data: {
        text,
        amount,
        userId,
      },
    });

    revalidatePath("/");
    return { data: transactionData };
  } catch (error) {
    return { error: "Transaction not added" };
  }
};

The demo for these code snippets is here

The github code is here

0
Subscribe to my newsletter

Read articles from Daniel Simonson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Daniel Simonson
Daniel Simonson

I am a software developer with a bachelor's degree in Computer Science. I've worked as a frontend React and Vue developer at various times as well as filling roles requiring full-stack development. I'm interested in full-stack / frontend JavaScript development using various frameworks and tools. I love coding and creating websites and apps. I focus on trying to get a little bit better than I was yesterday.