Observable-hooks offers first-class React Suspense support! Concurrent mode safe!

Also see the suspense example project.

# Benefits of Observable as Data Source

# Multiple Push

Since Observable implements multiple push protocol:

SINGLE MULTIPLE Pull Function Iterator Push Promise Observable

You can just keep pushing next values for new requests instead of replacing the resource.

# Race Conditions

You don't need to solve race conditions by moving resource to states .

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

Just switchMap and consume the resource as usual.

# Advanced Control

With abundant Observable operators you can easily chain subsequent requests, add timeout and retries or other advanced operations over multiple streams.

Just like the Render-as-You-Fetch (using Suspense) in React Docs, we first define the data source, then use it directly in Components under Suspense context.

# Observable Resource

ObservableResource transforms Observables into Relay-like Suspense compatible resource.

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

You can read the resouce with resource.read() but since Observable is multiple push we may need to re-trigger Suspense at some point. ObservableResource instance exposes a shouldUpdate$$ Subject which emits values when Suspense should restart.

But you don't need to worry about that. Observable-hooks offers a lightweight hook useObservableSuspense to properly consume Observable Resources.

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 ( ) { const posts = useObservableSuspense ( postsResource ) return ( < ul > { posts . map ( post => ( < li key = { post . id } > { post . text } </ li > ) ) } </ ul > ) }

By default ObservableResource will treat every value as "success" value, which means when new value is emitted, the Component will just re-render itself with the new value.

This is also known as Stale-While-Revalidate, a cache invalidation strategy popularized by HTTP RFC 5861 .

It first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.

To re-trigger Suspense ObservableResource also accepts an extra function that determines if the value is of success state. If false then a Suspense is triggered.

export const userResource = new ObservableResource ( userResource$$ , value => value != null )

In TypeScript if the resulted type is different from the input you will have to define the function as type predicate.

interface Success { status : 'success' value : string } interface Pending { status : 'pending' } type State = Success | Pending const input$$ = new Subject < State > ( ) const resouce = new ObservableResource ( input$$ , ( value : State ) : value is Success => value . status !== 'pending' )

# Error Handling

Errors from Observables will be collected and re-thrown by ObservableResource as rendering errors. Define an error boundary following the instructions on React Docs .

Do note that due to the design of RxJS, once an error occurs in an observble, the observable is killed. You should prevent errors from reaching observables or catchError in sub-observables.