Repository: zeit/swr (ts)

Issue:
Example for Suspense
Provide a simple and clear example to show SWR + Suspense, with [error handling](https://reactjs.org/docs/concurrent-mode-suspense.html#handling-errors) and data updates.


Pull request:
demo: suspense demo
close #90

This pull request adds a Suspense example.

With `<Suspense>`:

![Snipaste_2019-11-09_16-22-51](https://user-images.githubusercontent.com/12122021/68525502-31039d00-030d-11ea-85b0-40391f396b11.png)

With `<ErrorBoundary>`, when the application fails to get response (e.g the user is offline):

![Snipaste_2019-11-09_16-19-57](https://user-images.githubusercontent.com/12122021/68525495-29dc8f00-030d-11ea-88b3-7e4e436e9b5a.png)



Patch:
diff --git a/README.md b/README.md
index f2ef10c..28a0c91 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,7 @@ const { data, error, isValidating, revalidate } = useSWR(key, fetcher, options)
 - `onSuccess`: callback function when a request finishs successfully
 - `onError`: callback function when a request returns an error
 - `onErrorRetry`: handler for [error retry](#error-retries)
+- `initialData`: initial data to be returned (note: This is per-hook)
 
 When under a slow network (2G, <= 70Kbps), `errorRetryInterval` will be 10s, and
 `loadingTimeout` will be 5s by default.
diff --git a/examples/suspense/components/error-handling.js b/examples/suspense/components/error-handling.js
new file mode 100644
index 0000000..19d9a74
--- /dev/null
+++ b/examples/suspense/components/error-handling.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+export default class ErrorBoundary extends React.Component {
+  state = { hasError: false, error: null }
+  static getDerivedStateFromError(error) {
+    return {
+      hasError: true,
+      error
+    }
+  }
+  render() {
+    if (this.state.hasError) {
+      return this.props.fallback
+    }
+    return this.props.children
+  }
+}
diff --git a/examples/suspense/libs/fetch.js b/examples/suspense/libs/fetch.js
new file mode 100644
index 0000000..c2f95fc
--- /dev/null
+++ b/examples/suspense/libs/fetch.js
@@ -0,0 +1,6 @@
+import fetch from 'isomorphic-unfetch'
+
+export default async function (...args) {
+  const res = await fetch(...args)
+  return await res.json()
+}
diff --git a/examples/suspense/package.json b/examples/suspense/package.json
new file mode 100644
index 0000000..a7d4f7e
--- /dev/null
+++ b/examples/suspense/package.json
@@ -0,0 +1,18 @@
+{
+    "name": "suspense",
+    "version": "1.0.0",
+    "main": "index.js",
+    "license": "MIT",
+    "dependencies": {
+      "react": "^16.11.0",
+      "react-dom": "^16.11.0",
+      "isomorphic-unfetch": "3.0.0",
+      "next": "9.1.1",
+      "swr": "latest"
+    },
+    "scripts": {
+      "dev": "next",
+      "start": "next start",
+      "build": "next build"
+    }
+  }
diff --git a/examples/suspense/pages/[user]/[repo].js b/examples/suspense/pages/[user]/[repo].js
new file mode 100644
index 0000000..9dcb025
--- /dev/null
+++ b/examples/suspense/pages/[user]/[repo].js
@@ -0,0 +1,47 @@
+import { Suspense } from 'react'
+import Link from 'next/link'
+import fetcher from '../../libs/fetch'
+import ErrorHandling from '../../components/error-handling'
+
+import useSWR from 'swr'
+
+const isServer = typeof window === 'undefined'
+
+const Detail = ({ id }) => {
+  const { data } = useSWR('/api/data?id=' + id, fetcher, { suspense: true })
+
+  return (
+    <>
+      {data ? (
+        <div>
+          <p>forks: {data.forks_count}</p>
+          <p>stars: {data.stargazers_count}</p>
+          <p>watchers: {data.watchers}</p>
+        </div>
+      ) : null}
+    </>
+  )
+}
+
+export default () => {
+  const id =
+    typeof window !== 'undefined' ? window.location.pathname.slice(1) : ''
+
+  return (
+    <div style={{ textAlign: 'center' }}>
+      <h1>{id}</h1>
+      {!isServer ? (
+        <Suspense fallback={<div>loading...</div>}>
+          <ErrorHandling fallback={<div>oooops!</div>}>
+            <Detail id={id}></Detail>
+          </ErrorHandling>
+        </Suspense>
+      ) : null}
+      <br />
+      <br />
+      <Link href="/">
+        <a>Back</a>
+      </Link>
+    </div>
+  )
+}
diff --git a/examples/suspense/pages/api/data.js b/examples/suspense/pages/api/data.js
new file mode 100644
index 0000000..54da954
--- /dev/null
+++ b/examples/suspense/pages/api/data.js
@@ -0,0 +1,34 @@
+import fetch from 'isomorphic-unfetch'
+
+const projects = [
+  'facebook/flipper',
+  'vuejs/vuepress',
+  'rust-lang/rust',
+  'zeit/next.js',
+  'emperor/clothes'
+]
+
+export default (req, res) => {
+  if (req.query.id) {
+    if (req.query.id === projects[4]) {
+      setTimeout(() => {
+        res.json({ msg: 'not found' })
+      })
+
+      return
+    }
+    // a slow endpoint for getting repo data
+    fetch(`https://api.github.com/repos/${req.query.id}`)
+      .then(res => res.json())
+      .then(data => {
+        setTimeout(() => {
+          res.json(data)
+        }, 2000)
+      })
+
+    return
+  }
+  setTimeout(() => {
+    res.json(projects)
+  }, 2000)
+}
diff --git a/examples/suspense/pages/index.js b/examples/suspense/pages/index.js
new file mode 100644
index 0000000..efae635
--- /dev/null
+++ b/examples/suspense/pages/index.js
@@ -0,0 +1,36 @@
+import { Suspense } from 'react'
+import Link from 'next/link'
+import fetcher from '../libs/fetch';
+
+import useSWR from 'swr'
+
+const isServer = typeof window === 'undefined'
+
+const Repos = () => {
+  const { data } = useSWR('/api/data', fetcher, { suspense: true })
+
+  return (
+    <>
+      {data.map(project => (
+        <p key={project}>
+          <Link href="/[user]/[repo]" as={`/${project}`}>
+            <a>{project}</a>
+          </Link>
+        </p>
+      ))}
+    </>
+  )
+}
+
+export default () => {
+  return (
+    <div style={{ textAlign: 'center' }}>
+      <h1>Trending Projects</h1>
+      {!isServer ? (
+        <Suspense fallback={<div>loading...</div>}>
+          <Repos></Repos>
+        </Suspense>
+      ) : null}
+    </div>
+  )
+}
diff --git a/src/types.ts b/src/types.ts
index 65b92c9..00f46da 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,9 @@
-export interface ConfigInterface<Data = any, Error = any> {
+export type fetcherFn<Data> = (...args: any) => Data | Promise<Data>
+export interface ConfigInterface<
+  Data = any,
+  Error = any,
+  Fn extends fetcherFn<Data> = fetcherFn<Data>
+> {
   errorRetryInterval?: number
   loadingTimeout?: number
   focusThrottleInterval?: number
@@ -8,8 +13,9 @@ export interface ConfigInterface<Data = any, Error = any> {
   refreshWhenHidden?: boolean
   revalidateOnFocus?: boolean
   shouldRetryOnError?: boolean
-  fetcher?: any
+  fetcher?: Fn
   suspense?: boolean
+  initialData?: Data
 
   onLoadingSlow?: (key: string, config: ConfigInterface<Data, Error>) => void
   onSuccess?: (
diff --git a/src/use-swr-pages.tsx b/src/use-swr-pages.tsx
index c22b6ba..d2babef 100644
--- a/src/use-swr-pages.tsx
+++ b/src/use-swr-pages.tsx
@@ -88,7 +88,7 @@ function App () {
 }
 */
 
-export function useSWRPages<OffsetType, Data, Error>(
+export function useSWRPages<OffsetType = any, Data = any, Error = any>(
   pageKey: string,
   pageFn: pageComponentType<OffsetType, Data, Error>,
   SWRToOffset: pageOffsetMapperType<OffsetType, Data, Error>,
diff --git a/src/use-swr.ts b/src/use-swr.ts
index 8fbd496..ca60f32 100644
--- a/src/use-swr.ts
+++ b/src/use-swr.ts
@@ -18,7 +18,8 @@ import {
   triggerInterface,
   mutateInterface,
   broadcastStateInterface,
-  responseInterface
+  responseInterface,
+  fetcherFn
 } from './types'
 
 import defaultConfig, {
@@ -111,14 +112,14 @@ function useSWR<Data = any, Error = any>(
 ): responseInterface<Data, Error>
 function useSWR<Data = any, Error = any>(
   key: keyInterface,
-  fn?: Function,
+  fn?: fetcherFn<Data>,
   config?: ConfigInterface<Data, Error>
 ): responseInterface<Data, Error>
 function useSWR<Data = any, Error = any>(
   ...args
 ): responseInterface<Data, Error> {
   let _key: keyInterface,
-    fn: Function | undefined,
+    fn: fetcherFn<Data> | undefined,
     config: ConfigInterface<Data, Error> = {}
   if (args.length >= 1) {
     _key = args[0]
@@ -157,7 +158,7 @@ function useSWR<Data = any, Error = any>(
   const shouldReadCache = config.suspense || !useHydration()
 
   // stale: get from cache
-  let [data, setData] = useState(shouldReadCache ? cacheGet(key) : undefined)
+  let [data, setData] = useState((shouldReadCache ? cacheGet(key) : undefined) || config.initialData)
   let [error, setError] = useState(
     shouldReadCache ? cacheGet(keyErr) : undefined
   )
@@ -322,7 +323,7 @@ function useSWR<Data = any, Error = any>(
     // and trigger a revalidation
 
     const currentHookData = dataRef.current
-    const latestKeyedData = cacheGet(key)
+    const latestKeyedData = cacheGet(key) || config.initialData
 
     // update the state if the key changed or cache updated
     if (


Test patch:
diff --git a/test/use-swr.test.tsx b/test/use-swr.test.tsx
index fcc1566..65639d2 100644
--- a/test/use-swr.test.tsx
+++ b/test/use-swr.test.tsx
@@ -1,13 +1,7 @@
-import React, { Suspense, ReactNode, useState, useEffect } from 'react'
-import {
-  cleanup,
-  render,
-  waitForDomChange,
-  act,
-  fireEvent
-} from '@testing-library/react'
+import { act, cleanup, fireEvent, render, waitForDomChange } from '@testing-library/react'
+import React, { ReactNode, Suspense, useEffect, useState } from 'react'
 
-import useSWR, { trigger, mutate, SWRConfig } from '../src'
+import useSWR, { mutate, SWRConfig, trigger } from '../src'
 
 class ErrorBoundary extends React.Component<{ fallback: ReactNode }> {
   state = { hasError: false }
@@ -188,39 +182,55 @@ describe('useSWR', () => {
     await act(() => new Promise(res => setTimeout(res, 100)))
     expect(container.textContent).toMatchInlineSnapshot(`"err err err"`)
   })
-
+  
   it('should accept object args', async () => {
     const obj = { v: 'hello' }
     const arr = ['world']
-
+    
     function Page() {
       const { data: v1 } = useSWR(
         ['args-1', obj, arr],
         (a, b, c) => a + b.v + c[0]
       )
-
+      
       // reuse the cache
       const { data: v2 } = useSWR(['args-1', obj, arr], () => 'not called!')
-
+      
       // different object
       const { data: v3 } = useSWR(
         ['args-2', obj, 'world'],
         (a, b, c) => a + b.v + c
       )
-
+      
       return (
         <div>
           {v1}, {v2}, {v3}
         </div>
       )
     }
-    const { container } = render(<Page />)
-
+    
+    const { container } = render(<Page/>)
+    
     await waitForDomChange({ container })
     expect(container.textContent).toMatchInlineSnapshot(
       `"args-1helloworld, args-1helloworld, args-2helloworld"`
     )
   })
+  
+  it('should accept initial data', async () => {
+    function Page() {
+      const { data } = useSWR('initial-data-1', () => 'SWR', { initialData: 'Initial' })
+      return <div>hello, {data}</div>
+    }
+  
+    const { container } = render(<Page/>)
+  
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"hello, Initial"`)
+    await waitForDomChange({ container }) // mount
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(
+      `"hello, SWR"`
+    )
+  })
 })
 
 describe('useSWR - refresh', () => {
@@ -625,14 +635,14 @@ describe('useSWR - suspense', () => {
 
   it('should render multiple SWR fallbacks', async () => {
     function Section() {
-      const { data: v1 } = useSWR(
+      const { data: v1 } = useSWR<number>(
         'suspense-2',
         () => new Promise(res => setTimeout(() => res(1), 100)),
         {
           suspense: true
         }
       )
-      const { data: v2 } = useSWR(
+      const { data: v2 } = useSWR<number>(
         'suspense-3',
         () => new Promise(res => setTimeout(() => res(2), 100)),
         {
@@ -672,7 +682,7 @@ describe('useSWR - suspense', () => {
     expect(container.textContent).toMatchInlineSnapshot(`"hello"`)
   })
 
-  it.only('should throw errors', async () => {
+  it('should throw errors', async () => {
     function Section() {
       const { data } = useSWR(
         'suspense-5',