167 lines
4.9 KiB
TypeScript
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;
|