diff --git a/src/use-swr.ts b/src/use-swr.ts index 1fd4b852d..c2a2f3ca1 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -178,14 +178,27 @@ export const useSWRHandler = ( // new request should be initiated. const shouldStartNewRequest = !FETCH[key] || !opts.dedupe - // Do unmount check for calls: - // If key has changed during the revalidation, or the component has been - // unmounted, old dispatch and old event callbacks should not take any - // effect. - const isCurrentKeyMounted = () => - !unmountedRef.current && - key === keyRef.current && - initialMountedRef.current + /* + For React 17 + Do unmount check for calls: + If key has changed during the revalidation, or the component has been + unmounted, old dispatch and old event callbacks should not take any + effect + + For React 18 + only check if key has changed + https://github.com/reactwg/react-18/discussions/82 + */ + const callbackSafeguard = () => { + if (IS_REACT_LEGACY) { + return ( + !unmountedRef.current && + key === keyRef.current && + initialMountedRef.current + ) + } + return key === keyRef.current + } // The final state object when request finishes. const finalState: State = { @@ -197,7 +210,7 @@ export const useSWRHandler = ( setCache(finalState) // We can only set the local state if it's safe (still mounted with the same key). - if (isCurrentKeyMounted()) { + if (callbackSafeguard()) { setState(finalState) } } @@ -231,7 +244,7 @@ export const useSWRHandler = ( // we trigger the loading slow event. if (config.loadingTimeout && isUndefined(getCache().data)) { setTimeout(() => { - if (loading && isCurrentKeyMounted()) { + if (loading && callbackSafeguard()) { getConfig().onLoadingSlow(key, config) } }, config.loadingTimeout) @@ -264,7 +277,7 @@ export const useSWRHandler = ( // The timestamp maybe be `undefined` or a number if (!FETCH[key] || FETCH[key][1] !== startAt) { if (shouldStartNewRequest) { - if (isCurrentKeyMounted()) { + if (callbackSafeguard()) { getConfig().onDiscarded(key) } } @@ -298,7 +311,7 @@ export const useSWRHandler = ( ) { finishRequestAndUpdateState() if (shouldStartNewRequest) { - if (isCurrentKeyMounted()) { + if (callbackSafeguard()) { getConfig().onDiscarded(key) } } @@ -319,7 +332,7 @@ export const useSWRHandler = ( // Trigger the successful callback if it's the original request. if (shouldStartNewRequest) { - if (isCurrentKeyMounted()) { + if (callbackSafeguard()) { getConfig().onSuccess(newData, key, config) } } @@ -336,7 +349,7 @@ export const useSWRHandler = ( // Error event and retry logic. Only for the actual request, not // deduped ones. - if (shouldStartNewRequest && isCurrentKeyMounted()) { + if (shouldStartNewRequest && callbackSafeguard()) { currentConfig.onError(err, key, currentConfig) if ( shouldRetryOnError === true || @@ -371,7 +384,7 @@ export const useSWRHandler = ( // Here is the source of the request, need to tell all other hooks to // update their states. - if (isCurrentKeyMounted() && shouldStartNewRequest) { + if (callbackSafeguard() && shouldStartNewRequest) { broadcastState(cache, key, finalState) } @@ -551,9 +564,7 @@ export const useSWRHandler = ( // without providing any initial data. See: // https://github.com/vercel/swr/issues/1832 if (!IS_REACT_LEGACY && IS_SERVER) { - throw new Error( - 'Fallback data is required when using suspense in SSR.' - ) + throw new Error('Fallback data is required when using suspense in SSR.') } // Always update fetcher and config refs even with the Suspense mode. diff --git a/src/utils/broadcast-state.ts b/src/utils/broadcast-state.ts index 011d306e0..491cf0fec 100644 --- a/src/utils/broadcast-state.ts +++ b/src/utils/broadcast-state.ts @@ -1,5 +1,5 @@ import { Broadcaster } from '../types' -import { SWRGlobalState, GlobalState } from './global-state' +import { SWRGlobalState } from './global-state' import * as revalidateEvents from '../constants' import { createCacheHelper } from './cache' @@ -10,33 +10,34 @@ export const broadcastState: Broadcaster = ( revalidate, broadcast = true ) => { - const [EVENT_REVALIDATORS, STATE_UPDATERS, , FETCH] = SWRGlobalState.get( - cache - ) as GlobalState - const revalidators = EVENT_REVALIDATORS[key] - const updaters = STATE_UPDATERS[key] + const stateResult = SWRGlobalState.get(cache) + if (stateResult) { + const [EVENT_REVALIDATORS, STATE_UPDATERS, , FETCH] = stateResult + const revalidators = EVENT_REVALIDATORS[key] + const updaters = STATE_UPDATERS[key] - const [get] = createCacheHelper(cache, key) + const [get] = createCacheHelper(cache, key) - // Cache was populated, update states of all hooks. - if (broadcast && updaters) { - for (let i = 0; i < updaters.length; ++i) { - updaters[i](state) + // Cache was populated, update states of all hooks. + if (broadcast && updaters) { + for (let i = 0; i < updaters.length; ++i) { + updaters[i](state) + } } - } - // If we also need to revalidate, only do it for the first hook. - if (revalidate) { - // Invalidate the key by deleting the concurrent request markers so new - // requests will not be deduped. - delete FETCH[key] + // If we also need to revalidate, only do it for the first hook. + if (revalidate) { + // Invalidate the key by deleting the concurrent request markers so new + // requests will not be deduped. + delete FETCH[key] - if (revalidators && revalidators[0]) { - return revalidators[0](revalidateEvents.MUTATE_EVENT).then( - () => get().data - ) + if (revalidators && revalidators[0]) { + return revalidators[0](revalidateEvents.MUTATE_EVENT).then( + () => get().data + ) + } } - } - return get().data + return get().data + } } diff --git a/src/utils/env.ts b/src/utils/env.ts index 732e85447..9ad54dd52 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -1,7 +1,9 @@ import React, { useEffect, useLayoutEffect } from 'react' import { hasRequestAnimationFrame, isWindowDefined } from './helper' -export const IS_REACT_LEGACY = !(React as any).useId +// @ts-expect-error TODO: should remove this when the default react version is 18 +export const IS_REACT_LEGACY = !React.useId + export const IS_SERVER = !isWindowDefined || 'Deno' in window // Polyfill requestAnimationFrame