loading...
Cover image for Fetching Data in React with RxJS and <$> fragment
RxJS

Fetching Data in React with RxJS and <$> fragment

kosich profile image Kostia Palchyk ใƒป5 min read

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:

function App(){
  const [data, setData] = useState(null);

  // useEffect for fetching data on mount
  useEffect(() => {
    fetch('//...')
    .then(response => response.json())
    .then(data => setData(data));
  }, []);

  return <div>Data: { data }</div>
}
here and further I'll drop checking response.ok for brevity sake

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 divs 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!

Posted on by:

RxJS

This is where we write about RxJS. It's meant to be a place for everyone who is interested in RxJS.

Discussion

markdown guide