Skip to content

rossjackson/react-observable-rxjs-example

Repository files navigation

react-observable-rxjs-example

An approach on how to use Observable with React application

RxJS

RxJS is the Javascript implementation of ReactiveX. ReactiveX was created by Microsoft to allow reactive programming. It is based on the observer pattern. The central data source called the observable sends the items it receives one at a time. An Observable emits three events (next, error, complete).

One thing to note is that Observables are not asynchronous. It all depends on how you construct your Observable. If you have promises, then it will return it asynchronously.

Implementation

One benefit I will show you in this react-observable-rxjs-example is how it re-renders the component less than using pure useStates. Even though React has a virtual DOM to compare which has changed and update accordingly, it is still better to get lesser re-render.

I used Open-Meteo, a free weather API. In this example, I made the longitude, latitude, and temperature unit as states that users can update. I exposed a dropdown to select 3 different locations: Washington, DC, New York, NY, and San Francisco, CA. A final design looks like:

image

To run the code, install modules by running npm i and then you can start by npm start.

I have used BehaviorSubject as my Observable. It is a variant of Subject that requires a default value. Our default value is set in Washington, DC.

The dropdown locations can be seen in weatherHelper.ts. You can also see the default value.

image

I have maintain my custom hook and comment out some code to show you on how you can achieve Observables in React.

useWeather custom hook

The useWeather custom hook requires a UseWeatherProps object. These are defaultLocationKey, defaultTemperatureUnit and setResult shown below:

image

I have two states created for this custom hook which later be exposed as a return object.

image

weatherSubject is a BehaviorSubject that starts of as null.

I, then, instantiate weatherSubject inside of a useEffect so that I can unsubscribe to it when the component unmounts. It is a must to unsubscribe your Observable / Subject when you are done to avoid memory leaks. You can do this by returning a function within your useEffect. In my example, it is return () => weatherSubject.unsubsribe()

In my useEffect, I first check if weatherSubject is null. If it is, I instantiate it with BehaviorSubject with the intial values.

      if (!weatherSubject) {
         setWeatherSubject(
            new BehaviorSubject<WeatherRequestProps>({
               latitude: locations[defaultLocationKey].latitude,
               longitude: locations[defaultLocationKey].longitude,
               temperatureUnit: defaultTemperatureUnit,
            })
         )
         return
      }

The next thing React will do is fire up the useEffect again since now the weatherSubject has changed. This effect can be seen by the second argument of the useEffect which is [weatherSubject, defaultLocationKey, defaultTemperatureUnit, setResult].

The second time the useEffect gets triggered is when I subscribe to my weatherSubject.

      weatherSubject.subscribe(({ latitude, longitude, temperatureUnit }) => {
         setFetching(true)
         fetch(
            `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true&daily=temperature_2m_min,temperature_2m_max&temperature_unit=${temperatureUnit}&timezone=${
               new Intl.DateTimeFormat().resolvedOptions().timeZone
            }`
         )
            .then((response) => {
               if (!response.ok) throw 'API Error'
               return response.json()
            })
            .then((weather: Weather) => {
               setResult({
                  success: true,
                  weather
               })
            })
            .catch(() => {
               setResult({
                  success: false,
               })
            })
            .finally(() => setFetching(false))
      })

Here, you can see that the subscribe has the WeatherRequestProps as its request props. I then trigger the state to let the user of the hooks that the API is about to fetch something by calling setFetching(true). The next line of code is a common fetch call. I am handling the error on the first response and throwing it so the catch can be called by returning a success property of false. If it is a successful call, I pass the weather as a WeatherResponseProps and finally setting the fetching back to false.

I can then call it in my App.tsx as:

   const { fetching, weatherSubject } = useWeather({
      defaultLocationKey,
      defaultTemperatureUnit,
      setResult,
   })

Displaying the result like this:

            {result.weather && (
               <WeatherResultComponent
                  daily={result.weather.daily}
                  temperatureUnit={result.weather.daily_units.temperature_2m_max}
               />
            )}

