React: I like RxJS

dewaldels

Dewald Els

Posted on November 22, 2020

React: I like RxJS

⚠️ 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!

💖 💪 🙅 🚩
dewaldels
Dewald Els

Posted on November 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

React: I like RxJS
react React: I like RxJS

November 22, 2020