⚠️ 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
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
}
})
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
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
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 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
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
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
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
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
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
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
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
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!
Top comments (16)
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:
Fetching Data in React with RxJS and <$> fragment
Kostia Palchyk for RxJS ・ Aug 4 '20 ・ 5 min read
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
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
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
Keep on exploring and experimenting 👍
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:
See useObservableState
This is super interesting! This is actually what I had in mind for the next step.
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
?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
We can assume pretty standard REST JSON API endpoint (like in Rails)
Another slight variation of the same idea
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.
The article "I like RxJS" on the DEV Community celebrates this admiration for RxJS among developers. With its reactive programming paradigm, RxJS offers a wealth of possibilities for managing complex asynchronous operations in React applications Downloadhub. The author delves into their personal experiences and reasons for embracing RxJS, highlighting its ability to handle events, manage state, and simplify complex data flows
I use rxjs-hooks in React Projects. Hope it can help u!!!
Great thank you.
You can be interested by this ReactQuery
Regards
Futhermore, perhaps you don't need a big library like RxJS , flyd is perhaps a lighter alternative look at this
You can test it here FlydStream
Love React and RxJS and would definitely recommend you check out redux-observable - it's great...
redux-observable.js.org/
Absolutely love RxJS! It's a game-changer for managing asynchronous operations and makes coding so much smoother. whatsapp plus original