Implement a Custom useFormState Hook in React
This challenge asks you to build a custom React hook that mimics the functionality of a useFormState hook. This type of hook is invaluable for managing form states, handling submissions, and providing feedback to the user, especially in conjunction with server actions or similar asynchronous operations.
Problem Description
Your task is to implement a custom hook called useFormState in TypeScript. This hook should manage the state of a form, track its submission status, and capture any errors or data returned from a submission function.
Key Requirements:
- State Management: The hook should accept an initial form state and a submission function.
- Submission Function: The submission function will be an asynchronous function (or a function that returns a Promise) that takes the current form state as an argument and returns a result (which could be data or an error).
- State Updates: The hook should expose a way to dispatch an action that triggers the submission function. This dispatch function should update the form state with the result of the submission.
- Return Value: The hook should return an object containing:
formState: The current state of the form.isSubmitting: A boolean indicating whether the submission is currently in progress.error: Any error caught during the submission process.data: Any successful data returned by the submission function.dispatch: A function to trigger the form submission.
Expected Behavior:
- When
dispatchis called,isSubmittingshould becometrue. - The provided submission function should be invoked with the current
formState. - Upon successful completion of the submission function,
isSubmittingshould becomefalse, anddatashould be populated with the returned value. - If the submission function throws an error,
isSubmittingshould becomefalse, anderrorshould be populated with the caught error. - The
formStateshould be updated to reflect the result of the submission (either data or error).
Edge Cases:
- Handling multiple dispatches while a submission is already in progress (the hook should ideally queue or ignore subsequent dispatches until the current one finishes, or behave predictably).
- Ensuring proper cleanup of any ongoing asynchronous operations if the component unmounts before the submission completes (though for this challenge, we can focus on the core state management).
Examples
Let's assume we have a simple form with a single text input.
Example 1: Successful Submission
// Mock submission function
const mockSubmitFn = async (state: { message: string }) => {
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate network delay
if (state.message.toLowerCase().includes("success")) {
return { success: true, processedMessage: state.message.toUpperCase() };
}
throw new Error("This is not a success message.");
};
// Inside a React component
const initialFormState = { message: "" };
const { formState, isSubmitting, error, data, dispatch } = useFormState(mockSubmitFn, initialFormState);
// Initial render:
// formState = { message: "" }
// isSubmitting = false
// error = undefined
// data = undefined
// User types "Hello, this is a success message." into an input and the input's onChange updates formState.
// User clicks a submit button, which calls dispatch({ message: "Hello, this is a success message." })
// During submission:
// isSubmitting = true
// After submission:
// isSubmitting = false
// error = undefined
// data = { success: true, processedMessage: "HELLO, THIS IS A SUCCESS MESSAGE." }
// formState = { message: "Hello, this is a success message.", data: { success: true, processedMessage: "HELLO, THIS IS A SUCCESS MESSAGE." } } (assuming formState is updated with data)
Example 2: Submission with Error
// Mock submission function (same as above)
const mockSubmitFn = async (state: { message: string }) => {
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate network delay
if (state.message.toLowerCase().includes("success")) {
return { success: true, processedMessage: state.message.toUpperCase() };
}
throw new Error("This is not a success message.");
};
// Inside a React component
const initialFormState = { message: "" };
const { formState, isSubmitting, error, data, dispatch } = useFormState(mockSubmitFn, initialFormState);
// User types "This will fail." into an input and the input's onChange updates formState.
// User clicks a submit button, which calls dispatch({ message: "This will fail." })
// During submission:
// isSubmitting = true
// After submission:
// isSubmitting = false
// error = Error: This is not a success message.
// data = undefined
// formState = { message: "This will fail.", error: Error: This is not a success message. } (assuming formState is updated with error)
Example 3: Multiple Dispatches (Conceptual)
If dispatch is called twice rapidly while a submission is in progress, the behavior depends on the implementation. A robust implementation might only process the latest dispatch after the current one completes, or it might ignore subsequent dispatches. For this challenge, a simple sequential processing where only the last dispatched value is considered once the previous submission finishes is acceptable.
Constraints
- The
useFormStatehook must be implemented in TypeScript. - The submission function passed to the hook is expected to be an
asyncfunction or a function returning aPromise. - The hook should not introduce significant performance overhead beyond what is typical for React hooks.
- The
formStateshould be a deep copy or managed immutably to prevent unexpected side effects.
Notes
- Consider how you will handle the update to the
formStateitself. Should it simply be the initial state, or should it also incorporate thedataorerrorreturned from the submission? A common pattern is to merge the returneddataorerrorinto theformState. - Think about how to manage the
isSubmittingflag to prevent race conditions or multiple submissions. - The type for
FormStateshould be generic, allowing it to hold any shape of form data. - The return type of the submission function should also be considered for generic typing.