Now, the select elements don't need useStates and this is where the Observable shines a lot. You can make your select methods as Observers to the weatherSubject and inform the weatherSubject when a data has changed.

So given my two lovely selects shown here:

            <select onChange={handleSelectionChange} defaultValue={defaultLocationKey}>
               {locationKeys.map((location) => (
                  <option key={location} value={location}>
                     {location}
                  </option>
               ))}
            </select>
            <select
               onChange={handleTemperatureChange}
               defaultValue={defaultTemperatureUnit}
               className="margin-bottom"
            >
               <option value="celsius">Celcius</option>
               <option value="fahrenheit">Fahrenheit</option>
            </select>

You can see that the onChange event calls these:

   const handleSelectionChange = (e: ChangeEvent<HTMLSelectElement>) => {
      if (!weatherSubject) return
      const { latitude, longitude } = locations[e.currentTarget.value]
      weatherSubject.next({
         ...weatherRequest$.value,
         latitude,
         longitude,
      })
   }

   const handleTemperatureChange = (e: ChangeEvent<HTMLSelectElement>) => {
      if (!weatherSubject) return
      weatherSubject.next({
         ...weatherSubject.value,
         temperatureUnit: e.currentTarget.value as TemperatureUnitProps,
      })
   }

What it basically does is when the locations or temperature unit changes, to call the next() event so the weatherSubject will then update its subscriber. There is only one subscriber and it is in the useEffect of the custom hook. This will then update the result by calling the setResult.

I have added a console.log() before rendering component to observe how many times it gets called. You can uncomment the old way of updating states and you will see that it is less render than using useState.

weatherObservable.ts

Another approach to this is creating the weatherRequest$ as an exported BehaviorSubject. The $ at the end symbolizes that it is an Observable. A pattern used in Angular.

You can create another custom hook but for this example, I just added the useEffect in App.tsx.

   useEffect(() => {
      const sub = weatherRequest$
         .pipe(
            tap(() => setLoading(true)),
            switchMap(({ latitude, longitude, temperatureUnit }) =>
               fromFetch(
                  `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true&daily=temperature_2m_min,temperature_2m_max&temperature_unit=${temperatureUnit}&timezone=${
                     new Intl.DateTimeFormat().resolvedOptions().timeZone
                  }`
               ).pipe(
                  tap((response) => {
                     if (!response.ok) {
                        throw new Error('API error')
                     }
                  }),
                  concatMap<Response, Promise<Weather>>((response) => response.json()),
                  map(
                     (response) =>
                        ({
                           success: true,
                           weather: response,
                        } as WeatherResponseProps)
                  ),
                  catchError(() => {
                     return of<WeatherResponseProps>({
                        success: false,
                     })
                  }),
                  tap(() => setLoading(false))
               )
            )
         )
         .subscribe((res) => setResult(res))
      return () => sub.unsubscribe()
   }, [])

This useEffect will only fire once [] on render. You can see that I subscribe and unsubscribe at the end when the component unmounts to avoid memory leak. The first thing I did was to use .pipe(). The pipe() function takes one argument and uses it to return a value.

Within the .pipe(), I used tap() so I can perform side-effect and inform the component that I am about to call the API to let the app know to show a fetching state. SwitchMap is then used to pass the request argument to the fromFetch(). The cool thing about SwitchMap() is that it discards the latest network call when the new event arrives. Another pipe() is called within a pipe() and this is completely fine since Javascript's functions are first class functions. Which mean you can treat functions as values, pass them as arguments, or even return a function from another function. The preceding functions within the second pipe is basically handling the same then() function on our custom hook. The first is tap() to check if the response.ok is set to false, if yes, throw an error so catchError can act on it and return another Observable with a success: false. If everything is good, then map() returns the Weather object and formats it to WeartherResponseProps.

Please let me know if you have any questions or you see other ways on how to do so!

About

An approach on how to use Observable with React application

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published