loading...
Cover image for React: I like RxJS

React: I like RxJS

sumodevelopment profile image Dewald Els Updated on ・7 min read

⚠️ Heads up: ⚠️ This article is an opinion and experiment. I am open to comments and criticisms of this approach.

UPDATE: 23 November 2020

After the tremendous kind and helpful comments I've reworked my initial idea. It's completely changed but so far I think it's and improvement.

I've unintentionally ended up with a very Redux-esque solution. So I think I'm going to call the end of the experiment. :) I have learnt a lot about available options for React and also some new things using RxJS.

Thanks again for all the kind comments and pointers. As well as links to the awesome projects that are up and running.

useStore Custom Hook

import { store$ } from "./store";
import { useEffect, useState } from "react";

function useStore(stateToUse, defaultValue = []) {

    const [ state, setState ] = useState(defaultValue)

    useEffect(() => {
        const sub$ = store$.subscribe(data => {
            setState(data[stateToUse])
        })

        return () =>  sub$.unsubscribe()
    },[stateToUse])

    return state
}

export default useStore
Enter fullscreen mode Exit fullscreen mode

Hooks/useStore.js

Store.js - Central App state

import {Subject} from "rxjs";

let AppState = {
    movies: []
}

export const store$ = new Subject();
export const dispatcher$ = new Subject()

dispatcher$.subscribe(data => {
    switch (data.action) {
        case 'GET_MOVIES':
            fetch('http://localhost:5000/movies')
                .then(r => r.json())
                .then(movies => {
                    AppState = {
                        ...AppState,
                        movies
                    }
                    return AppState
                })
                .then(state => store$.next(state))
            break
        case 'CLEAR_MOVIES':
            AppState = {
                ...AppState,
                movies: []
            }
            store$.next( AppState )
            break
        case 'DELETE_MOVIE':
            AppState = {
                ...AppState,
                movies: AppState.movies.filter( movie => movie.id !== data.payload )
            }
            store$.next( AppState )
            break
        case 'ADD_MOVIE':
            AppState = {
                movies: [ ...AppState.movies, data.payload ]
            }
            store$.next( AppState )
            break
        default:
            store$.next( AppState )
            break
    }
})
Enter fullscreen mode Exit fullscreen mode

/store.js

Very Redux-like syntax with the added benefit of being able to do Asynchronous actions. Because the store is subscription based it will simply notify any subscriptions of the new state when it arrives.

It might be worth separating states into their own stores, this way a component does not get the entire AppState when the subscription fires .next()

Movie/MovieList.js

import React, { useEffect } from 'react'
import MovieListItem from "./MovieListItem";
import { dispatcher$ } from "../store";
import useStore from "../useStore";

const MovieList = () => {

    const movies = useStore('movies' )

    useEffect(() => {
        dispatcher$.next({ action: 'GET_MOVIES' })
    }, [])

    // unchanged JSX.
    return (
        <main>
            <ul>
                { movies.map(movie =>
                    <MovieListItem key={ movie.id } movie={movie} />
                )}
            </ul>
        </main>
    )
}

export default MovieList
Enter fullscreen mode Exit fullscreen mode

/Movie/MovieList.js

This component now no longer needs a subscription in an useEffect and simply needs to dispatch the action to get movies. (Very redux-ish).

Navbar.js

import { dispatcher$ } from "../store";
import useStore from "../useStore";

