RxJS in a Single React Component (No Store/Redux)
Tags: rxjs, react
Published: 2021-08-31 00:46:05

After you've got the basics of reactive programming down, the next question is usually _'ok great, so how do I use this thing?'_. A quick search for using RxJS with React usually winds up at one of the following solutions:

A. Use Redux with `redux-observable` middleware.

B. Write your own store that's very similar to redux, but powered by RxJS.

While both are valid solutions, they don't really help if you're only looking to use RxJS in a single component/hook. You don't want a global store!



## TL;DR

If you just want to see the hook, and an example here it is.

**useObserve()**

```ts
import {useEffect, useMemo, useState} from 'react'
import {Subject} from 'rxjs'

export function useObserve<T>(value: T) {
  const [ready, setReady] = useState(false)
  const subject = useMemo(() => new Subject<T>(), [])

  useEffect(() => {
    if (!ready) {
      return
    }

    subject.next(value)
  }, [value, ready, subject])

  const onReady = useMemo(() => {
    return ready ? null : () => setReady(true)
  }, [ready])

  return {value$: subject, onReady}
}

``` 

And here is an example of it in action:

```ts
export function usePriceForCredits(numCredits: number) {
  const [loading, setLoading] = useState(true)
  const [price, setPrice] = useState<number | null>(null)
  const {value$, onReady} = useObserve(numCredits)

  useEffect(() => {
    if (!onReady) {
      return
    }

    value$
      .pipe(
        tap(() => {
          setLoading(true)
          setPrice(null)
        }),
        debounceTime(1000),
        switchMap((numCredits: number) => {
          const url = api(`/price_for_credits?num_credits=${numCredits}`)

          const request = ajax.get(url, {
            'Content-Type': 'application/json', // Avoid rxjs from serializing data into [object, object]
          })

          return request
        }),
        map((res) => res.response.price),
        tap(() => {
          setLoading(false)
        }),
      )
      .subscribe({
        next: setPrice,
      })

    onReady()
  }, [value$, onReady, token])

  return {
    loading: loading,
    price: price,
  }
}
```

## Breaking It Down

If you're curious about how I got to the above solution, let's keep going. 

I'll be creating a custom hook that calculates the price given a number of credits:

- The number of credits is updated via a slider.
- If we fetched the price on every change we'd be sending way too many requests.
- Want to debounce sending requests so we only send once after the user has stopped sliding.

A perfect case for some rx!

### Creating the Observable

Here's our hook:

```ts
export function usePriceForCredits(numCredits: number) {
  // ...
}
```

We want to observer whenever `numCredits` changes. Let's manually send updated values whenever it changes.


_Side note: `redux-observable` also uses `Subject` under the hood._

```ts
 function usePriceForCredits(numCredits: number) {
  const subject = useMemo(() => new Subject<number>(), [])

  useEffect(() => {
    if(!subject) {
      return
     }
    
     subject.next(numCredits)
  }, [numCredits, subject])
}
```

- We wrap subject in a `useMemo` to avoid React creating new `Subject` on every render.
- `useEffect` to handle when `numCredits` changes.
- `subject.next()` sends a new value to the subject.

### Writing the pipeline

Now on to the fun part! With our new observable (subject) we can write the actual pipeline that does the work.

```ts
 const [price, setPrice] = useState<number | null>(null)

  useEffect(() => {
    subject
      .pipe(
        tap(() => {
          setPrice(null)
        }),
        debounceTime(1000),
        switchMap((numCredits: number) => {
          const url = api(`/price_for_credits?num_credits=${numCredits}`)

          const request = ajax.get(url, {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json', // Avoid rxjs from serializing data into [object, object]
          })

          return request
        }),
        map((res) => res.response.price),
      )
      .subscribe({
        next: setPrice,
      })
  }, [subject, token])
```

- Set in a `useEffect` to avoid subscribing on every render.
- Use `tap` for side-effects
- `debounceTime(1000)` - The debounce we needed!
- `switchMap()` - returning an `ajax` observable that'll automatically cancel requests for us. 
- Finally, `.subscribe({next: ...})`  to kick off the subscription. In this example we're just setting the value via `setPrice`

### A Bug!

Eagle-eyed readers might have spotted it, but there's actually a race-condition in the code above. The initial value is sent before the subscription is ready! This results in us always missing the first value.

In this example we'll need to fetch the price for the initial number of credits to so users don't start with a 0 price.

```ts
 const [ready, setReady] = useState(false)

  useEffect(() => {
    if (!ready) {
      return
    }
    subject.next(numCredits)
  }, [numCredits, subject, ready])


  useEffect(() => {
    if (ready) {
      return
    }

    subject
      .pipe(
        //... same as above
      )
      .subscribe(
        //... same as above
      )

   setReady(true)
  }, [subject, token])
```

- Introduce a `ready` flag to know when to start sending values
- Set `ready` to `true` only **after** pipeline is set.