<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Butch Imperial</title>
    <description>The latest articles on Forem by Butch Imperial (@butch_imperial_dd21d71158).</description>
    <link>https://forem.com/butch_imperial_dd21d71158</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3649203%2Fdcb49b1d-e88e-4b4a-b53c-a48651cd580f.png</url>
      <title>Forem: Butch Imperial</title>
      <link>https://forem.com/butch_imperial_dd21d71158</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/butch_imperial_dd21d71158"/>
    <language>en</language>
    <item>
      <title>Unified Create and Edit Form With NextJS</title>
      <dc:creator>Butch Imperial</dc:creator>
      <pubDate>Sun, 07 Dec 2025 11:32:37 +0000</pubDate>
      <link>https://forem.com/butch_imperial_dd21d71158/unified-create-and-edit-form-with-nextjs-1g6p</link>
      <guid>https://forem.com/butch_imperial_dd21d71158/unified-create-and-edit-form-with-nextjs-1g6p</guid>
      <description>&lt;p&gt;In this topic, we’re going to create a form that can handle both adding new data and updating existing data. This is just a guide to help you build a unified form for both creating and editing entries.&lt;/p&gt;

&lt;p&gt;Using a single, &lt;strong&gt;unified form&lt;/strong&gt; brings several benefits: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It reduces code duplication&lt;/li&gt;
&lt;li&gt;Makes maintenance easier&lt;/li&gt;
&lt;li&gt;Ensures consistency in data handling and validation&lt;/li&gt;
&lt;li&gt;Improves the user experience by providing a familiar interface for both creating and editing&lt;/li&gt;
&lt;li&gt;Speeds up development since you don’t need to build separate forms for each action. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, we’ll walk through how to create a form for adding and editing branches&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Create the necessary types and functions.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, we will create the instance of the &lt;strong&gt;branch&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export type BranchInstance = {
   id: string,
   branch_name: string,
   location: string,
   img: string,
   google_coordinate: string
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;, we’ll create a &lt;strong&gt;Zod validation schema&lt;/strong&gt; for the branch instance. You can use any validation library you prefer, but in this example, we’ll use Zod. The goal is to validate every field in the branch instance, so all of its fields will be included in the Zod object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";

// validator
export const BranchSchema = z.object({
  branch_name: z.string().min(1, "Branch name is required"),
  location: z.string().min(1, "Location is required"),
  google_coordinate: z.string().min(1, "Google Map embed link is required"),
  img: z.string().min(1, "One Image of the place is required")
});

// Infer the TypeScript type from the Zod schema
type BranchSchemaType = z.infer&amp;lt;typeof BranchSchema&amp;gt;;

// for errors type
export type zodBranchErrorsType = {
  [K in keyof BranchSchemaType]?: string[];
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Third&lt;/strong&gt;, we’ll create a save response object. This should include the status, message, errors, and the returned data or value. We’ll use this structure as the response for useActionState.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export type BranchSaveResponse = {
   status: number,
   data: BranchInstance
   message: string,
   errors?: {
     fieldErrors?: zodBranchErrorsType;
     formErrors?: string[];
  },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can do the &lt;strong&gt;generic version&lt;/strong&gt; for reusability&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export type SaveResponse&amp;lt;T&amp;gt; = {
  status: number;
  data: T;
  message: string;
  errors?: {
    fieldErrors?: Record&amp;lt;string, any&amp;gt;; // Or a generic type if you want
    formErrors?: string[];
  };
};

export type BranchSaveResponse = SaveResponse&amp;lt;BranchInstance&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fourth&lt;/strong&gt;, create a function that &lt;strong&gt;converts form data&lt;/strong&gt; into a regular JavaScript object so it’s easier to work with and manipulate. Paste the code to AI to explain further.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export function formDataToObject(fd: FormData) {
  // This object will hold the final key-value pairs
  const out: Record&amp;lt;string, FormDataEntryValue | FormDataEntryValue[]&amp;gt; = {};

  // This set keeps track of all keys that are arrays (ending with "[]")
  const arrayKeys = new Set&amp;lt;string&amp;gt;();

  // Loop through all key-value pairs in the FormData
  for (const [rawKey, value] of fd.entries()) {
    // Check if the field name ends with "[]", which means it's an array field
    const isArray = rawKey.endsWith("[]");

    // Remove the "[]" from the key for cleaner object property names
    const key = isArray ? rawKey.slice(0, -2) : rawKey;

    // Remember that this key is meant to represent an array
    if (isArray) arrayKeys.add(key);

    // If this key already exists in the object, we need to merge the new value
    if (key in out) {
      const prev = out[key];

      // If it's already an array, add the new value to it
      // Otherwise, convert the previous single value into an array
      out[key] = Array.isArray(prev) ? [...prev, value] : [prev, value];
    } else {
      // Otherwise, just set the key to this value
      out[key] = value;
    }
  }

  // Ensure that all keys marked as arrays are stored as arrays,
  // even if only one value was present in the FormData
  for (const key of arrayKeys) {
    const v = out[key];
    out[key] = Array.isArray(v) ? v : v === undefined ? [] : [v];
  }

  // Return the final, cleaned-up object
  return out;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Create a reusable component.
&lt;/h2&gt;

&lt;p&gt;Since the branch form contains fields that is text and image, we will create a component for each to maximize re-usability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, we have the input component for text, number, email, and password fields. In our case, we only need the text input, but you can reuse this component for number, email, or password as well. The key props are &lt;strong&gt;name&lt;/strong&gt;, which is used by the server to identify the field; &lt;strong&gt;defaultValue&lt;/strong&gt;, which sets the field’s initial value; and &lt;strong&gt;errors&lt;/strong&gt;, which is an array if it’s empty, that means there are no validation error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export function InputField({
  label,
  name,
  defaultValue,
  errors,
  placeholder,
  type = "text", // default is text
}: {
  label: string,
  name: string,
  defaultValue?: string | number | null, 
  errors?: string[],
  placeholder?: string, // optional placeholder
  type?: "text" | "number" | "email" | "password", 
}) {
  return (
    &amp;lt;div className="flex flex-col gap-2"&amp;gt;
      &amp;lt;label htmlFor={name} className="label"&amp;gt;{label}&amp;lt;/label&amp;gt;
      &amp;lt;input
        type={type}
        id={name}
        name={name}
        className={"input border " + ((errors?.length ?? 0) &amp;gt; 0 ? " input-error" : "input-neutral border-gray-300")}
        defaultValue={defaultValue as string | number}
        placeholder={placeholder}
      /&amp;gt;
      {errors?.map((error, index) =&amp;gt; (
        &amp;lt;p key={index} className="text-sm text-red-500 mt-1"&amp;gt;{error}&amp;lt;/p&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Next&lt;/strong&gt;, we’ll create an &lt;strong&gt;image component&lt;/strong&gt;. This component will upload the dropped image to Appwrite storage using an action function, and it will only store the &lt;strong&gt;image ID returned by Appwrite&lt;/strong&gt;. The data sent to the server is just a &lt;strong&gt;string not the actual image file&lt;/strong&gt;. This helps protect the server and avoids hitting file upload limits.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;edit mode&lt;/strong&gt;, the component keeps the &lt;strong&gt;previous image ID&lt;/strong&gt; while allowing the user to upload a new one. The key detail is that if the user removes the previous image, its ID is &lt;strong&gt;saved in a hidden input field&lt;/strong&gt; for example, if the image field is named image, the removed image ID is stored as imageMarkedID. You can paste this code into AI tools to help understand how it works.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Note: This method of saving files (images, PDFs, DOCX, etc.) is safer than uploading directly to the server. The key is how the file is sent and stored. *&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const UploadIcon: React.FC&amp;lt;{ className?: string }&amp;gt; = ({ className }) =&amp;gt; (
    &amp;lt;svg
        className={className}
        stroke="currentColor"
        fill="none"
        strokeWidth="2"
        viewBox="0 0 24 24"
        strokeLinecap="round"
        strokeLinejoin="round"
        xmlns="http://www.w3.org/2000/svg"
    &amp;gt;
        &amp;lt;path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"&amp;gt;&amp;lt;/path&amp;gt;
        &amp;lt;polyline points="17 8 12 3 7 8"&amp;gt;&amp;lt;/polyline&amp;gt;
        &amp;lt;line x1="12" y1="3" x2="12" y2="15"&amp;gt;&amp;lt;/line&amp;gt;
    &amp;lt;/svg&amp;gt;
);

const CloseIcon: React.FC&amp;lt;{ className?: string }&amp;gt; = ({ className }) =&amp;gt; (
    &amp;lt;svg
        className={className}
        stroke="currentColor"
        fill="currentColor"
        strokeWidth="0"
        viewBox="0 0 512 512"
        height="1em"
        width="1em"
        xmlns="http://www.w3.org/2000/svg"
    &amp;gt;
        &amp;lt;path d="M289.94 256l95-95A24 24 0 00351 127l-95 95-95-95a24 24 0 00-34 34l95 95-95 95a24 24 0 1034 34l95-95 95 95a24 24 0 0034-34z"&amp;gt;&amp;lt;/path&amp;gt;
    &amp;lt;/svg&amp;gt;
);

function ImageUploader({
    label, name, errors, initial
}: {
    label: string,
    name: string,
    errors: string[],
    initial?: string /* if provided, then the image is in update state */
}) {
    const [image, setImage] = useState&amp;lt;string | null&amp;gt;(null);
    const [isDragging, setIsDragging] = useState&amp;lt;boolean&amp;gt;(false);
    const fileInputRef = useRef&amp;lt;HTMLInputElement&amp;gt;(null);
    const [imageId, setImageId] = useState&amp;lt;string | null&amp;gt;(null);

    // used only if `initial` is provided
    const [markedDeletedId, setMarkedDeletedId] = useState&amp;lt;string[]&amp;gt;([])

    const processFile = useCallback(async (file: File) =&amp;gt; {
        if (file &amp;amp;&amp;amp; file.type.startsWith('image/')) {

            /** upload to upwrite */
            toast.loading("Uploading main image", { id: "uploadMainImage" })

            const { success, id, error } = await uploadImageAction(file);

            if (success) {
                setImageId(id)
                console.log("ImageID: ", imageId)
                toast.success("Uploaded successfully!", { id: "uploadMainImage" })

                const reader = new FileReader();
                reader.onloadend = () =&amp;gt; {
                    setImage(reader.result as string);
                };

                reader.readAsDataURL(file);
            } else {
                console.log(error)
                toast.error(error, { id: "uploadMainImage" })
                setImage(null)
                if (fileInputRef.current) {
                    fileInputRef.current.value = '';
                }

            }

        } else {
            // You can add more robust error handling here
            alert('Invalid file type. Please upload an image.');
        }
    }, []);

    const handleDragOver = useCallback((e: React.DragEvent&amp;lt;HTMLDivElement&amp;gt;) =&amp;gt; {
        e.preventDefault();
        e.stopPropagation();
        setIsDragging(true);
    }, []);

    const handleDragLeave = useCallback((e: React.DragEvent&amp;lt;HTMLDivElement&amp;gt;) =&amp;gt; {
        e.preventDefault();
        e.stopPropagation();
        setIsDragging(false);
    }, []);

    const handleDrop = useCallback((e: React.DragEvent&amp;lt;HTMLDivElement&amp;gt;) =&amp;gt; {
        e.preventDefault();
        e.stopPropagation();
        setIsDragging(false);
        const files = e.dataTransfer.files;
        if (files &amp;amp;&amp;amp; files.length &amp;gt; 0) {
            processFile(files[0]);
        }

    }, [processFile]);

    const handleClick = () =&amp;gt; {
        fileInputRef.current?.click();
    };

    const handleFileChange = (e: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
        const files = e.target.files;
        if (files &amp;amp;&amp;amp; files.length &amp;gt; 0) {
            processFile(files[0]); // displays the image to UI and upload it to appwrite
        }
    };

    const handleRemoveImage = async (e: React.MouseEvent&amp;lt;HTMLButtonElement&amp;gt;) =&amp;gt; {
        e.stopPropagation();

        // if initial is provided, then we are in update mode, defer the deletion
        if (initial) {
            // save the marked delete to a state
            setMarkedDeletedId([...markedDeletedId, imageId])

            setImage(null)
            if (fileInputRef.current) {
                fileInputRef.current.value = '';
            }
            setImageId(null)

            return
        }

        toast.loading("Deleting Main image", { id: "deleteMainImage" })

        const { success, error } = await deleteImageAction(imageId)

        if (success) {
            setImage(null)
            if (fileInputRef.current) {
                fileInputRef.current.value = '';
            }
            setImageId(null)

            toast.success("Deleted successfully!", { id: "deleteMainImage" })
        } else {

            toast.error(error, { id: "deleteMainImage" })
        }

    };

    React.useEffect(() =&amp;gt; {
        async function init() {
            if (initial) {

                try {

                    const url = await getImageAction(initial)
                    setImageId(initial)
                    setImage(url)

                } catch (e) {
                    console.log("Error parsing initial value for image uploader:", e)
                    throw new Error("Something went wrong wrong getting the image url", e)
                }
            }
        }
        init();
    }, []);

    console.log("Rendered ImageUploader with imageId:", imageId, "errors:", errors)

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;label htmlFor={name} className="label pb-2"&amp;gt;{label}&amp;lt;/label&amp;gt;

            {
                // render the markId value hidden so the backend can accept it
                markedDeletedId?.map(item =&amp;gt;
                    &amp;lt;input key={item} type="text" name={`${name.endsWith("[]") ? name.slice(0, -2) : name}MarkedId[]`} defaultValue={item} hidden /&amp;gt;
                )}

            &amp;lt;input
                value={imageId || ''}
                type="text"
                name={name}
                readOnly
                hidden /&amp;gt;
            &amp;lt;input
                type="file"
                ref={fileInputRef}
                onChange={handleFileChange}
                accept="image/*"
                className="hidden"
            /&amp;gt;
            {image ? (
                &amp;lt;div className="relative group w-full h-80 rounded-lg overflow-hidden"&amp;gt;
                    &amp;lt;img src={image} alt="Upload preview" className="w-full h-full object-cover" /&amp;gt;
                    &amp;lt;div className="absolute inset-0 hover:bg-black/20 bg-opacity-0 group-hover:bg-opacity-40 transition-all duration-300 flex items-center justify-center"&amp;gt;
                        &amp;lt;button
                            onClick={handleRemoveImage}
                            className="absolute top-3 right-3 bg-white rounded-full p-2 text-gray-700 hover:bg-gray-200 hover:text-black opacity-0 group-hover:opacity-100 transition-opacity duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-white"
                            aria-label="Remove image"
                            type='button'
                        &amp;gt;
                            &amp;lt;CloseIcon className="w-6 h-6" /&amp;gt;
                        &amp;lt;/button&amp;gt;
                    &amp;lt;/div&amp;gt;

                    {/** Errors */}

                &amp;lt;/div&amp;gt;
            ) : (
                &amp;lt;div
                    onClick={handleClick}
                    onDragOver={handleDragOver}
                    onDragLeave={handleDragLeave}
                    onDrop={handleDrop}
                    className={`w-full h-80 rounded-lg border-2 border-dashed flex flex-col items-center justify-center cursor-pointer transition-all duration-300
            ${isDragging ? 'border-indigo-600 bg-indigo-50' : 'border-gray-300 hover:border-indigo-500 hover:bg-gray-50'} ${(errors?.length &amp;gt; 0 ? "border-red-600" : "")}`}
                &amp;gt;
                    &amp;lt;div className="text-center p-8"&amp;gt;
                        &amp;lt;UploadIcon className={`w-16 h-16 mx-auto mb-4 transition-colors duration-300 ${isDragging ? 'text-indigo-600' : 'text-gray-400'}`} /&amp;gt;
                        &amp;lt;p className="text-lg font-semibold text-gray-700"&amp;gt;Drag &amp;amp; drop your image here&amp;lt;/p&amp;gt;
                        &amp;lt;p className="text-gray-500 mt-1"&amp;gt;or &amp;lt;span className="text-indigo-600 font-medium"&amp;gt;click to browse&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                        &amp;lt;p className="text-xs text-gray-400 mt-4"&amp;gt;Supports: JPG, JPEG, PNG, GIF&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            )}

            {/** errors */}
            {errors?.map((error, index) =&amp;gt; (
                &amp;lt;p key={index} className="text-sm text-red-500 mt-1"&amp;gt;{error}&amp;lt;/p&amp;gt;
            ))}
        &amp;lt;/div&amp;gt;

    );
};


export default ImageUploader;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that the &lt;strong&gt;preparation is ready&lt;/strong&gt;, we will create a component for creating and updating.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create a Single Form Component.
&lt;/h2&gt;

&lt;p&gt;Now we’ll create the BranchForm. It should accept an optional data parameter, which will contain the values to be updated when editing. Make sure to also include the BranchInstance you created earlier, since it represents the structure of the form data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export function BranchForm({ data = null
}: { data?: BranchInstance | null }) {  
    return ()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside BranchForm, &lt;strong&gt;create a constant named initial&lt;/strong&gt; that inherits the properties of BranchSaveResponse. Initialize the fields you need so the form has a default response state to work with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const initial: BranchSaveResponse = {
   status: 0, // initial data
   value: data? data : {
      id: '',
      branch_name: '',
      location: '',
      img: '',
      google_coordinate: ''
   },
   message: "",
   errors: {}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below the initial constant, &lt;strong&gt;set up useRouter and useActionState&lt;/strong&gt;. These will handle the logic for running actions and processing the response returned by useActionState. The states are for &lt;strong&gt;pending&lt;/strong&gt;, &lt;strong&gt;failed to save&lt;/strong&gt;, and &lt;strong&gt;save successfully&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const router = useRouter();
const [formState, actionState, isPending] = useActionState(saveBranch, initial);
if (isPending) {
   toast.loading(`Saving Branch...`, { id: "toast" })
} else if (formState?.status === 400) {
   toast.error("Failed to save branch", { id: "toast" })
}else if ([200,201].includes(formState?.status)){
   toast.success("Branch saved successfully", { id: "toast" })
   router.push("/admin/branches")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add the form fields&lt;/strong&gt; and set the form’s action to the useActionState action. The hidden id input is important because it stores the branch ID, if it’s empty, the form is in create mode; if it has a value, it means we’re updating an existing branch.&lt;/p&gt;

&lt;p&gt;You’ll also notice that we’re &lt;strong&gt;passing both the default values and the form state errors&lt;/strong&gt;. This ensures the fields always stay updated, and if any errors occur, they’ll be displayed next to their corresponding inputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form action={actionState} className="mt-4 flex flex-col gap-4"&amp;gt;
   &amp;lt;input type="hidden" name="id" value={initial.value.id} /&amp;gt;
   &amp;lt;InputField2 label="Branch Name" name="branch_name" defaultValue={formState.value.branch_name} errors={formState.errors?.fieldErrors?.branch_name}/&amp;gt;
   &amp;lt;InputField2 label="Location" name="location" defaultValue={formState.value.location} errors={formState.errors?.fieldErrors?.location}/&amp;gt;
   &amp;lt;InputField2 label="Google Coordinate" name="google_coordinate" defaultValue={formState.value.google_coordinate} errors={formState.errors?.fieldErrors?.google_coordinate} /&amp;gt;
   &amp;lt;ImageUploader label="Image" name="img" initial={formState.value.img} errors={formState.errors?.fieldErrors?.img} /&amp;gt;
   &amp;lt;button type="submit" className="btn btn-primary w-fit mt-4 btn-lg self-end" disabled={isPending}&amp;gt;Submit&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;overall code&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export function BranchForm({
    data = null
}: { data?: BranchInstance | null }) {

    const initial: BranchSaveResponse = {
        status: false,
        value: data? data : {
            id: '',
            branch_name: '',
            location: '',
            img: '',
            google_coordinate: ''
        },
        message: "",
        errors: {}
    }

    const router = useRouter();

    const [formState, actionState, isPending] = useActionState(saveBranch, initial);

    if (isPending) {
        toast.loading(`Saving Branch...`, { id: "toast" })
    } else if (formState?.status === false &amp;amp;&amp;amp; formState?.message) {
        toast.error("Failed to save branch", { id: "toast" })
    }

    useEffect(() =&amp;gt; {

        if (formState?.status === true) {
            toast.success("Branch saved successfully", { id: "toast" })
            router.push("/admin/branches")
        }

    }, [formState?.status, router]);


    return (
        &amp;lt;form action={actionState} className="mt-4 flex flex-col gap-4"&amp;gt;
            &amp;lt;input type="hidden" name="id" value={initial.value.id} /&amp;gt;

            &amp;lt;InputField2 label="Branch Name" name="branch_name" defaultValue={formState.value.branch_name} errors={formState.errors?.fieldErrors?.branch_name}/&amp;gt;
            &amp;lt;InputField2 label="Location" name="location" defaultValue={formState.value.location} errors={formState.errors?.fieldErrors?.location}/&amp;gt;
            &amp;lt;InputField2 label="Google Coordinate" name="google_coordinate" defaultValue={formState.value.google_coordinate} errors={formState.errors?.fieldErrors?.google_coordinate} /&amp;gt;
            &amp;lt;ImageUploader label="Image" name="img" initial={formState.value.img} errors={formState.errors?.fieldErrors?.img} /&amp;gt;
            &amp;lt;button type="submit" className="btn btn-primary w-fit mt-4 btn-lg self-end" disabled={isPending}&amp;gt;Submit&amp;lt;/button&amp;gt;
        &amp;lt;/form&amp;gt;
    );

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Create the action function for creating and editing.
&lt;/h2&gt;

&lt;p&gt;Set up the &lt;strong&gt;function&lt;/strong&gt; returning a promise of BranchSaveRespone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export async function saveBranch(prevState: BranchSaveResponse, formData: FormData) : Promise&amp;lt;BranchSaveResponse&amp;gt; {
   ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the function, &lt;strong&gt;check&lt;/strong&gt; if the current user has &lt;strong&gt;permission&lt;/strong&gt; to save a branch. If not, throw an error to stop execution and return early. This step is &lt;strong&gt;optional&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// implement your own security here, this is just a simple one
const cookie = await verifySessionCookie()
    if(!cookie.valid)
        throw new Error('Invalid Action')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Convert&lt;/strong&gt; the form data into a JavaScript object. Make sure it matches the BranchInstance structure and also includes any fields with MarkedID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const entries = formDataToObject(formData) as BranchInstance &amp;amp; {
    imgMarkedId?: string[];
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Separate&lt;/strong&gt; the id from the converted form data and &lt;strong&gt;validate&lt;/strong&gt; the remaining data using Zod. You can either create a custom validation function or use the Zod schema for BranchInstance directly. In this example, I created one.&lt;/p&gt;

&lt;p&gt;If the &lt;strong&gt;validation&lt;/strong&gt; fails, return a response containing the message, a status of 400, the data, and the validation errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// seperate the id and the data
const { id, ...data } = entries

// return validation error if there is an invalid value
const validated_result = zod_validate&amp;lt;BranchInstance&amp;gt;(BranchSchema, data)

// If the validated status results to false, return the errors
if (!validated_result.status)
   return {
      status: 400,
      message: "validation error occured",
      value: entries,
      errors: validated_result.errors
   }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;strong&gt;update mode&lt;/strong&gt;, which id exist, check if any MarkedIDs exist for images, and delete them if they do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// only in update mode delays the deletion of image
if (id)
    // delete existing images from appwrite storage
    deleteImageFromField(entries?.imgMarkedId as string | string[])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Save&lt;/strong&gt; the validated data to Firebase along with its id. If the id is an empty string, the function is in create mode; otherwise, it’s in update mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;saveDocItem('branches', validated_result.value, id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Revalidate&lt;/strong&gt; the page which you will redirect after saving the data&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;revalidatePath("/admin/branches");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Return&lt;/strong&gt; status updated as ok if id exists, otherwise 201 for created&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return { status: id ? 200 : 201, value: entries, message: "Branch saved successfully" };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;strong&gt;finished&lt;/strong&gt;, a &lt;strong&gt;single form&lt;/strong&gt; that can create and update a &lt;strong&gt;branch&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Form Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// create mode
&amp;lt;BranchForm /&amp;gt;

// update mode
const data = fetchBranch(id) as BranchInstance
&amp;lt;BranchForm data={data}/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
