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 {};
}
}
}
Created
June 21, 2024 01:27
-
-
Save ellemedit/3018f496722dc01858c0c6097ae4a6a1 to your computer and use it in GitHub Desktop.
React Form Action + Validation + Focus + State patterns
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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