Skip to content

Instantly share code, notes, and snippets.

@ellemedit
Created June 21, 2024 01:27
Show Gist options
  • Save ellemedit/3018f496722dc01858c0c6097ae4a6a1 to your computer and use it in GitHub Desktop.
Save ellemedit/3018f496722dc01858c0c6097ae4a6a1 to your computer and use it in GitHub Desktop.
React Form Action + Validation + Focus + State patterns

Usage

function LoginForm() {
  const [state, dispatch] = useActionState(loginFormAction, {});
  const [_isPending, formAction] = useFormAction(async (formData, form) => {
    dispatch({ type: "RESET" });
    const errors = validateForm(formData);
    if (errors != null) {
      dispatch({ type: "SET_INVALID_INPUTS", form, errors });
      return;
    }
    dispatch({ type: "SERVER_ACTION", form });
  });
  
  return (
    <form onSubmit=
      <Input name="email" error={state.errors?.email} />
      ...
    </form>
  )
}

interface FormActionState {
  errors?: {
    email?: string;
    password?: string;
  }
}

type FormActionPayload =
  | {
      type: "REQUEST_SERVER";
      form: HTMLFormElement;
    }
  | {
      type: "SET_INVALID_INPUTS";
      errors: TokenCreateInputValidationErrors;
      form: HTMLFormElement;
    }
  | { type: "RESET_ERRORS" };

async function loginFormAction(
  state: FormActionState,
  payload: FormActionPayload,
): Promise<FormActionState> {
  switch (payload.type) {
    case "REQUEST_SERVER": {
      const { form } = payload;
      const { errors } = await attemptToLogin(new FormData(form));
      const nextState = {
        errors: {
          ...state.errors,
          ...errors,
        },
      };
      focusOnFirstErrorElement(form, new Set(Object.keys(nextState.errors)));
      return nextState;
    }
    case "SET_INVALID_INPUTS": {;
      const { errors } = payload;
      const nextState = {
        errors: {
          ...state.errors,
          ...errors,
        },
      };
      focusOnFirstErrorElement(form, new Set(Object.keys(nextState.errors)));
      return nextState;
    }
    case "RESET_ERRORS": {
      return {};
    }
  }
}
export function focusOnFirstErrorElement(form: HTMLFormElement, errorInputNames: Set<string>) {
for (const candidate of form.querySelectorAll('[name]')) {
const name = 'name' in candidate && typeof candidate.name === 'string' ? candidate.name : null;
if (name == null) {
console.error("name attribute is required on form element. This might be a bug. Element:", candidate);
continue;
}
if (errorInputNames.has(name)) {
const focus = 'focus' in candidate && typeof candidate.focus === 'function' ? candidate.focus : null;
if (focus == null) {
console.error("focus method is required on form element. This might be a bug. Element:", candidate);
return;
}
focus.call(candidate);
return;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment