39948-vm/frontend/src/hooks/useEditPageSync.ts
2026-04-05 18:46:16 +04:00

167 lines
4.9 KiB
TypeScript

/**
* useEditPageSync Hook
*
* Handles the common pattern in edit pages:
* 1. Fetch entity when ID is available
* 2. Sync form values when entity data changes
*
* Reduces ~50 lines of duplicated code across edit pages.
*/
import { useEffect, useState, useCallback } from 'react';
import { useRouter } from 'next/router';
import { AsyncThunk } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import type { RootState } from '../stores/store';
interface UseEditPageSyncOptions<T extends Record<string, unknown>> {
/** Redux selector to get the entity from state (can return single entity, array, or any slice state value) */
entitySelector: (state: RootState) => unknown;
/** Redux async thunk to fetch the entity */
fetchAction: AsyncThunk<unknown, { id?: string; query?: string }, object>;
/** Initial form values */
initialValues: T;
/** Optional post-processing of entity data before setting form values */
postProcess?: (entity: T, initial: T) => T;
/** Optional ID override (defaults to router.query.id) */
idOverride?: string;
}
interface UseEditPageSyncReturn<T> {
/** Current form values */
values: T;
/** Set form values directly */
setValues: React.Dispatch<React.SetStateAction<T>>;
/** Entity ID from router */
id: string | null;
/** Whether initial fetch is loading */
isLoading: boolean;
/** Whether entity was found */
isFound: boolean;
}
/**
* Hook for syncing edit page forms with Redux entity state
*
* @example
* const initVals = { name: '', permissions: [] };
*
* const EditRolesPage = () => {
* const { values, id, isLoading } = useEditPageSync({
* entitySelector: (state) => state.roles.data,
* fetchAction: fetch,
* initialValues: initVals,
* });
*
* const handleSubmit = async (data) => {
* await dispatch(update({ id, data }));
* router.push('/roles/roles-list');
* };
*
* return (
* <Formik enableReinitialize initialValues={values} onSubmit={handleSubmit}>
* ...
* </Formik>
* );
* };
*/
export function useEditPageSync<T extends Record<string, unknown>>(
options: UseEditPageSyncOptions<T>,
): UseEditPageSyncReturn<T> {
const {
entitySelector,
fetchAction,
initialValues,
postProcess,
idOverride,
} = options;
const router = useRouter();
const dispatch = useAppDispatch();
// Get ID from router or override
const routerId = router.query.id;
const id =
idOverride ?? (Array.isArray(routerId) ? routerId[0] : routerId) ?? null;
// Local state for form values
const [values, setValues] = useState<T>(initialValues);
const [isLoading, setIsLoading] = useState(false);
const [isFound, setIsFound] = useState(false);
// Get entity from Redux store
const rawEntity = useAppSelector(entitySelector);
// Handle both array and single entity - when single entity, it's stored directly
// When array, it means we fetched a list (shouldn't happen in edit pages)
// Filter out primitive types (number, boolean, string) from the slice state
const entity = (() => {
if (rawEntity === null || rawEntity === undefined) return null;
if (typeof rawEntity !== 'object') return null; // Filter out primitives
if (Array.isArray(rawEntity)) {
return rawEntity.length === 1 ? (rawEntity[0] as T) : null;
}
return rawEntity as T;
})();
// Fetch entity when ID changes
useEffect(() => {
if (id) {
setIsLoading(true);
dispatch(fetchAction({ id }))
.then(() => setIsLoading(false))
.catch(() => setIsLoading(false));
}
}, [id, dispatch, fetchAction]);
// Sync form values when entity changes
useEffect(() => {
if (entity && typeof entity === 'object' && !Array.isArray(entity)) {
// Build new values by copying from entity to initial structure
const newValues = { ...initialValues };
Object.keys(initialValues).forEach((key) => {
if (key in entity) {
(newValues as Record<string, unknown>)[key] = entity[key as keyof T];
}
});
// Apply post-processing if provided
const finalValues = postProcess
? postProcess(newValues, initialValues)
: newValues;
setValues(finalValues);
setIsFound(true);
}
}, [entity, initialValues, postProcess]);
return {
values,
setValues,
id,
isLoading,
isFound,
};
}
/**
* Simplified version that returns just values tuple
* for drop-in replacement in existing code
*
* @example
* const [initialValues, setInitialValues] = useEditPageSyncSimple({
* entitySelector: (state) => state.roles.data,
* fetchAction: fetch,
* initialValues: initVals,
* });
*/
export function useEditPageSyncSimple<T extends Record<string, unknown>>(
options: UseEditPageSyncOptions<T>,
): [T, React.Dispatch<React.SetStateAction<T>>] {
const { values, setValues } = useEditPageSync(options);
return [values, setValues];
}
export default useEditPageSync;