/** * 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> { /** 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; /** 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 { /** Current form values */ values: T; /** Set form values directly */ setValues: React.Dispatch>; /** 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 ( * * ... * * ); * }; */ export function useEditPageSync>( options: UseEditPageSyncOptions, ): UseEditPageSyncReturn { 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(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)[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>( options: UseEditPageSyncOptions, ): [T, React.Dispatch>] { const { values, setValues } = useEditPageSync(options); return [values, setValues]; } export default useEditPageSync;