Skip to content

Instantly share code, notes, and snippets.

@victorcolina22
Last active December 17, 2025 22:49
Show Gist options
  • Select an option

  • Save victorcolina22/8d2ddf81ab60946c266a420309c8d55e to your computer and use it in GitHub Desktop.

Select an option

Save victorcolina22/8d2ddf81ab60946c266a420309c8d55e to your computer and use it in GitHub Desktop.

Custom hook for React to manage form states

Install TanStack Query to properly use

npm i @tanstack/react-query

Also you need to copy and paste this function in your project to enable the comparation

export const deepCompare = (objA: any, objB: any) => {
	if (objA === objB) return true; // Misma referencia o valores primitivos

	if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
		return false; // Ningún objeto es null
	}

	const keysA = Object.keys(objA);
	const keysB = Object.keys(objB);

	if (keysA.length !== keysB.length) return false; // Diferente número de propiedades

	for (const key of keysA) {
		if (!keysB.includes(key) || !deepCompare(objA[key], objB[key])) {
			return false; // Si falta una key o sus values no coinciden
		}
	}
	return true;
};

Code

import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query';
import { deepCompare } from '@utils/common/object.util';
import { useEffect, useMemo } from 'react';

type EditionData = {
	isEditing: boolean;
};

type UseEditionOptions = {
	/**
	 * Crear snapshot inicial al montar
	 */
	snapshotOnMount?: boolean;
};

/**
 * Custom hook para el manejo de la edición de la pre póliza.
 *
 * @param {string} key - La "key" para los datos de edición.
 * @param {Partial<T>} [data] - La data inicial para la edición. Inicializado por defecto con un objeto vacío.
 * @param {UseEditionOptions} [options] - Opciones para el hook. Inicializado por defecto con un objeto vacío.
 */
export function useEdition<T extends object = Record<string, unknown>>(
	key: string,
	data: Partial<T> = {},
	options: UseEditionOptions = {},
): {
	/**
	 * La "queryKey" para los datos de edición.
	 */
	baseKey: readonly ['edition', string];

	/**
	 * La "queryKey" para los datos del snapshot.
	 */
	snapKey: readonly ['edition', string, 'snapshot'];

	/**
	 * La "queryKey" para los datos que se envían a editar.
	 */
	sendToEditKey: readonly ['edition', string, 'send-to-edit'];

	/**
	 * Una función para obtener los datos de edición actuales.
	 */
	getCurrent: () => T | undefined;

	/**
	 * Una función para obtener los datos de edición del snapshot.
	 */
	getSnapshot: () => T | undefined;

	/**
	 * Una función para obtener los datos que se envían a editar.
	 */
	getDataToEdit: () => T | undefined;

	/**
	 * Una función para verificar si los datos de edición han sido modificados.
	 */
	isModified: () => boolean;

	/**
	 * Una función para obtener si la edición está siendo editada.
	 */
	getIsEditing: () => boolean | undefined;

	/**
	 * Una función para actualizar los datos de edición.
	 *
	 * @param {Partial<T>} editionData - Los nuevos datos de edición para establecer.
	 */
	setEditionData: (editionData: Partial<T>) => void;

	/**
	 * Una función para tomar un snapshot de los datos de edición y poder compararlos con los datos actuales.
	 */
	takeSnapshot: () => void;

	/**
	 * El QueryClient utilizado por el hook.
	 */
	queryClient: QueryClient;
} {
	const queryClient = useQueryClient();
	const equals = deepCompare;

	const baseKey = useMemo(() => ['edition', key] as const, [key]);
	const snapKey = useMemo(() => ['edition', key, 'snapshot'] as const, [key]);
	const sendToEditKey = useMemo(() => ['edition', key, 'send-to-edit'] as const, [key]);

	useQuery<Partial<T>>({
		queryKey: baseKey,
		initialData: () => queryClient.getQueryData<T>(baseKey) ?? { ...data, isEditing: false },
		staleTime: Infinity,
	});
	useQuery<Partial<T>>({
		queryKey: snapKey,
		initialData: () => queryClient.getQueryData<T>(baseKey) ?? { ...data },
		staleTime: Infinity,
	});

	// Inicializa el estado base una vez montado
	useEffect(() => {
		const current = queryClient.getQueryData<T>(baseKey);
		if (data && Object.keys(data).length) {
			queryClient.setQueryData<Partial<T>>(baseKey, {
				...current,
				...data,
			});
		}

		// Snapshot opcional al montar
		if (options.snapshotOnMount) {
			const afterInit = queryClient.getQueryData<T>(baseKey);
			if (afterInit) {
				const { isEditing, ...payload } = afterInit as T & EditionData; // eslint-disable-line @typescript-eslint/no-unused-vars
				queryClient.setQueryData<T>(snapKey, payload as T);
				queryClient.setQueryData<T>(baseKey, {
					...afterInit,
				});
			}
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []); // intencional: solo al montar

	const getCurrent = () => queryClient.getQueryData<T>(baseKey) as T | undefined;
	const getSnapshot = () => queryClient.getQueryData<T>(snapKey) as T | undefined;
	const getIsEditing = () => queryClient.getQueryData<T & EditionData>(baseKey)?.isEditing as boolean | undefined;
	const getDataToEdit = () => queryClient.getQueryData<T>(sendToEditKey) as T | undefined;

	const setEditionData = (editionData: Partial<T>) => {
		const current = getCurrent();
		const merged = {
			...current,
			...editionData,
		} as T;

		queryClient.setQueryData<T>(baseKey, merged);

		handleSendDataToEdit(editionData, getSnapshot() as T);
	};

	const takeSnapshot = () => {
		const current = getCurrent();
		if (!current) return;

		const plain = { ...current };
		const { isEditing, ...payload } = plain as T & EditionData; // eslint-disable-line @typescript-eslint/no-unused-vars

		queryClient.setQueryData<T>(snapKey, payload as T);
		cleanDataToSend();
	};

	const isModified = () => {
		const current = getCurrent();
		const snap = getSnapshot();
		if (!current || !snap) return false;

		const plainCurrent = { ...current };
		const { isEditing, ...payload } = plainCurrent as T & EditionData; // eslint-disable-line @typescript-eslint/no-unused-vars
		const { isEditing: snapIsEditing, ...restSnap } = snap as T & EditionData; // eslint-disable-line @typescript-eslint/no-unused-vars

		return !equals(payload, restSnap);
	};

	const handleSendDataToEdit = (data: Partial<T>, dataToCompare: Partial<T>) => {
		if (!queryClient.getQueryData(sendToEditKey)) {
			queryClient.setQueryData<Partial<T>>(sendToEditKey, { ...data });
		}

		Object.entries(dataToCompare).forEach(([dataToCompareKey, dataToCompareValue]) => {
			Object.entries(data).forEach(([dataKey, dataValue]) => {
				if (dataKey === dataToCompareKey) {
					if (equals(dataValue, dataToCompareValue)) {
						delete (queryClient.getQueryData(sendToEditKey) as any)[dataKey];
					} else {
						queryClient.setQueryData<Partial<T>>(sendToEditKey, {
							...queryClient.getQueryData(sendToEditKey),
							...data,
						});
					}
				}
			});
		});
	};

	const cleanDataToSend = () => {
		queryClient.removeQueries({
			queryKey: sendToEditKey,
		});
	};

	return {
		baseKey,
		snapKey,
		sendToEditKey,
		getCurrent,
		getSnapshot,
		getDataToEdit,
		isModified,
		getIsEditing,
		setEditionData,
		takeSnapshot,
		queryClient,
	};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment