We often need to fetch data in our components. Here's an example using useState hook and fetch API to get and display some data:
Looks alright?
Well, this approach lacks few important features:
- cancelling fetching on component unmount (e.g if user leaves current page)
- handling errors
- displaying loading indicator
To handle all these issues nicely we'll use RxJS!
RxJS is a very mighty tool to manage and coordinate async events (like fetching and UI events). Learning it will pay you back 10 fold!
Please, don't get freaked out now, I'll walk you through adding and using it ๐
tl;dr: resulting app playground and <$> fragment library
Let's start with updating our App to use RxJS!
๐ Power Up
First we'll switch to RxJS' fromFetch โ it's a wrapper around native fetch:
function App(){
const [data, setData] = useState(null);
useEffect(() => {
fromFetch('//...')
.subscribe(response =>
response.json().then(data => setData(data))
);
}, []);
return <div>Data: { data }</div>
}
.subscribe
method is an analogue for .then
in Promises โ it will receive value updates from the RxJS stream (currently it will handle only one update, but there'll be more)
Also .subscribe
returns an object with which we can cancel the "subscription". This will help us solve our first issue: cancelling fetching on component unmount.
function App(){
const [data, setData] = useState(null);
useEffect(() => {
const subscription = fromFetch('//...')
.subscribe(response =>
response.json().then(data => setData(data))
);
// this function will be called on component unmount
// it will terminate the fetching
return () => subscription.unsubscribe();
}, []);
return <div>Data: { data }</div>
}
See React's useEffect#cleaning-up-an-effect docs section for details
Hurray: 1 done, 2 left!
Let's do a small cleanup before we go further:
๐ง Refactoring and <$> fragment
As you can see, we're using response.json()
async operation inside our subscribe
function โ this is a bad practice for a number of reasons: this stream would not be reusable and cancellation wont work if we're already on stage of response.json()
parsing.
We'll use a mergeMap
RxJS operator to fix this:
function App(){
const [data, setData] = useState(null);
useEffect(() => {
const subscription = fromFetch('//...')
.pipe(
// mergeMap is an operator to do another async task
mergeMap(response => response.json())
)
.subscribe(data => setData(data));
return () => subscription.unsubscribe();
}, []);
return <div>Data: { data }</div>
}
UPD: @benlesh made a good point that one can use RxJS' ajax.getJSON instead of fetch wrapper, and skip the mergeMap
. E.g.: ajax.getJSON(url).subscribe(/* etc. */)
. I will keep the fromFetch
approach for educational and laziness reasons ๐
We've separated response.json()
operation from results handling. And with our subscribe
handler only responsible for displaying data โ we can now use <$>
fragment!
<$> โ is a small (1Kb) package to display RxJS values in our React components.
It will subscribe to provided stream for us and display updates in place. And also unsubscribe on component unmount, so we won't need to worry about that too!
function App(){
// we need useMemo to ensure stream$ persist
// between App re-renders
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json())
)
, []);
return <div>Data: <$>{ stream$ }</$></div>
}
Note that we've dropped useState
and .subscribe
: <$> does all that!
So, we're ready to add more operators to continue solving our tasks. Let's add a loading indicator!
โณ Loading indicator
function App(){
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json()),
// immediately show a loading text
startWith('loading...')
)
, []);
return <div>Data: <$>{ stream$ }</$></div>
}
startWith
will prepend async data stream with provided value. In our case it looks somewhat like this:
start -o---------------------------o- end
^ show 'loading' ^ receive and display
| immediately | response later
Awesome: 2 done, 1 left!
We'll handle errors next:
โ ๏ธ Error handling
Another operator catchError
will let us handle error from fetching:
function App(){
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json()),
catchError(() => of('ERROR')),
startWith('loading...')
)
, []);
return <div>Data: <$>{ stream$ }</$></div>
}
Now if fetching fails โ we'll display 'ERROR' text.
If you want to dig deeper, I wrote a detailed article on managing errors: "Error handling in RxJS or how not to fail with Observables" โ suppressing, strategic fallbacks, retries simple and with exponential delays โ it's all there.
3 done, 0 left!
Let's finalize with moving some div
s around:
๐ผ Better UI
Most likely we'd like to show properly highlighted error and styled (maybe even animated) loading indicator. To do that โ we'll simply move our JSX right into the stream:
function App(){
const stream$ = useMemo(() =>
fromFetch('//...')
.pipe(
mergeMap(response => response.json()),
// now we'll map not only to text
// but to JSX
map(data => <div className="data">Data: { data }</div>),
catchError(() => of(<div className="err">ERROR</div>)),
startWith(<div className="loading">loading...</div>)
)
, []);
return <$>{ stream$ }</$>
}
Note that now we can fully customize view for each state!
๐ฐ Bonus: anti-flickering
Sometimes if the response comes too quickly we'll see the loading indicator flash for a split second. This is generally undesirable since we've worked long on our loading indicator animation and want to ensure user sees it through ๐
To fix this we'll split out fetching Observable creation and join the fetching with a 500ms delay:
function App(){
const stream$ = useMemo(() =>
customFetch('//...').pipe(
map(data => <div className="data">Data: { data }</div>),
catchError(() => of(<div className="err">ERROR</div>)),
startWith(<div className="loading">loading...</div>)
)
, []);
return <$>{ stream$ }</$>
}
function customFetch(URL) {
// wait for both fetch and a 500ms timer to finish
return zip(
fromFetch(URL).pipe( mergeMap(r => r.json()) ),
timer(500) // set a timer for 500ms
).pipe(
// then take only the first value (fetch result)
map(([data]) => data)
)
}
Now our loved user will see the loading animation for at least 500ms!
4 done, ๐ฐ left!
A few final words:
๐ Outro
Here's our resulting app if you want to play around with it.
To start using RxJS in your React components just do:
npm i rxjs react-rxjs-elements
And then drop a stream inside <$>
:
import { timer } from 'rxjs';
import { $ } from 'react-rxjs-elements';
function App() {
return <$>{ timer(0, 1000) } ms</$>
}
That's it, I hope you've learnt something new!
Thank you for reading this article! Stay reactive and have a nice day ๐
If you enjoyed reading โ please, indicate that with โค๏ธ ๐ฆ ๐ buttons
Follow me on twitter for more React, RxJS, and JS posts:
The End
Thanks to @niklas_wortmann and @sharlatta for reviewing!
Top comments (3)
Great stuff! It was a good read thanks!
Thanks! ๐
I got a bunch of other articles on Rx if you're interested ๐
Iโll check it out for sure!