const Navbar = () => {

    const movieCount = useStore('movies').length

    const onClearMovies = () => {
        if (window.confirm('Are you sure?')) {
            dispatcher$.next({ action: 'CLEAR_MOVIES' })
        }
    }

    return (
        <nav>
            <ul>
                <li>Number of movies { movieCount }</li>
            </ul>
            <button onClick={ onClearMovies }>Clear movies</button>
        </nav>
    )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

/Navbar/Navbar.js

END OF UPDATE.


Source Code:

You can download the source code here:
React with RxJS on Gitlab

Introduction

If you are a serious React developer you have no doubt integrated React Redux into your applications.

React redux offers separation of concerns by taking the state out of the component and keeping it in a centralised place. Not only that it also offers tremendous debugging tools.

This post in no way or form suggests replacing React Redux or The ContextAPI.

👋 Hello RxJS

RxJS offers a huge API that provides any feature a developer could need to manage data in an application. I've not ever scratched the surface of all the features.

In fact, this "experiment" only uses Observables and Subscriptions.

If you're not familiar with RxJS you can find out more on their official website:

RxJS Official Documentation

RxJS in React

I'll be honest, I haven't done a Google search to see if there is already a library that uses RxJS in React to manage state...

BUT, To use RxJS in React seems pretty straight forward. I've been thinking about doing this experiment for some time and this is that "version 0.0.1" prototype I came up with.

My main goal is simplicity without disrupting the default flow of React Components.

🤷‍♀️ What's the Problem?

Simply put: Sharing state

The problem most beginners face is sharing state between unrelated components. It's fairly easy to share state between parents and child components. Props do a great job. But sharing state between siblings, are far removed components become a little more challenging even in small apps.

As an example, sharing the number of movies in your app between a MovieList component and a Navbar component.

The 3 options that I am aware of:

  • Lifting up the state: Move the component state up to it's parent, which in most cases will be an unrelated component. This parent component now contains unrelated state and must contain functions to update the state.
  • ContextAPI: Implement the ContextAPI to create a new context and Provide the state to components. To me, This would probably be the best approach for this scenario.
  • React Redux: Add React Redux to your tiny app and add layers of complexity which in a lot of cases are unnecessary.

Let's go off the board for Option number 4.

🎬 React Movies App

I know, cliche, Todo's, Movies, Notes apps. It's all so watered down, but here we are.

Setup a new Project

I started off by creating a new React project.

npx create-react-app movies
Enter fullscreen mode Exit fullscreen mode

Components

After creating the project I created 3 components. The components MovieList, MovieListItem and Navbar are simple enough. See the code below.

MovieList.js

import React, { useState } from 'react'

const MovieList = () => {
    const [ movies, setMovies ] = useState([])
    return (
        <main>
            <ul>
                { movies.map(movie =>
                    <MovieListItem key={ movie.id } movie={movie} /> 
                )}
            </ul>
        </main>
    )
}
export default MovieList
Enter fullscreen mode Exit fullscreen mode

Movie/MovieList.js

MovieListItem.js

const MovieListItem = ({ movie }) => {

    const onMovieDelete = () => {
        // Delete a movie
    }

    return (
        <li onClick={ onMovieDelete }>
            <div>
                <img src={ movie.cover } alt={ movie.title } />
            </div>
            <div >
                <h4>{ movie.title }</h4>
                <p>{ movie.description }</p>
            </div>
        </li>
    )
}

export default MovieListItem
Enter fullscreen mode Exit fullscreen mode

Movie/MovieList.js

Navbar.js

import { useState } from 'react'

const Navbar = () => {
    const [movieCount, setMovieCount] = useState(0)
    return (
        <nav>
            <ul>
                <li>Number of movies { movieCount }</li>
            </ul>
        </nav>
    )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

Navbar/Navbar.js

The first thing I wanted to do is keep the state management of React. I think it works well in a component and didn't want to disrupt this flow.

Each component can contain it's own state.

🔧 Services

I come from an Angular background so the name Services felt like a good choice.

MovieService.js

The service contains a class with static methods to make use of RxJS Observables.

import { BehaviorSubject } from 'rxjs'

class MovieService {
    static movies$ = new BehaviorSubject([])

    static getMovies() {
        fetch('http://localhost:3000/movies')
            .then(r => r.json())
            .then((movies) => this.setMovies(movies))
    }

    static setMovies(movies) {
        this.movies$.next(movies)
    }

    static deleteMovie(id) {
        this.movies$.next(this.movies$.value.filter(movie => movie.id !== id))
    }

    static clearMovies() {
        this.movies$.next([])
    }
}


export default MovieService
Enter fullscreen mode Exit fullscreen mode

Services/MovieService.js

This MovieService uses static methods to avoid me having to manage a single instance of the service. I did it this way to keep it simple for the experiment.

🛠 Integrating the Service into the MovieList component.

I did not want to alter the way React components work, specifically how they set and read state.

Here is the MovieList component using the Service to get and set the movies from a local server.

import React, { useEffect, useState } from 'react'
import MovieService from "../Services/Movies"
import MovieListItem from "./MovieListItem";

const MovieList = () => {

    // Keep the way a component uses state.
    const [ movies, setMovies ] = useState([])

    // useEffect hook to fetch the movies initially.
    useEffect(() => {
        // subscribe to the movie service's Observable.
        const movies$ = MovieService.movies$.subscribe(movies => {
            setMovies( movies )
        })

        // Trigger the fetch for movies.
        MovieService.getMovies()

        // Clean up subscription when the component is unmounted.
        return () => movies$.unsubscribe()

    }, [])

    // unchanged JSX.
    return (
        <main>
            <ul>
                { movies.map(movie => 
                    <MovieListItem key={ movie.id } movie={movie} /> 
                )}
            </ul>
        </main>
    )
}

export default MovieList
Enter fullscreen mode Exit fullscreen mode

Movie/MovieList.js - with Service

Accessing data in an unrelated component

At this point, the movie data is stored in the Observable (BehaviorSubject) of the MovieService. It is also accessible in any other component by simply subscribing to it.

Navbar - Getting the number of movies

import { useEffect, useState } from 'react'
import MovieService from "../Services/Movies"

const Navbar = () => {

    const [movieCount, setMovieCount] = useState(0)

    useEffect(() => {
        // subscribe to movies
        const movies$ = MovieService.movies$.subscribe(movies => {
            setMovieCount( movies.length )
        })
        return () => movies$.unsubscribe()
    })

    return (
        <nav>
            <ul>
                <li>Number of movies { movieCount }</li>
            </ul>
        </nav>
    )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

Navbar/Navbar.js - with Service

The default flow of the component remains unchanged. The benefit of using the subscriptions is that only components and its children subscribing to the movies will reload once the state updates.

🗑 Deleting a movie:

To take this a step further, we can test the subscriptions by create a delete feature when a movie is clicked.

Add Delete to the MovieListItem Component

import MovieService from "../Services/Movies";
import styles from './MovieListItem.module.css'

const MovieListItem = ({ movie }) => {

    // Delete a movie.
    const onMovieDelete = () => {
        if (window.confirm('Are you sure?')) {
            // Delete a movie - Subscription will trigger
            // All components subscribing will get newest Movies.
            MovieService.deleteMovie(movie.id)
        }
    }

    return (
        <li onClick={ onMovieDelete } className={ styles.MovieItem }>
            <div className={ styles.MovieItemCover }>
                <img src={ movie.cover } alt={ movie.title } />
            </div>
            <div className={ styles.MovieItemDetails }>
                <h4 className={ styles.MovieItemTitle }>{ movie.title }</h4>
                <p>{ movie.description }</p>
            </div>
        </li>
    )
}

export default MovieListItem
Enter fullscreen mode Exit fullscreen mode

Movie/MovieListItem.js - Delete a movie

This change above is very simple. None of the other components needs to change and will received the latest state because it is subscribing to the Service's BehaviorSubject.

👨🏻‍🏫 What I learnt?

Well, there are many ways to skin a cat. The main drawback of using this approach is sacrificing the React Redux DevTools. If an application grows I have a suspicion all the subscriptions could become cumbersome and hard to debug.

Tools like RxJS Spy could be a solution to keep track and debug your code.

Simplicity

I do enjoy the simplicity of this approach and it doesn't currently disrupt the default React features but tries to compliment them.

📝 I'd love to heard from you guys and get some opinions, both positive and negative.

📖 Thanks for reading!

Discussion

pic
Editor guide
Collapse
kosich profile image
Kostia Palchyk

Hey, nice article!

As a small improvement, I'd suggest using streams in MovieService. getMovies instead of promises: it'll be easier to manage requests, especially if two clients (e.g. widgets) are trying to fetch the same data.

Also, please check out this article of mine:

And yeah, there are many packages that try to integrate React and Rx, but it's still definitely worth investigating this direction, as I'm sure there are many things yet to be found!

GL

Collapse
sumodevelopment profile image
Dewald Els Author

Oh regarding the movies being accessed from multiple widgets, I agree, this is an immediate problem I thought of but figured for the experiment i would put that aside for now. It is definitely not well structured in its current state and I will definitely check out using streams

Collapse
sumodevelopment profile image
Dewald Els Author

Thanks for the comment and

Ah yes! This is exactly what is was playing with last night! Great article by the way!

I’m still fighting the idea of mixing API requests with subscriptions.

For my experiment I want to try and completely separate fetch from the subscriptions.

I’m not even quite sure what I want to do after reading all the amazing comments and links to existing libraries

Collapse
kosich profile image
Kostia Palchyk

Keep on exploring and experimenting 👍

Collapse
stereobooster profile image
stereobooster

Can you show example of code on how to remove movie on the server e.g. do a REST call DELETE http://localhost:3000/movies/id?

Collapse
sumodevelopment profile image
Dewald Els Author

Sure! I want to focus on writing a custom hook first. The code for deleting on the server would really depend on your application backend setup

Collapse
stereobooster profile image
stereobooster

The code for deleting on the server would really depend on your application backend setup

We can assume pretty standard REST JSON API endpoint (like in Rails)

GET /movies - return list
GET /movies/id - return one item
DELETE /movies/id - delete one item
POST /movies/id - update one item
Enter fullscreen mode Exit fullscreen mode

Another slight variation of the same idea

Collapse
beorn profile image
Bjørn Stabell

Nice summary - RxJS / reactive programming is really great :)

I'm wondering if instead of all the useEffect stuff, maybe you can just use an observable hook like useObservableState:

const output = useObservableState(input$, initialState)
Enter fullscreen mode Exit fullscreen mode

See useObservableState

Collapse
sumodevelopment profile image
Dewald Els Author

This is super interesting! This is actually what I had in mind for the next step.

Collapse
beorn profile image
Bjørn Stabell

Another suggestion is to look at RxDB - then you can extend the reactivity all the way to do the database. The content "state" is basically pushed to the database, only exposed as hooks in your SPA.

Collapse
artydev profile image
artydev

Great thank you.
You can be interested by this ReactQuery
Regards

Collapse
artydev profile image
artydev

Futhermore, perhaps you don't need a big library like RxJS , flyd is perhaps a lighter alternative look at this

var update = flyd.stream(222);

var App = function ({update}) {
  var [init, setInit] = React.useState(false);
  var [up, setUp] = React.useState(update())

  if (!init) {
   setInit(true);
   update.map((s) => setUp(s));
  }

  return (
    <div>
      <h1>{up}</h1>
    </div>
  );
};

ReactDOM.render(  
  <App update = {update} />,
  document.getElementById("app")
);

// Test
update(999)
Enter fullscreen mode Exit fullscreen mode

You can test it here FlydStream

Collapse
dezfowler profile image
Derek Fowler

Love React and RxJS and would definitely recommend you check out redux-observable - it's great...
redux-observable.js.org/