One of my favorite tools in the Angular framework is the Async pipe. It allows me to render the latest value of an Observable to the screen. The component will then re-render to display the next value of the observable each time it receives a new notification. Finally, the subscription to the observable will be disposed of as soon as the component dismouns. Automatically! It allows me to build my asynchronous code in neat, RxJs streams.
A common use would be to synchronize some value from a data store like NgRx and display it in a component with some adjustments such as formatting. I don't have to use the OnInit life cycle method to start the subscription, nor do I need to use the OnDestroy life cycle method to unsubscribe.
Now, I'm not here to tell you that you should be using RxJs with React. I know I can certainly build a simpler app without it but if I find myself working with many APIs and having tons of Promises to take care of, then I think RxJs is merited. For this reason, I'd like to share with you my personal implementation of the Async pipe from Angular as a React component.
A Simple Example Problem - Stock Prices
We'll build a theoretical, stock monitoring tool which receives a feed of stock prices that we can filter by each stock's ticker. We need a component which displays the ticker and the latest price for this stock. Let's assume that we have a service which provides this value as an Observable stream of number values which we need to reformat as a string with the dollar sign and a two point precision. Simple enough, right?
The Async pipe in Angular 2+
Our Angular component will consume the API from a service, format the latest value and print them to the screen.
import { Component, Input } from '@angular/core';
import { StockPricesService } from './stock-prices.service';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-stock-price',
templateUrl: 'stock-price.component.html'
})
export class StockPriceComponent {
@Input() ticker: string;
price$ = this.stockPrices.getStockPrice(this.ticker).pipe(
map((price: number) => `$${price.toFixed(2)}`)
);
constructor(private stockPrices: StockPricesService) {}
}
Our Component's HTML will look something like this:
<article>
<section>{{ ticker }}</section>
<section>{{ price$ | async }}</section>
</article>
Finally, we might use this component like this:
<app-stock-price [ticker]="'GE'"></app-stock-price>
The React version of the Stock Price Component
Here's how we might implement this same component in React:
import React, { useMemo } from "react";
import { map } from "rxjs/operators";
import { Async } from "./async";
import { useStockPrices } from "./use-stock-prices";
export interface StockPriceProps {
ticker: string;
}
export function StockPrice({ ticker }: StockPriceProps): JSX.Element {
const { getStockPrice } = useStockPrices();
const price$ = useMemo(
() => getStockPrice(ticker).pipe(map((price: number) => `$${price.toFixed(2)}`)),
[ticker]
);
return (
<article>
<section>{ticker}</section>
<section>
<Async>{price$}</Async>
</section>
</article>
);
}
Obviously, the Async
component doesn't exist yet so we'll have to implement it to get the desired effect. Here's how I did it:
import React, { PropsWithChildren, useState, useEffect } from "react";
import { Observable, isObservable, Subscription } from "rxjs";
type AsyncProps<T> = PropsWithChildren<{ observable?: Observable<T> }>;
export function Async<T = any>({
observable,
children
}: AsyncProps<T>): JSX.Element {
const [value, setValue] = useState<T | null>(null);
useEffect(() => {
let subscription: Subscription;
if (isObservable(observable)) {
subscription = observable.subscribe(setValue);
} else if (isObservable(children)) {
subscription = (children as Observable<T>).subscribe(setValue);
}
return () => subscription.unsubscribe();
}, [observable, children]);
return <>{value}</>;
}
Notice that we can also pass the observable as a prop like so:
<Async observable={price$} />
Is this even useful?
If you're a skeptical developer like I am, you may have noticed that this hardly saves us any trouble from just performing the deliberate subscription on the Stock Price component. We still need to use the useMemo
hook to apply the pipe transformation on the observable. It all comes down to how many observable s you want to synchronize your component to. If you only have one, then it's probably not worth it, but if you have more than one or two, or if you're just trying to display the observable directly without transformation, then it may very well be worth it.
What do you think?
Top comments (5)
npmjs.com/package/reactivex-react
React is just not geared towards using Observables, it's totally centered around Promises.
Agreed, you can however, call the
.toPromise()
method on the final Observable.Why does the return function unsubscribe?
The useEffect hook can optionally return a 'clean-up' function which will be called once the input, in this case the 'ticker' observable changes or if the component unmounts. So, if our Observable where to change then we'd unsubscribe from the first one before subscribing to the new one.