From c13adacadb6a4a1966d7ce4cc96e20f0787c426f Mon Sep 17 00:00:00 2001 From: danybeltran Date: Sat, 13 Sep 2025 00:22:56 -0600 Subject: [PATCH] fixes(mutate): Fixes unexpected revalidation after data mutation --- package.json | 2 +- src/hooks/use-fetch.ts | 142 ++++++++++++++++++++++++++--------------- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 0e22cf9..f2ca8df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-react", - "version": "3.8.1", + "version": "3.8.2", "description": "React hooks for data fetching", "main": "dist/index.js", "scripts": { diff --git a/src/hooks/use-fetch.ts b/src/hooks/use-fetch.ts index d8d1ea2..eff0c5e 100644 --- a/src/hooks/use-fetch.ts +++ b/src/hooks/use-fetch.ts @@ -1,5 +1,12 @@ 'use client' -import { useState, useEffect, useMemo, useRef, useCallback } from 'react' +import { + useState, + useEffect, + useMemo, + useRef, + useCallback, + startTransition +} from 'react' import { abortControllers, @@ -1033,8 +1040,45 @@ export function useFetch( ] ) + // At the top of your component, create a ref to hold all dependencies for the listener. + const listenerDeps = useRef({ + thisDeps, + idString, + requestCallId, + forceMutate, + imperativeFetch, + setData, + setOnline, + setLoading, + setError, + setCompletedAttempts, + handleMutate, + url, + onMutate + }) + + // On every render, update the ref's current value to ensure the listener always has the latest functions and props. + // This does not trigger the effect below. + listenerDeps.current = { + thisDeps, + idString, + requestCallId, + forceMutate, + imperativeFetch, + setData, + setOnline, + setLoading, + setError, + setCompletedAttempts, + handleMutate, + url, + onMutate + } + useEffect(() => { - function waitFormUpdates(v: any) { + function waitFormUpdates(event: any) { + // Destructure the latest dependencies from the ref inside the listener + const deps = listenerDeps.current const { isMutating, data: $data, @@ -1042,45 +1086,55 @@ export function useFetch( online, loading, completedAttempts - } = v || {} - - if (isMutating) { - if (serialize($data) !== serialize(cacheForMutation.get(resolvedKey))) { - cacheForMutation.set(idString, $data) - if (isMutating) { - forceMutate($data) - if (handleMutate) { - if (url === '') { - ;(onMutate as any)($data, imperativeFetch) - } else { - if (!runningMutate.get(resolvedKey)) { - runningMutate.set(resolvedKey, true) - ;(onMutate as any)($data, imperativeFetch) - } - } - } + } = event || {} + + // 1. Handle mutations with cleaner logic + if ( + isMutating && + serialize($data) !== serialize(cacheForMutation.get(resolvedKey)) + ) { + cacheForMutation.set(deps.idString, $data) + deps.forceMutate($data) + + if (deps.handleMutate) { + const canCallOnMutate = + deps.url === '' || !runningMutate.get(resolvedKey) + if (canCallOnMutate) { + if (deps.url !== '') runningMutate.set(resolvedKey, true) + ;(deps.onMutate as any)($data, deps.imperativeFetch) } } } - if (v.requestCallId !== requestCallId) { - if (!willSuspend.get(resolvedKey)) { - if (inDeps('data') && isDefined($data)) { - setData($data) - } - if (inDeps('online') && isDefined(online)) { - setOnline(online) - } - if (inDeps('loading') && isDefined(loading)) { - setLoading(loading) - } - if (inDeps('error') && isDefined($error)) { - setError($error) - } - if (inDeps('completedAttempts') && isDefined(completedAttempts)) { - setCompletedAttempts(completedAttempts) + // 2. Handle state synchronization from other hooks + if ( + event.requestCallId !== deps.requestCallId && + !willSuspend.get(resolvedKey) + ) { + // Create a map to avoid the long if-chain + const stateUpdates = { + data: { value: $data, setter: deps.setData }, + online: { value: online, setter: deps.setOnline }, + loading: { value: loading, setter: deps.setLoading }, + error: { value: $error, setter: deps.setError }, + completedAttempts: { + value: completedAttempts, + setter: deps.setCompletedAttempts } } + + // Loop through the map to update state concisely using a transition + startTransition(() => { + for (const key in stateUpdates) { + const update = stateUpdates[key as keyof typeof stateUpdates] + if ( + deps.thisDeps[key as keyof typeof stateUpdates] && + isDefined(update.value) + ) { + update.setter(update.value) + } + } + }) } } @@ -1089,22 +1143,7 @@ export function useFetch( return () => { requestsProvider.removeListener(resolvedKey, waitFormUpdates) } - }, [ - thisDeps, - resolvedKey, - idString, - requestCallId, - forceMutate, - imperativeFetch, - setData, - setOnline, - setLoading, - setError, - setCompletedAttempts, - handleMutate, - url, - onMutate - ]) + }, [resolvedKey]) const reValidate = useCallback( async function reValidate() { @@ -1257,7 +1296,6 @@ export function useFetch( requestCallId, resolvedKey, revalidateOnMount, - suspense, canRevalidate, url, resolvedDataKey