109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
/**
|
|
* useFormSync Hook
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
|
import type { RootState } from '../stores/store';
|
|
import type { AsyncThunk } from '@reduxjs/toolkit';
|
|
import type { BaseEntity } from '../types/entities';
|
|
import type { FetchParams } from '../types/api';
|
|
|
|
interface UseFormSyncOptions<T, TFormValues> {
|
|
entitySelector: (state: RootState) => T | T[] | undefined;
|
|
fetchAction: AsyncThunk<unknown, FetchParams, { rejectValue: unknown }>;
|
|
initialValues: TFormValues;
|
|
transformEntity?: (entity: T) => TFormValues;
|
|
}
|
|
|
|
interface UseFormSyncReturn<TFormValues> {
|
|
formValues: TFormValues;
|
|
setFormValues: (values: TFormValues) => void;
|
|
isLoading: boolean;
|
|
entityId: string | undefined;
|
|
resetForm: () => void;
|
|
}
|
|
|
|
/**
|
|
* Hook for synchronizing form state with Redux entity data
|
|
* Replaces the redundant triple-useEffect pattern in edit pages
|
|
*
|
|
* @param options - Configuration options
|
|
* @returns Form state and management functions
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const { formValues, isLoading, entityId } = useFormSync({
|
|
* entitySelector: (state) => state.users.users,
|
|
* fetchAction: fetch,
|
|
* initialValues: { firstName: '', lastName: '', email: '' },
|
|
* })
|
|
* ```
|
|
*/
|
|
export function useFormSync<
|
|
T extends BaseEntity,
|
|
TFormValues extends Record<string, unknown>,
|
|
>(options: UseFormSyncOptions<T, TFormValues>): UseFormSyncReturn<TFormValues> {
|
|
const { entitySelector, fetchAction, initialValues, transformEntity } =
|
|
options;
|
|
|
|
const router = useRouter();
|
|
const dispatch = useAppDispatch();
|
|
const [formValues, setFormValues] = useState<TFormValues>(initialValues);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// Get entity ID from router
|
|
const entityId = router.query.id as string | undefined;
|
|
|
|
// Get entity data from Redux
|
|
const entityData = useAppSelector(entitySelector);
|
|
|
|
// Fetch entity data when ID is available
|
|
useEffect(() => {
|
|
if (!entityId) return;
|
|
|
|
setIsLoading(true);
|
|
dispatch(fetchAction({ id: entityId })).finally(() => setIsLoading(false));
|
|
}, [entityId, dispatch, fetchAction]);
|
|
|
|
// Sync form values with fetched entity data (single useEffect replaces redundant pair)
|
|
useEffect(() => {
|
|
if (!entityData) return;
|
|
|
|
// Handle both single entity and array cases
|
|
const entity = Array.isArray(entityData) ? undefined : entityData;
|
|
|
|
if (entity && typeof entity === 'object') {
|
|
if (transformEntity) {
|
|
setFormValues(transformEntity(entity));
|
|
} else {
|
|
// Default: merge entity data into initial values
|
|
const newValues = { ...initialValues };
|
|
Object.keys(initialValues).forEach((key) => {
|
|
const entityKey = key as keyof T;
|
|
if (entityKey in entity) {
|
|
(newValues as Record<string, unknown>)[key] = entity[entityKey];
|
|
}
|
|
});
|
|
setFormValues(newValues);
|
|
}
|
|
}
|
|
}, [entityData, initialValues, transformEntity]);
|
|
|
|
// Reset form to initial values
|
|
const resetForm = useCallback(() => {
|
|
setFormValues(initialValues);
|
|
}, [initialValues]);
|
|
|
|
return {
|
|
formValues,
|
|
setFormValues,
|
|
isLoading,
|
|
entityId,
|
|
resetForm,
|
|
};
|
|
}
|
|
|
|
export default useFormSync;
|