From aa869c762043276d4eea458b57ec2c59bab02b66 Mon Sep 17 00:00:00 2001 From: CaptainPants <2021556+CaptainPants@users.noreply.github.com> Date: Mon, 12 Dec 2022 14:51:15 +1100 Subject: [PATCH] Rebased onto current master. Removing personal email. --- _internal/types.ts | 29 ++++++++++++-- core/use-swr.ts | 63 +++++++++++++++++++++++++++++-- test/use-swr-middlewares.test.tsx | 3 +- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/_internal/types.ts b/_internal/types.ts index 95b3cdac0..b2f765bf3 100644 --- a/_internal/types.ts +++ b/_internal/types.ts @@ -4,7 +4,15 @@ import type { defaultConfig } from './utils/config' export type GlobalState = [ Record, // EVENT_REVALIDATORS Record, // MUTATION: [ts, end_ts] - Record, // FETCH: [data, ts] + Record< + string, + [ + data: any, + timestamp: number, + revalidateCount: number, + abort: AbortController | undefined + ] + >, // FETCH: [data, timestamp, revalidateCount, abort] Record>, // PRELOAD ScopedMutator, // Mutator (key: string, value: any, prev: any) => void, // Setter @@ -14,15 +22,24 @@ export type FetcherResponse = Data | Promise export type BareFetcher = ( ...args: any[] ) => FetcherResponse +/** + * Second parameter provided to fetcher functions. At present this only allows + * for 'signal' for cancelling a request in progress, but allows for future + * enhancements. + */ +export interface FetcherOptions { + signal?: AbortSignal + [key: string]: unknown +} export type Fetcher< Data = unknown, SWRKey extends Key = Key > = SWRKey extends () => infer Arg | null | undefined | false - ? (arg: Arg) => FetcherResponse + ? (arg: Arg, options: FetcherOptions) => FetcherResponse : SWRKey extends null | undefined | false ? never : SWRKey extends infer Arg - ? (arg: Arg) => FetcherResponse + ? (arg: Arg, options: FetcherOptions) => FetcherResponse : never export type BlockingData< @@ -200,6 +217,12 @@ export interface PublicConfiguration< * @link https://swr.vercel.app/docs/advanced/react-native#customize-focus-and-reconnect-events */ isVisible: () => boolean + + /** + * Enable this to pass an instance of AbortSignal to your fetcher that will be aborted when there no useSWR hooks remaining that are listening for the result of the fetch (as + * a result for example of navigation, or change of search input). This allows you to cancel a long running a request if the result is no longer relevant, e.g. when doing a search. + */ + abortDiscardedRequests?: boolean } export type FullConfiguration< diff --git a/core/use-swr.ts b/core/use-swr.ts index ded17466d..1eb08bf13 100644 --- a/core/use-swr.ts +++ b/core/use-swr.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef, useDebugValue, useMemo } from 'react' +import { useCallback, useRef, useDebugValue, useMemo, useEffect } from 'react' import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js' import { @@ -25,6 +25,7 @@ import { import type { State, Fetcher, + FetcherOptions, Key, SWRResponse, RevalidatorOptions, @@ -104,6 +105,10 @@ export const useSWRHandler = ( const stateDependencies = useRef({}).current + // When starting a revalidate, record an abort callback here so that it + // can be called if a newer revalidation happens. + const abandonRef = useRef<(() => void) | undefined>() + const fallback = isUndefined(fallbackData) ? config.fallback[key] : fallbackData @@ -271,6 +276,7 @@ export const useSWRHandler = ( unmountedRef.current || getConfig().isPaused() ) { + abandonRef.current?.() return false } @@ -341,17 +347,58 @@ export const useSWRHandler = ( }, config.loadingTimeout) } + const options: FetcherOptions = {} + let abortController: AbortController | undefined + + if ( + config.abortDiscardedRequests && + typeof AbortController !== 'undefined' + ) { + abortController = new AbortController() + options.signal = abortController.signal + } + // Start the request and save the timestamp. // Key must be truthy if entering here. FETCH[key] = [ - currentFetcher(fnArg as DefinitelyTruthy), - getTimestamp() + currentFetcher(fnArg as DefinitelyTruthy, options), + getTimestamp(), + 0, // Number of revalidate calls waiting on this fetcher result + abortController ] } + const currentRequestInfo = FETCH[key] + + // Reference count the number of revalidate calls that are awaiting this request + currentRequestInfo[2] += 1 + abandonRef.current?.() + + if (config.abortDiscardedRequests) { + // Store an abort callback in abortRef to be called if/when the next revalidate happens + // or if the component is unmounted. Will only do anything the first time its invoked. + let abandoned = false + + abandonRef.current = () => { + if (!abandoned) { + abandoned = true + // Decrement the reference count + currentRequestInfo[2] -= 1 + // If this was the last awaiter then abort the request + if (currentRequestInfo[2] == 0) { + // Note: we may consider it OK to abort a request thats already + // resolved as its basically a no-op. The other option is to clear + // the abortcontroller immediately after the await so that its not + // aborted. + currentRequestInfo[3]?.abort() + } + } + } + } + // Wait until the ongoing request is done. Deduplication is also // considered here. - ;[newData, startAt] = FETCH[key] + ;[newData, startAt] = currentRequestInfo newData = await newData if (shouldStartNewRequest) { @@ -627,6 +674,14 @@ export const useSWRHandler = ( } }, [refreshInterval, refreshWhenHidden, refreshWhenOffline, key]) + // On final unmount + useEffect( + () => () => { + abandonRef.current?.() + }, + [] + ) + // Display debug info in React DevTools. useDebugValue(returnedData) diff --git a/test/use-swr-middlewares.test.tsx b/test/use-swr-middlewares.test.tsx index 1febe6174..a4d2b1048 100644 --- a/test/use-swr-middlewares.test.tsx +++ b/test/use-swr-middlewares.test.tsx @@ -2,6 +2,7 @@ import { act, screen } from '@testing-library/react' import React, { useState, useEffect, useRef } from 'react' import type { Middleware } from 'swr' import useSWR, { SWRConfig } from 'swr' +import type { FetcherSecondParameter } from 'swr/_internal' import { withMiddleware } from 'swr/_internal' import { @@ -242,7 +243,7 @@ describe('useSWR - middleware', () => { function Page() { const { data } = useSWR( [key, { hello: 'world' }], - (_, o) => { + (_, o: FetcherSecondParameter & { hello: string }) => { return o.hello }, {