nico.fyi
    Published on

    Cancel action or modify payload before useActionState

    New features for useResettableActionState, the enhanced useActionState

    Authors

    Just updated my open source React hook useResettableActionState. At the beginning, I made it to allow me to reset the state after submitting the form using useActionState. Now it also allows you to cancel the action or modify the payload before calling the server action.

    Cancelling the action

    Before submitting a form, you may want to cancel the action if the entered values are not valid. For example, when updating a user's password, you may want to cancel the action if the user fails to repeat the new password correctly. In this case, you can provide a function to the new beforeAction argument. Code example:

    app/page.tsx
    'use client';
    import { doSomething } from './actions';
    import { useResettableActionState } from 'use-resettable-action-state';
    
    export default function Form({ initialState }: { initialState: { password: string | null, error: string | null } }) {
      const [state, submit, isPending, reset, payload] = useResettableActionState(
        doSomething,
        initialState,
        undefined,
        async (payload, abortController) => {
          if (payload?.get('password') !== payload?.get('repeat-password')) {
            abortController.abort({
              error: 'Passwords do not match',
            });
          }
          return payload;
        },
      );
    
      return (
        <form action={submit}>
          {state && !state.error && <p>Success!</p>}
          {state && state.error && <p className="bg-red-500 text-white p-4">{state.error}</p>}
          <input
            type="password"
            name="password"
            id="password"
            placeholder="Enter new password"
          />
          <input
            type="password"
            name="repeat-password"
            id="repeat-password"
            placeholder="Repeat the new password"
          />
          <p>{state && state.data?.message}</p>
    
          <button disabled={isPending} type="submit">
            {isPending ? 'Loading...' : 'Submit'}
          </button>
        </form>
      );
    }
    

    As you can see from the code example above, the beforeAction function receives the payload and the abort controller. You can use the abort controller to cancel the action.

    Modifying the payload before calling the server action

    There may be cases where you want to modify the payload before calling the server action. For example, you may want to acquire a Google reCAPTCHA token and submit it along with the form data. The token has an expiration time, so it's better to acquire it before calling the server action instead of when the page is loaded. The user might take a while to complete the form, and the token might expire before the user submits the form.

    In the example below, I'm using the React reCAPTCHA v3 library to acquire the token. This library has a useGoogleReCaptcha hook that returns a function to acquire the token, executeRecaptcha. Then I use the beforeAction function to acquire the token and modify the payload.

    app/page.tsx
    export default function Form() {
      const { executeRecaptcha } = useGoogleReCaptcha();
    
      const [state, submit, isPending, reset, payload] = useResettableActionState(
        doSomething,
        null,
        undefined,
        async (payload, abortController) => {
          const token = await executeRecaptcha?.("doSomething");
          if (!token) {
            abortController.abort({
              error: "reCAPTCHA verification failed",
            });
            return payload;
          }
          payload?.set("token", token);
          return payload;
        }
      );
      const formRef = useRef<HTMLFormElement>(null);
    
      return (
        <form
          id="theform"
          ref={formRef}
          className="flex flex-col items-center justify-start space-y-4 text-center p-4 w-full max-w-md bg-slate-50"
          action={submit}
        >
          {!isPending && state && state.error && (
            <p className="bg-red-500 text-white p-4">{state.error}</p>
          )}
          <pre className="text-sm text-muted-foreground text-left">
            {!isPending &&
              state &&
              JSON.stringify(state.data?.recaptchaResponse, null, 2)}
          </pre>
          <input
            required
            disabled={isPending}
            type="text"
            name="name"
            id="name"
            placeholder="Enter your name"
            defaultValue={(payload?.get("name") as string) || ""}
          />
    
          <div className="flex flex-row justify-between items-center w-full">
            <button
              form="theform"
              disabled={isPending}
              className="bg-primary text-primary-foreground hover:bg-primary/80 py-2 px-4 rounded-md disabled:bg-muted-foreground"
              type="submit"
            >
              {isPending ? "Loading..." : "Submit"}
            </button>
          </div>
          {isPending && (
            <p className="text-sm text-muted-foreground">
              (3s delay in doSomething action)
            </p>
          )}
        </form>
      );
    }
    

    You can check out the demo here.

    Conclusion

    The beforeAction function is a convenient way to cancel the action or modify the payload before calling the server action. Thanks to it, you won't need to use useEffect or useState to manage the submission of your form.

    Additionally, the library is now 100% covered by tests as shown here! 🥳