Repository: zeit/swr (ts)

Issue:
Mention React Native support on readme

Pull request description:
Improve readme
- [x] mention React Native, close #99
- [x] add args array example
- [ ] ~#70~ will do it in a separate PR


Patch:
diff --git a/README.md b/README.md
index f2ef10c..2747b59 100644
--- a/README.md
+++ b/README.md
@@ -33,12 +33,15 @@ It features:
 - Pagination
 - TypeScript ready
 - Suspense mode
+- React Native support
 - Minimal API
 
 ...and a lot more.
 
 With SWR, components will get **a stream of data updates constantly and automatically**. Thus, the UI will be always **fast** and **reactive**.
 
+<br/>
+
 ## Quick Start
 
 ```js
@@ -66,6 +69,8 @@ library to handle that part.
 
 Check out [swr.now.sh](https://swr.now.sh) for more demos of SWR.
 
+<br/>
+
 ## Usage
 
 Inside your React project directory, run the following:
@@ -88,7 +93,7 @@ const { data, error, isValidating, revalidate } = useSWR(key, fetcher, options)
 
 #### Parameters
 
-- `key`: a unique key string for the request (or a function / null) [(advanced usage)](#conditional-fetching)  
+- `key`: a unique key string for the request (or a function / array / null) [(advanced usage)](#conditional-fetching)  
 - `fetcher`: (_optional_) a Promise returning function to fetch your data [(details)](#data-fetching) 
 - `options`: (_optional_) an object of options for this SWR hook
 
@@ -114,18 +119,22 @@ 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.
 
 You can also use [global configuration](#global-configuration) to provide default options.
 
+<br/>
+
 ## Examples
 
 - [Global Configuration](#global-configuration)
 - [Data Fetching](#data-fetching)
 - [Conditional Fetching](#conditional-fetching)
 - [Dependent Fetching](#dependent-fetching)
+- [Multiple Arguments](#multiple-arguments)
 - [Manually Revalidate](#manually-revalidate)
 - [Local Mutation](#local-mutation)
 - [Suspense Mode](#suspense-mode)
@@ -133,9 +142,9 @@ You can also use [global configuration](#global-configuration) to provide defaul
 
 ### Global Configuration
 
-You can use `SWRConfig` to provide global configurations (`options`) for all SWR hooks. 
+The context `SWRConfig` can provide global configurations (`options`) for all SWR hooks. 
 
-In this example, all `useSWR` hooks will use the same fetcher provided to load JSON data, and refresh every 3 seconds (except the user API):
+In this example, all SWRs will use the same fetcher provided to load JSON data, and refresh every 3 seconds by default:
 
 ```js
 import useSWR, { SWRConfig } from 'swr'
@@ -143,7 +152,7 @@ import useSWR, { SWRConfig } from 'swr'
 function Dashboard () {
   const { data: events } = useSWR('/api/events')
   const { data: projects } = useSWR('/api/projects')
-  const { data: user } = useSWR('/api/user', { refreshInterval: 0 })
+  const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // don't refresh
   // ...
 }
 
@@ -164,7 +173,7 @@ function App () {
 ### Data Fetching
 
 `fetcher` is a function **accepts the `key`** of SWR, and returns a value or a Promise.  
-You can use any library you to handle data fetching, for example:
+You can use any library to handle data fetching, for example:
 
 ```js
 import fetch from 'unfetch'
@@ -200,6 +209,8 @@ function App () {
 }
 ```
 
+_If you want to pass variables to a GraphQL query, check out [Multiple Arguments](#multiple-arguments)._
+
 Note that `fetcher` can be skipped from the parameters if it's provided gloablly.
 
 ### Conditional Fetching
@@ -235,9 +246,31 @@ function MyProjects () {
 }
 ```
 
+### Multiple Arguments
+
+In some scenarios, it's useful pass multiple arguments (can be any value or object) to the `fetcher` function. For example:
+
+```js
+const token = props.token
+
+useSWR('/api/data', url => fetchWithToken(url, token))
+```
+
+**This is incorrect**. Because the identifier of the data is `'/api/data'`, which is also the index of the cache. 
+When `token` changes, SWR will still treat it as the same key and request. 
+
+Instead, you can use an array as the `key` parameter, which contains multiple arguments of `fetcher`:
+
+```js
+useSWR(['/api/data', token], fetchWithToken)
+```
+
+This solves the problem. The identifier of the request is now the combination of both values. SWR **shallowly** compares
+the arguments on every render, and triggers the validation if any of them has changed.
+
 ### Manually Revalidate
 
-You can broadcast a revalidation message to all SWR data inside any component by calling
+You can broadcast a revalidation message globally to all SWRs with the same key by calling
 `trigger(key)`.
 
 This example shows how to automatically refetch the login info (e.g.: inside `<Profile/>`) 
@@ -336,6 +369,8 @@ useSWR(key, fetcher, {
 })
 ```
 
+<br/>
+
 ## Authors
 - Shu Ding ([@shuding_](https://twitter.com/shuding_)) – [ZEIT](https://zeit.co)
 - Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) – [ZEIT](https://zeit.co)
@@ -344,5 +379,7 @@ useSWR(key, fetcher, {
 
 Thanks to Ryan Chen for providing the awesome `swr` npm package name!
 
+<br/>
+
 ## License
 The MIT License.
diff --git a/examples/local-state-sharing/libs/store.js b/examples/local-state-sharing/libs/store.js
new file mode 100644
index 0000000..344662c
--- /dev/null
+++ b/examples/local-state-sharing/libs/store.js
@@ -0,0 +1,3 @@
+const initialStore = { name: "john" };
+
+export default initialStore;
diff --git a/examples/local-state-sharing/package.json b/examples/local-state-sharing/package.json
new file mode 100644
index 0000000..3584ad4
--- /dev/null
+++ b/examples/local-state-sharing/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "local-state-sharing",
+  "version": "1.0.0",
+  "main": "index.js",
+  "license": "MIT",
+  "dependencies": {
+    "next": "9.1.1",
+    "swr": "link:../.."
+  },
+  "scripts": {
+    "dev": "next",
+    "start": "next start",
+    "build": "next build"
+  }
+}
diff --git a/examples/local-state-sharing/pages/index.js b/examples/local-state-sharing/pages/index.js
new file mode 100644
index 0000000..966016f
--- /dev/null
+++ b/examples/local-state-sharing/pages/index.js
@@ -0,0 +1,52 @@
+import { useState } from "react"
+import initialStore from "../libs/store"
+import useSWR, { mutate } from "swr"
+
+function Profile() {
+  const { data } = useSWR("globalState", () => Promise.resolve(initialStore))
+  const [value, updateValue] = useState((data || {}).name)
+  if (!data) {
+    return null
+  }
+  return (
+    <div>
+      <h1>My name is {data.name}.</h1>
+      <input
+        value={value}
+        onChange={e => updateValue(e.target.value)}
+        style={{ width: 200, marginRight: 8 }}
+      />
+      <button
+        type="button"
+        onClick={() => {
+          mutate("globalState", { ...data, name: value })
+        }}
+      >
+        Uppercase my name!
+      </button>
+    </div>
+  )
+}
+
+function Other() {
+  const { data } = useSWR("globalState", () => Promise.resolve(initialStore))
+  if (!data) {
+    return null
+  }
+  return (
+    <div style={{ border: "1px solid #ddd", marginTop: 30, padding: 20 }}>
+      <h1>
+        Another Component: <br />
+        My name is {data.name}.
+      </h1>
+    </div>
+  )
+}
+
+export default () => (
+  <div style={{ padding: 40 }}>
+    useSWR can share state between components:
+    <Profile />
+    <Other />
+  </div>
+)
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/package.json b/package.json
index 48a4648..82f21ba 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "swr",
-  "version": "0.1.8",
+  "version": "0.1.9",
   "description": "React Hooks library for remote data fetching",
   "main": "./dist/index.js",
   "types": "./dist/index.d.ts",
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',