Next.js Discord

Discord Forum

useActionState form

Unanswered
Lakeland Terrier posted this in #help-forum
Open in Discord
Lakeland TerrierOP
I want to make a reusable form that takes a string as prop that determines what action is called.

"use client";
import { useActionState } from "react";
import * as actions from "@/actions";
import { useState } from "react";

interface Props {
  id: number;
  exerciseActionName: keyof typeof actions;
}

export default function ExerciseForm({ id, exerciseActionName }: Props) {
  const _action = actions[exerciseActionName];

  const [error, action, isPending] = useActionState(_action.bind(null, id), {
    errors: {},
  });

  const [quantity, setQuantity] = useState<number | "">("");

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.currentTarget.value;
    if (value === "" || /^\d*\.?\d*$/.test(value)) {
      setQuantity(value === "" ? "" : parseFloat(value));
    }
  };

  return (
    <form action={action} className="flex flex-end flex-col gap-y-2">
      <div className="form-group flex items-center gap-x-2">
        <input
          type="text"
          inputMode="numeric"
          name="quantity"
          id={JSON.stringify(id)}
          value={quantity === "" ? "" : quantity}
          onChange={handleInputChange}
          className="number-input"
          autoComplete="off"
        />
        <button
          type="submit"
          disabled={isPending}
          className="purchase-button"
        >
          +
        </button>
        {isPending && <p>...</p>}
        {error.errors._form ? (
          <div className="rounded text-red bg-red-200 border error-message-icon-wrapper">
            {error.errors._form.join(", ")}
          </div>
        ) : null}
      </div>
    </form>
  );
}

22 Replies

Lakeland TerrierOP
This is the error I am getting

Type error: The 'this' context of type '((id: number, formData: FormData) => Promise<void>) | ((id: number, formData: FormData) => Promise<void>) | ((id: number, formData: FormData) => Promise<...>) | ... 33 more ... | ((id: number, formState: JobState, formData: FormData) => Promise<...>)' is not assignable to method's 'this' of type '(this: null, args_0: number, formData: FormData) => Promise<void>'.
  Type '(formData: FormData) => Promise<void>' is not assignable to type '(this: null, args_0: number, formData: FormData) => Promise<void>'.
    Types of parameters 'formData' and 'args_0' are incompatible.
      Type 'number' is not assignable to type 'FormData'.
  12 |   const _action = actions[exerciseActionName];
  13 |
> 14 |   const [error, action, isPending] = useActionState(_action.bind(null, id), {
     |                                                     ^
  15 |     errors: {},
  16 |   });
  17 |
 ELIFECYCLE  Command failed with exit code 1.
Error: Command "pnpm run build" exited with 1
Here is an example of the working non-resuable form I am trying to make reusable.

"use client";
import { useActionState } from "react";
import * as actions from "@/actions";
import { useState } from "react";

interface Props {
  id: number;
}

export default function TheJobForm({ id }: Props) {
  const [error, action, isPending] = useActionState(
    actions.theJob.bind(null, id),
    {
      errors: {},
    }
  );
  const [quantity, setQuantity] = useState<number | "">("");

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.currentTarget.value;
    if (value === "" || /^\d*\.?\d*$/.test(value)) {
      setQuantity(value === "" ? "" : parseFloat(value));
    }
  };

  return (
    <form action={action} className="flex flex-end flex-col gap-y-2">
      <div className="form-group flex items-center gap-x-2">
        <input
          type="text"
          inputMode="numeric"
          name="quantity"
          id={JSON.stringify(id)}
          value={quantity === "" ? "" : quantity}
          onChange={handleInputChange}
          className="number-input"
          autoComplete="off"
        />
        <button
          type="submit"
          disabled={isPending}
          className="purchase-button"
        >
          +
        </button>
        {isPending && <p>...</p>}
        {error.errors._form ? (
          <div className="rounded text-red bg-red-200 border error-message-icon-wrapper">
            {error.errors._form.join(", ")}
          </div>
        ) : null}
      </div>
    </form>
  );
}
Lakeland TerrierOP
I would love to see everyones solutions for forms with vanilla R19 + N15. I really like useActionState and my validation/error handling with the promise.
Lakeland TerrierOP
No bites? Guess I'll go with the non-reusable form implementations until I figure it out.
Lakeland TerrierOP
Almost there but still not working
Asiatic Lion
Can you post your action functions?
Lakeland TerrierOP
Action functions work with the non-reusable form. But let me pull one for show.
Asiatic Lion
It seems like your function signature for the action doesnt match what useActionState expects
Lakeland TerrierOP
Here is a reusable button I made
"use client";
import { useActionState } from "react";
import * as actions from "@/actions";
import dynamic from "next/dynamic";

