# Render-as-You-Fetch (结合 Suspense)

Observable-hooks 提供了专门的 React Suspense 支持!支持同步模式!

也可以看看 suspense 样例项目。

# 使用响应式数据源的好处

# 多次推值

因为响应式实现了多次推值协议:

单次 多次
拉值 Function Iterator
推值 Promise Observable

你可以一直 next 推新的值而无须推一遍换一个数据源。

# 竞态条件

你无须为了解决资源争用的竞态条件而将数据源写到状态中 (opens new window)

const initialResource = fetchProfileData(0);

function App() {
  const [resource, setResource] = useState(initialResource);

直接 switchMap 即可。

# 高级控制

通过丰富的 Observable 操作符你可以轻易串联多个请求、超时处理、失败重试或者其它针对多个流的复杂操作。

# 使用

与 React 官方文档的 Render-as-You-Fetch (using Suspense) (opens new window) 一样,我们先定义数据源,然后在组件中结合 Suspense Context 使用。

# Observable Resource

ObservableResource 将 Observables 转换为类 Relay 的兼容 Suspense 的数据源。

// api.js
import { ObservableResource } from 'observable-hooks'

const postResource$$ = new Subject()

export const postsResource = new ObservableResource(postResource$$.pipe(
  switchMap(id => fakePostsXHR(id))
))

export function fetchPosts(id) {
  postResource$$.next(id)
}

# Observable Suspense Hook

你可以通过 resource.read() 读取数据源的值。但因为 Observable 可以多次推值,我们还需要在特定时机重新触发 Suspense。 ObservableResource 实例暴露了一个 shouldUpdate$$ Subject,当需要重新触发 Suspense 时它会产生值。

但你无须处理这些细节,Observable-hooks 还提供一个轻量的 hook useObservableSuspense 来正确地接入 Observable 数据源。

// App.jsx
import { useObservableSuspense } from 'observable-hooks'

import { postsResource, fetchPosts } from './api'

fetchPosts('crimx')

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading posts...</h1>}>
      <ProfileTimeline />
    </Suspense>
  )
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = useObservableSuspense(postsResource)
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  )
}

# Stale-While-Revalidate 模式

默认情况下 ObservableResource 把每个产生的值当作是“成功”的状态。这种情况下当新的值产生时,组件便直接重新渲染。

这也被叫做 Stale-While-Revalidate 模式, 一个从 HTTP RFC 5861 (opens new window) 广泛流传的缓存失效策略。

它首先从缓存中返回数据,然后发请求,最终切换到新的值。

# 多次触发 Suspense

要多次触发 Suspense, ObservableResource 还接受一个额外的回调参数用于判断新产生的值是否为“成功”状态,如果不是则重新触发 Suspense。

export const userResource = new ObservableResource(
  userResource$$,
  // Trigger Suspense on null and undefined
  value => value != null
)

在 TypeScript 中如果产生值最终得到的类型与传入的类型不一样,你可以将回调定义为断言函数:

interface Success {
  status: 'success'
  value: string
}

interface Pending {
  status: 'pending'
}

type State = Success | Pending

const input$$ = new Subject<State>()

const resource = new ObservableResource(
  input$$,
  (value: State): value is Success => value.status !== 'pending'
)

# 异常处理

Observables 产生的异常会被 ObservableResource 收集并作为渲染异常重新抛出。参照 React 文档 (opens new window)定义错误边界组件。

同时注意由于 RxJS 的设计,一个 Observable 一旦产生了异常便会被永远断流。要么避免异常传到 Observables 上,要么在子流中使用 catchError (opens new window)

如果 Observable 确实产生了异常,可以在需要恢复组件时(如错误边界组件中处理)重载数据源。 对于 Cold Observable 调用 resource.reload(),对于 Hot Observable 调用 resource.reload(newObservable$)。建议尽可能在 ObservableResource 使用 Cold Observable 以便重载。