React useForm Hook Challenge
This challenge asks you to build a custom React hook, useForm, that simplifies form management in React applications. A well-built useForm hook should abstract away common form logic such as handling input changes, managing form state, validating fields, and submitting the form, allowing developers to focus on UI and business logic.
Problem Description
Your task is to create a useForm hook in TypeScript that provides the following functionalities:
-
State Management:
- Manage the state of form values. The initial values should be configurable.
- Provide a way to access and update these form values.
-
Input Handling:
- Provide a function to handle
onChangeevents for form inputs. This function should automatically update the corresponding form value based on the input'snameandvalue(orcheckedfor checkboxes).
- Provide a function to handle
-
Validation:
- Allow defining validation rules for individual form fields.
- Provide a mechanism to trigger validation.
- Store and expose validation errors.
-
Submission:
- Provide a function to handle form submission. This function should:
- Prevent the default form submission behavior.
- Trigger validation before submitting.
- If validation passes, call a user-provided
onSubmithandler with the form values. - If validation fails, update the errors state.
- Provide a function to handle form submission. This function should:
-
Reset:
- Provide a function to reset the form to its initial values and clear any errors.
Key Requirements:
- The hook should be generic, accepting a type
Tfor the form values. - The hook should return an object containing:
values: The current form values.errors: An object containing validation errors for each field.handleChange: A function to handle input change events.handleSubmit: A function to handle form submission.resetForm: A function to reset the form.setFieldValue: A function to manually set a specific field's value.setErrors: A function to manually set validation errors.
Expected Behavior:
- When an input's value changes, the corresponding field in the
valuesstate should be updated. - When
handleSubmitis called, validation should run. If any errors are found,errorsstate should be updated, and theonSubmithandler should not be called. - If validation passes, the
onSubmithandler should be called with the currentvalues. resetFormshould restorevaluesto the initial state and clearerrors.setFieldValueshould update a single field's value.setErrorsshould update the errors state directly.
Edge Cases:
- Handling different input types (text, number, checkbox, radio buttons).
- Form fields with no validation rules.
- Empty initial values.
Examples
Example 1: Basic Form Usage
// Assuming you have a component that uses the useForm hook like this:
const initialValues = {
firstName: '',
lastName: '',
};
const validationRules = {
firstName: (value: string) => (value ? '' : 'First Name is required'),
lastName: (value: string) => (value ? '' : 'Last Name is required'),
};
function MyForm() {
const { values, errors, handleChange, handleSubmit, resetForm } = useForm(initialValues, validationRules);
const onSubmit = (data: typeof initialValues) => {
console.log('Form submitted:', data);
// Perform actual submission logic here
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="firstName">First Name:</label>
<input
type="text"
id="firstName"
name="firstName"
value={values.firstName}
onChange={handleChange}
/>
{errors.firstName && <p style={{ color: 'red' }}>{errors.firstName}</p>}
</div>
<div>
<label htmlFor="lastName">Last Name:</label>
<input
type="text"
id="lastName"
name="lastName"
value={values.lastName}
onChange={handleChange}
/>
{errors.lastName && <p style={{ color: 'red' }}>{errors.lastName}</p>}
</div>
<button type="submit">Submit</button>
<button type="button" onClick={resetForm}>Reset</button>
</form>
);
}
Input to the useForm hook (initial values and rules):
const initialValues = {
firstName: '',
lastName: '',
};
const validationRules = {
firstName: (value: string) => (value ? '' : 'First Name is required'),
lastName: (value: string) => (value ? '' : 'Last Name is required'),
};
User Interaction and Expected values and errors state changes:
-
Initial Load:
values:{ firstName: '', lastName: '' }errors:{}
-
User types "John" into First Name:
values:{ firstName: 'John', lastName: '' }errors:{}
-
User clicks Submit without filling Last Name:
valuesremains{ firstName: 'John', lastName: '' }errorsbecomes{ lastName: 'Last Name is required' }
-
User types "Doe" into Last Name and clicks Submit:
values:{ firstName: 'John', lastName: 'Doe' }errors:{}onSubmithandler is called with{ firstName: 'John', lastName: 'Doe' }.
-
User clicks Reset:
values:{ firstName: '', lastName: '' }errors:{}
Example 2: Checkbox and Manual Field Setting
// Component using useForm
const initialValues = {
agreeToTerms: false,
email: '',
};
const validationRules = {
email: (value: string) => {
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
return '';
},
};
function RegistrationForm() {
const {
values,
errors,
handleChange,
handleSubmit,
setFieldValue, // Added for manual setting
} = useForm(initialValues, validationRules);
const onSubmit = (data: typeof initialValues) => {
console.log('Registration submitted:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
type="checkbox"
id="agreeToTerms"
name="agreeToTerms"
checked={values.agreeToTerms} // Use 'checked' for checkboxes
onChange={handleChange}
/>
<label htmlFor="agreeToTerms">I agree to the terms</label>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
</div>
<button type="submit">Register</button>
<button
type="button"
onClick={() => setFieldValue('email', 'prefilled@example.com')}
>
Prefill Email
</button>
</form>
);
}
Input to the useForm hook:
const initialValues = {
agreeToTerms: false,
email: '',
};
const validationRules = {
email: (value: string) => {
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
return '';
},
};
User Interaction and Expected State Changes:
-
Initial Load:
values:{ agreeToTerms: false, email: '' }errors:{}
-
User checks the "agreeToTerms" checkbox:
values:{ agreeToTerms: true, email: '' }errors:{}
-
User clicks "Prefill Email" button:
values:{ agreeToTerms: true, email: 'prefilled@example.com' }errors:{}
-
User clears email and clicks Submit:
values:{ agreeToTerms: true, email: '' }errors:{ email: 'Email is required' }
Constraints
- The
useFormhook must be implemented using TypeScript. - The hook should be performant and avoid unnecessary re-renders.
- The
handleChangefunction must correctly infer the input type (e.g.,event.target.valuefor text inputs,event.target.checkedfor checkboxes). - The
validationRulesobject should have keys matching the field names ininitialValues. - The
onSubmithandler passed tohandleSubmitwill receive the currentvaluesobject.
Notes
- Consider how to handle deeply nested form values if needed (though for this challenge, flat structures are sufficient).
- The
handleChangefunction needs to correctly identify whether to useevent.target.valueorevent.target.checked. - Think about how to structure the
errorsobject to correspond to thevaluesobject. - You might want to create helper types or interfaces to improve the type safety of your hook.
- For validation, the validation rule function for a field should return an empty string (
'') if there are no errors for that field, and a string representing the error message otherwise.