type ActionKeys = keyof typeof actions;

interface Props {
  godId: string;
  onComplete: string;
  actionId: number;
  charId: string;
}
// is the form not revalidating? here is an attempt
// export const revalidate = 0;

// I dont think I need actionId?

const CompleteActionForm = ({ godId, onComplete, charId, actionId }: Props) => {
  const isValidAction = (action: string): action is ActionKeys => {
    return action in actions;
  };

  const fallbackAction = () => {
    return <div>(`Invalid action: ${onComplete}`);</div>;
  };

  const actionFunction = isValidAction(onComplete)
    ? (actions[onComplete] as (id: number) => void).bind(null, Number(charId))
    : fallbackAction;

  const [error, action, isPending] = useActionState(actionFunction, null);

  return (
    <form action={action}>
      <input type="hidden" />
      <button type="submit" disabled={isPending}>
        {isPending ? "..." : "Finish"}
      </button>
    </form>
  );
};
// not sure about this export

export default dynamic(() => Promise.resolve(CompleteActionForm), {
  ssr: false,
});


But give me one second to get an action
Asiatic Lion
I'm also a bit confused since I thought useActionState only returns two items
Lakeland TerrierOP
"use server";
import { db } from "@/db";
import { Character } from "@prisma/client";
import { auth } from "@/auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import paths from "@/paths";
import { z } from "zod";
import { CharAction } from "@prisma/client";

import dayjs from "dayjs";
var utc = require("dayjs/plugin/utc");
dayjs.extend(utc);

const createSchema = z.object({
  quantity: z.number().positive(),
});

interface JobState {
  errors: {
    quantity?: string[];
    _form?: string[];
  };
  success?: boolean;
}

export async function benchpress(
  id: number,
  formState: JobState,
  formData: FormData
): Promise<JobState> {
  const session = await auth();
  if (!session || !session.user) {
    return {
      errors: {
        _form: ["U"],
      },
    };
  }

  if (!id) {
    return {
      errors: {
        _form: ["E2"],
      },
    };
  }

  const result = createSchema.safeParse({
    quantity: Number(formData.get("quantity")),
  });

  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors,
    };
  }

  const character = await db.character.findUnique({
   /*
      DB blahblahblah
  */
  });
  if (!character) {
    return {
      errors: {
        _form: ["E1"],
      },
    };
  }
  if (character.charAction) {
    return {
      errors: {
        _form: ["Already have an action in progress"],
      },
    };
  }
   /*
      more error logic

  */
  try {
    const [createCharAction, updatedCharacter] = await db.$transaction([
      /*
        DB blahlahblah
      */
    ]);
  } catch (err: unknown) {
    if (err instanceof Error) {
      return {
        errors: {
          _form: [err.message],
        },
      };
    } else {
      return {
        errors: {
          _form: ["F"],
        },
      };
    }
  }

  revalidatePath(paths.characterShow(character.parentId, Number(id)));
  redirect(paths.characterShow(character.parentId, Number(id)));
}
Asiatic Lion
I think the id argument shouldn't be there, just state and form data
Lakeland TerrierOP
useActionState returns isPending also
so I need to pass id in the formData
Asiatic Lion
or state
@Lakeland Terrier useActionState returns isPending also
Asiatic Lion
I believe you, but I don't know why the docs don't mention it https://19.react.dev/reference/react/useActionState
Lakeland TerrierOP
Docs for useActionState are trash
Asiatic Lion
lol fair
Lakeland TerrierOP
Vanilla form handling and date handling should be so much easier.
@Asiatic Lion I believe you, but I don't know why the docs don't mention it https://19.react.dev/reference/react/useActionState
it is not ready for use yet.

at the moment, pretend that useActionState doesn't give you the isPending. if you use it, things can break and that's your fault.
Lakeland TerrierOP
Gotta love it when advice involves pretending.