observable-hooks

repository·main·Indexed 21 days ago

https://github.com/crimx/observable-hooks

A library providing Concurrent mode compatible React hooks for RxJS Observables. It enables integration between React's component model and RxJS reactive streams through hooks such as useObservable, useSubscription, useObservableState, and useObservableCallback for managing subscriptions, state extraction, and event bridging.

Tokens
19.8K
Snippets
57
Records
68
Agent score
76%

What's inside observable-hooks

  1. Overview of observable-hooks

    main

    observable-hooks provides React hooks designed specifically for working with RxJS Observables. It enables seamless integration between React's reactive primitives (props, state, context) and RxJS Observables using pure functions, avoiding common patterns like the tap hack.

    Key features include:

    • Concurrent Mode Safety: Designed to work safely with React's concurrent rendering.
    • Suspense Support: Supports the Render-as-You-Fetch pattern using React Suspense.
    • Full RxJS Power: Provides uncompromised access to the full RxJS API.
    • Performance: Optimized for minimal impact on application performance.
  2. When to use observable-hooks vs state management

    main

    Relationship with State Management

    observable-hooks is not a replacement for global state management tools like Redux. Instead, it is intended to reduce the need to dump complex, transient, or highly interactive asynchronous logic into a global state store.

    Best Practices

    • Use selectively: Do not turn everything into an Observable. Only use this library where complex asynchronous coordination is required.
    • Coexistence: It is designed to work side-by-side with standard React hooks and other state management solutions.
  3. Convert Observables to React State or Callbacks

    main

    To bring values from the Observable World into the Normal World, use the following hooks:

    Observable to State

    Use these hooks to subscribe to an observable and store its latest emitted value in React state, triggering re-renders:

    • useObservableState(input$, initialOutput)
    • useObservableEagerState(input$, initialOutput)
    • useLayoutObservableState(input$, initialOutput)

    Observable to Callbacks

    If you need to perform side effects (like logging or calling a function) whenever an observable emits, instead of managing state, use useSubscription. This is preferred over manual useEffect for managing RxJS subscriptions within React.

    useSubscription(input$, onNext)

    // Observable to State
    const output = useObservableState(input$, initialOutput);
    
    // Observable to Callbacks
    useSubscription(input$, v => log(v));
  4. Core Concepts of observable-hooks

    main

    Integration Model

    observable-hooks provides a bridge between React and RxJS. It is designed to handle:

    • React to RxJS: Converting props, state, and context into Observables.
    • RxJS to React: Converting Observables into React state, props, or events.
    • Conditional Rendering: Using streams of React Components for dynamic UI.
    • Data Fetching: Implementing 'Render-as-You-Fetch' patterns using React Suspense.

    When to use it

    • Use it to encapsulate complex sync and async logic within reusable components.
    • Use it when you want to leverage the power of RxJS (operators, marble testing, etc.) within a React lifecycle.
    • Note: It is not a replacement for global state management (like Redux), but a tool to reduce the need for global state by managing local complex logic via Observables. It is intended to work side-by-side with standard React hooks.
  5. Understand the Observable World vs. Normal World concept

    main

    The core design of observable-hooks is based on two conceptual partitions:

    1. Observable World: The domain where RxJS observable pipelines reside. These pipelines can exist inside or outside of React components.
    2. Normal World: Any part of your application that is not part of an observable pipeline (e.g., standard React component logic, state, props, and event handlers).

    observable-hooks acts as the bridge between these two worlds, allowing you to move data from observables into React state, or from React events into observable streams.

  6. Handle complex observable dependencies using Higher-order Dependencies pattern

    main

    For cleaner code when dealing with multiple observable dependencies, use the Higher-order Dependencies pattern. This involves separating standard React dependencies (like props or state) from observable dependencies.

    1. Create intermediate observables for your dependencies using useObservable or useObservableCallback.
    2. Pass those observables into the dependency array of your main useObservable call.
    3. Use switchMap within the transformation function to access these dependencies from the inputs$ stream. This prevents the transformation function from needing to know about the component's local scope.
    const [onChange, textChange$] = useObservableCallback(event$ => event$.pipe(...))
    
    // Create an intermediate observable for meta values
    const metaValues$ = useObservable(identity, [props.a, stateB])
    
    const enhanced$ = useObservable(
      inputs$ => inputs$.pipe(
        // Access dependencies via switchMap from the inputs stream
        switchMap(([metaValues$, textChange$]) => metaValues$.pipe(
          withLatestFrom(textChange$)
          // ...
        ))
      )),
      [metaValues$, textChange$] // Pass the observables as dependencies
    )
  7. Understand the purpose of observable-hooks

    main

    The observable-hooks library provides a simple, flexible, testable, and performant way to reuse complex asynchronous logic (such as intricate animation sequences or user interaction flows) within React Components.

    It is designed to bridge the gap between RxJS Observables and React Hooks. Instead of using hooks solely for simple state management, you can use observable-hooks to manage complex async logic that would otherwise be difficult to coordinate using standard React state and effects.

  8. Use ObservableResource for React Suspense

    main

    The ObservableResource class rewires an RxJS Observable into a Relay-like Suspense resource. It allows you to bridge the push-based nature of Observables with the pull-based nature of React Suspense, enabling a concurrent-mode safe way to handle asynchronous data fetching.

    // Conceptual usage pattern
    const resource = new ObservableResource(input$);
    const value = useObservableSuspense(resource);
  9. Handle observable dependencies using Direct Dependencies pattern

    main

    When an observable transformation depends on other observables created within the same component, you can use the Direct Dependencies pattern. Instead of referencing local variables directly inside the transformation function (which couples the logic to the component scope), you treat the required observables as part of the inputs$ stream.

    To do this, include the dependency observables in the useObservable dependency array. Inside the transformation function, you can then derive the necessary sub-observables from the inputs$ stream using RxJS operators like switchMap.

    const [onChange, textChange$] = useObservableCallback(event$ => event$.pipe(...))
    
    const enhanced$ = useObservable(
      inputs$ => {
        // Derive the dependency from the inputs stream
        const textChange$ = inputs$.pipe(
          distinctUntilKeyChanged(2),
          switchMap(inputs => inputs[2])
        )
        return inputs$.pipe(
          withLatestFrom(textChange$)
          // ...
        )
      },
      [props.a, stateB, textChange$] // Include the dependency here
    )
  10. Use observable-hooks with normal React Context values

    main

    If you are passing standard, non-observable values through React Context, you can use the useObservable hook to transform that value into an observable stream. By passing the context value into the dependency array of useObservable, the stream will re-evaluate whenever the context value changes.

    const normalValue = useContext(NormalValueContext)
    
    const normalValueList$ = useObservable(
      inputs$ => inputs$.pipe(
        scan((acc, inputs) => [...acc, ...inputs], []),
        take(10)
      ),
      [normalValue]
    )
  11. Use observable-hooks with Observable React Context values

    main

    You can pass Observables directly through React Context. To consume these, use useSubscription or state hooks like useObservableState.

    useSubscription will automatically handle unsubscribing from the previous observable and subscribing to the new one whenever the context value changes.

    Important: If you use useObservable to transform an observable retrieved from context, you must include that observable in the dependency array to ensure the transformation logic updates when the context provides a new stream.

    // Subscribing directly to an observable from context
    const num$ = useContext(ObservableValueContext)
    useSubscription(num$, value => {
      console.log('useSubscription', value)
    })
    
    // Using state hooks (which use useSubscription internally)
    const num$ = useContext(ObservableValueContext)
    const num = useObservableState(num$)
    
    // Transforming an observable from context
    const num$ = useContext(ObservableValueContext)
    
    const numList$ = useObservable(
      input$ => input$.pipe(
        switchMap(([num$]) =>
          num$.pipe(
            scan((acc, inputs) => [...acc, inputs], []),
            take(20)
          )
        )
      ),
      [num$]
    )
  12. Implement Render-as-You-Fetch with React Suspense

    main

    You can use observable-hooks to implement the Render-as-You-Fetch pattern with React Suspense. This approach is concurrent-mode safe and leverages Observables as a data source.

    Instead of moving resources into component state to handle race conditions, you can use RxJS operators like switchMap to manage data streams. This allows you to keep pushing new values (e.g., new request IDs) into the same resource rather than replacing the resource itself.

    import { useObservableSuspense } from 'observable-hooks'
    import { postsResource, fetchPosts } from './api'
    
    // Trigger the fetch
    fetchPosts('crimx')
    
    function ProfileTimeline() {
      // useObservableSuspense handles the Suspense lifecycle
      const posts = useObservableSuspense(postsResource)
      return (
        <ul>
          {posts.map(post => (
            <li key={post.id}>{post.text}</li>
          ))}
        </ul>
      )
    }