From 403f76c3f8336466c9f75b3e1fc9572deaa2cb2e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 1 Dec 2019 20:14:34 +0800 Subject: [PATCH 1/2] implement suspense guards --- src/index.ts | 1 + src/use-swr-suspense.ts | 32 ++++++++++++++++++++++++++++++++ src/use-swr.ts | 24 +++++++++++++++++------- test/use-swr.test.tsx | 39 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/use-swr-suspense.ts diff --git a/src/index.ts b/src/index.ts index 26030211d..b74fd52dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,5 @@ export { keyInterface, responseInterface } from './types' +export { useSWRSuspenseStart, useSWRSuspenseEnd } from './use-swr-suspense' export default useSWR diff --git a/src/use-swr-suspense.ts b/src/use-swr-suspense.ts new file mode 100644 index 000000000..a61855ab4 --- /dev/null +++ b/src/use-swr-suspense.ts @@ -0,0 +1,32 @@ +// TODO: add documentation + +const suspenseGroup = { + promises: [], + started: false +} + +function useSWRSuspenseStart() { + if (suspenseGroup.started) { + suspenseGroup.started = false + throw new Error('Wrong order of SWR suspense guards.') + } + suspenseGroup.started = true + suspenseGroup.promises = [] +} + +function useSWRSuspenseEnd() { + if (!suspenseGroup.started) { + throw new Error('Wrong order of SWR suspense guards.') + } + if (!suspenseGroup.promises.length) { + suspenseGroup.started = false + return + } + suspenseGroup.started = false + throw Promise.race(suspenseGroup.promises).then(() => { + // need to clean up the group + suspenseGroup.promises = [] + }) +} + +export { suspenseGroup, useSWRSuspenseStart, useSWRSuspenseEnd } diff --git a/src/use-swr.ts b/src/use-swr.ts index d9cd83306..0a682f814 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -31,6 +31,7 @@ import defaultConfig, { cacheGet, cacheSet } from './config' +import { suspenseGroup } from './use-swr-suspense' import SWRConfigContext from './swr-config-context' import isDocumentVisible from './libs/is-document-visible' import useHydration from './libs/use-hydration' @@ -186,9 +187,12 @@ function useSWR( fn = config.fetcher } + const inSuspenseGroup = suspenseGroup.started + const inSuspense = config.suspense || inSuspenseGroup + // it is fine to call `useHydration` conditionally here - // because `config.suspense` should never change - const shouldReadCache = config.suspense || !useHydration() + // because suspense mode should never be toggled + const shouldReadCache = inSuspense || !useHydration() const initialData = (shouldReadCache ? cacheGet(key) : undefined) || config.initialData const initialError = shouldReadCache ? cacheGet(keyErr) : undefined @@ -490,7 +494,7 @@ function useSWR( }, [key, config.refreshInterval, revalidate]) // suspense - if (config.suspense) { + if (inSuspense) { if (IS_SERVER) throw new Error('Suspense on server side is not yet supported!') @@ -517,11 +521,17 @@ function useSWR( typeof CONCURRENT_PROMISES[key].then === 'function' ) { // if it is a promise - throw CONCURRENT_PROMISES[key] + if (inSuspenseGroup) { + // we don't throw here if it's in suspense group + suspenseGroup.promises.push(CONCURRENT_PROMISES[key]) + } else { + // single suspense swr + throw CONCURRENT_PROMISES[key] + } + } else { + // it's a value, return it directly (override) + latestData = CONCURRENT_PROMISES[key] } - - // it's a value, return it directly (override) - latestData = CONCURRENT_PROMISES[key] } if (typeof latestData === 'undefined' && latestError) { diff --git a/test/use-swr.test.tsx b/test/use-swr.test.tsx index 6c45d5da1..0053c7f6d 100644 --- a/test/use-swr.test.tsx +++ b/test/use-swr.test.tsx @@ -7,7 +7,13 @@ import { } from '@testing-library/react' import React, { ReactNode, Suspense, useEffect, useState } from 'react' -import useSWR, { mutate, SWRConfig, trigger } from '../src' +import useSWR, { + mutate, + SWRConfig, + trigger, + useSWRSuspenseStart, + useSWRSuspenseEnd +} from '../src' class ErrorBoundary extends React.Component<{ fallback: ReactNode }> { state = { hasError: false } @@ -833,4 +839,35 @@ describe('useSWR - suspense', () => { // 'suspense-7' -> undefined -> 'suspense-8' expect(renderedResults).toEqual(['suspense-7', 'suspense-8']) }) + + it('should work with suspense guards', async () => { + const fetcher = () => new Promise(res => setTimeout(() => res('data'), 100)) + + function Section() { + useSWRSuspenseStart() + const { data: a } = useSWR('suspense-guards-1', fetcher) + const { data: b } = useSWR('suspense-guards-2', fetcher) + const { data: c } = useSWR('suspense-guards-3', fetcher) + useSWRSuspenseEnd() + + expect(a).toBe('data') + expect(b).toBe('data') + expect(c).toBe('data') + + return ( +
+ {a}, {b}, {c} +
+ ) + } + const { container } = render( + fallback}> +
+ + ) + + expect(container.textContent).toMatchInlineSnapshot(`"fallback"`) + await act(() => new Promise(res => setTimeout(res, 110))) // it only takes 100ms + expect(container.textContent).toMatchInlineSnapshot(`"data, data, data"`) + }) }) From fdebfea7af242fec8b433e683824475907005cf0 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 3 Dec 2019 02:14:23 +0800 Subject: [PATCH 2/2] adopt the callback form to avoid misuse --- src/index.ts | 2 +- src/use-swr-suspense.ts | 20 +++++++++++++++++--- test/use-swr.test.tsx | 21 +++++++++------------ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index b74fd52dc..fa60245c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,5 +8,5 @@ export { keyInterface, responseInterface } from './types' -export { useSWRSuspenseStart, useSWRSuspenseEnd } from './use-swr-suspense' +export { useSWRSuspense } from './use-swr-suspense' export default useSWR diff --git a/src/use-swr-suspense.ts b/src/use-swr-suspense.ts index a61855ab4..41ba5ac77 100644 --- a/src/use-swr-suspense.ts +++ b/src/use-swr-suspense.ts @@ -1,11 +1,13 @@ // TODO: add documentation +import useSWR from './use-swr' + const suspenseGroup = { promises: [], started: false } -function useSWRSuspenseStart() { +function _internal_useSWRSuspenseStart() { if (suspenseGroup.started) { suspenseGroup.started = false throw new Error('Wrong order of SWR suspense guards.') @@ -14,7 +16,7 @@ function useSWRSuspenseStart() { suspenseGroup.promises = [] } -function useSWRSuspenseEnd() { +function _internal_useSWRSuspenseEnd() { if (!suspenseGroup.started) { throw new Error('Wrong order of SWR suspense guards.') } @@ -29,4 +31,16 @@ function useSWRSuspenseEnd() { }) } -export { suspenseGroup, useSWRSuspenseStart, useSWRSuspenseEnd } +function useSWRSuspense(callback) { + _internal_useSWRSuspenseStart() + const data = callback(useSWR) + _internal_useSWRSuspenseEnd() + return data +} + +export { + suspenseGroup, + useSWRSuspense, + _internal_useSWRSuspenseStart, + _internal_useSWRSuspenseEnd +} diff --git a/test/use-swr.test.tsx b/test/use-swr.test.tsx index 0053c7f6d..4bd025f7d 100644 --- a/test/use-swr.test.tsx +++ b/test/use-swr.test.tsx @@ -7,13 +7,7 @@ import { } from '@testing-library/react' import React, { ReactNode, Suspense, useEffect, useState } from 'react' -import useSWR, { - mutate, - SWRConfig, - trigger, - useSWRSuspenseStart, - useSWRSuspenseEnd -} from '../src' +import useSWR, { mutate, SWRConfig, trigger, useSWRSuspense } from '../src' class ErrorBoundary extends React.Component<{ fallback: ReactNode }> { state = { hasError: false } @@ -844,12 +838,15 @@ describe('useSWR - suspense', () => { const fetcher = () => new Promise(res => setTimeout(() => res('data'), 100)) function Section() { - useSWRSuspenseStart() - const { data: a } = useSWR('suspense-guards-1', fetcher) - const { data: b } = useSWR('suspense-guards-2', fetcher) - const { data: c } = useSWR('suspense-guards-3', fetcher) - useSWRSuspenseEnd() + const [a, b, c] = useSWRSuspense(swr => { + const { data: a_ } = swr('suspense-guards-1', fetcher) + const { data: b_ } = swr('suspense-guards-2', fetcher) + // you can use `useSWR` too but the linter might yell + const { data: c_ } = useSWR('suspense-guards-3', fetcher) + return [a_, b_, c_] + }) + // will be executed after *all* SWRs inside are resolved expect(a).toBe('data') expect(b).toBe('data') expect(c).toBe('data')