DEV Community

loading...
Cover image for Reactive apps with React + RxJs + TypeScript

Reactive apps with React + RxJs + TypeScript

Carlos Gómez Suárez
Computer Science Engineer. Intermediate Software Developer. High level nerd.
Updated on ・8 min read

A modern way to develop web apps.

Reducing complexity using Redux... or not

When I started to learn React I often heard a lot of Redux, the storage concepts, reducers and the stuff related with Redux pattern. I'm such fan and advocate of the design patterns, I was so excited to setup a React app with Redux.

Finally, the day arrived and I needed to setup Redux on my first React app and... I was no such fan of it.

Yes, it's true, Redux makes our React application follow a scalable and maintanable way for our app state management, but at the same time, it’s a tedious way to add new features because you need to add a lot of code: define an action, create a dispatcher, create a reducer, etc.

I used Redux a few times. Obviously, I prefer to use Redux instead to use nothing in our state management application. I know that there's a lot of Redux alternatives, like MobX (I'll talk more about it in a moment) or GraphQL (which I like a lot, but maybe you need to plan a very complex architecture, schemas and queries).

I was looking for something simple but powerful. Interestingly, what I was looking for was in Angular. I mean, not the Angular framework itself but a library that is commonly used in Angular applications: RxJS.

Stop thinking in a static data store, start thinking in data streams instead

In 2017, ThoughtWorks recommended to adopt ReactiveX on its technology radar.

“Distributed systems often utilize multithreading, event-based communication and nonblocking I/O to improve the overall system efficiency. These programming techniques impose challenges such as low-level threading, synchronization, thread safety, concurrent data structures, and non-blocking I/O. The open source ReactiveX library beautifully abstracts away these concerns, provides the required application plumbing, and extends the observable pattern on streams of asynchronous events. ReactiveX also has an active developer community and supports a growing list of languages, the most recent addition being RxSwift. It also implements binding to mobile and desktop platforms.”

I repeat, I’m a big fan of design patterns and ReactiveX implements a pattern very well: Observables. Observable pattern is a very interesting pattern and is very useful on most of scenarios. As you could read, iOS Applications uses ReactiveX too. Keep in mind that ReactiveX has a lot of adaptations for the different programming languages. It can be used both in back-end and front-end.

In a nutshell, there’s a data source that will emit values through time, it will be observed by at least one subscription. The magic comes when the data source manipulates the received values asynchronous to obtain more sophisticated or more simple data entities. This is possible due pipe operators.

MobX has implemented the Observable pattern too, but in my opinion, the main disavantage of this library is that uses Object Oriented Programming. This limiting us a little bit to use Reactive Programming properly.

React + ReactiveX = A modern and minimalist way to develop apps

Since I learned ReactiveX, I changed the way I develop my applications. I really enjoy to use it. To start with the code stuff, here's an example that I've implemented on my website.

Quick view

This is a little bit advanced, so if you don't know much about ReactiveX don't worry, I'll talk in depth later.

import { Observable, animationFrameScheduler, fromEvent, of } from "rxjs";
import { distinctUntilChanged, filter, map, pairwise, switchMap, throttleTime } from "rxjs/operators";
import { ScrollType, ScrollMovement } from "./types";

export const watchScroll$ = (): Observable<ScrollType> =>
    of(typeof window === "undefined")
        .pipe(
            filter((undefinedWindow) => (!undefinedWindow)),
            switchMap(() => fromEvent(window, "scroll", {passive: true})),
            throttleTime(0, animationFrameScheduler),
            map(() => (window.pageYOffset)),
            pairwise(),
            map(([previous, current]) =>
                (
                    current < previous || current === 0
                        ? ScrollMovement.UP
                        : ScrollMovement.DOWN
                )
            ),
            distinctUntilChanged()
        );
Enter fullscreen mode Exit fullscreen mode

What you was just read is a service that provides to me information about the scroll movements of the window. Window object has a lot of information but what I wanted to get was if the user was scrolling up or scrolling down, the purpose of this was enable to shown or hide a nvigation bar on my site. Look the navigation component.

import * as React from "react";
import "./Nav.component.scss";
import {useObservable} from "rxjs-hooks";
import {watchScroll$} from "./nav.service";
import {ScrollMovement, ScrollType} from "./types";
// other imports

function Nav(): JSX.Element {
    const scrollDirection: ScrollType = useObservable(watchScroll$, ScrollMovement.UP);

    return (
        <div className={cn("Nav", {"hidden": scrollDirection === ScrollMovement.DOWN})}>
            <div className="Nav__Item">
                <a className="Logo" href="/">
                    <img src={Logo} alt="Carlos Gómez"/>
                </a>
            </div>
            <div className="Nav__Item">
                <Menu/>
            </div>
        </div>
    );
}

export default Nav;
Enter fullscreen mode Exit fullscreen mode

Notice that I'm using a custom hook called useObservable, you could create your own custom hook but I decided to use this due is factory-based. This hook returns me the last emmited value by watchScroll$, then it closes the suscription. This get triggered every render. I just adding the hidden class to my div if the user is scrolling up, if not, that class will be removed.

But, how does ReactiveX work?

Let's get started with the basics.

  1. Create a data source. By convention, an observable variable uses $ at it's name.
import { from } for "rxjs";

const mySource$: Observable<number> = from([1, 2, 3, 4, 5]);
Enter fullscreen mode Exit fullscreen mode

In this case I'm using from function. This creates an observable with the given params, there's a few methods that RxJS provides to create Observables like of or fromEvent. At this point this observable do nothing because I don't have suscriptors associate it. Remember that an observable needs at least one suscriptor to start to work.

  1. Create a suscription. Suscription method will listed every value that the source emit.
import { from } for "rxjs";

const mySource$: Observable<number> = from([1, 2, 3, 4, 5]);

mySource$.subscribe();
Enter fullscreen mode Exit fullscreen mode

At this moment the Observable already started to emit the values but we can't see it or manipulate them because I don't passing any observer at the suscription. The RxJS Observer is an object like:

interface Observer<T> {
  closed?: boolean;
  next: (value: T) => void;
  error: (err: any) => void;
  complete: () => void;
}
Enter fullscreen mode Exit fullscreen mode

So we can pass something like this at the suscribe() method.

import { from } for "rxjs";

const mySource$: Observable<number> = from([1, 2, 3, 4, 5]);

mySource$.subscribe({
  next: (value) => console.log("Next: " ,value),
  error: (error) => console.error(error),
  complete: () => console.log("Observable is completed")
});
Enter fullscreen mode Exit fullscreen mode

This finally will show us at the console the values that the observable emitted:

Next: 1
Next: 2
Next: 3
Next: 4
Next: 5
Enter fullscreen mode Exit fullscreen mode

But, Observer has a few things that I don't want to touch in this article like error and complete. So, with practical purposes I will simplify it a little bit:

import { from } for "rxjs";

const mySource$: Observable<number> = from([1, 2, 3, 4, 5]);

mySource$.subscribe((next) => console.log(next));
Enter fullscreen mode Exit fullscreen mode

This works same as before.

  1. Add pipe operators. So, at this moment we have a very basic observable that isn't useful. How about if we add a little bit of complexity to this excercise. An Observable could be emit values of any types, can emit Objects, numbers, strings, JSX.Elements, more Observables, etc. So imagine that the back-end developer sent me a list of tasks.
import { Observable, of } from "rxjs";

interface Task {
    name: string;
    id: number;
    completed: boolean;
}

// Imagine this comes from the back-end
const tasks: Task[] = [
    {
        name: "Cleanup and prune system",
        id: 1,
        completed: false
    },
    {
        name: "Daily backup",
        id: 2,
        completed: true
    },
    {
        name: "Execute security scripts",
        id: 3,
        completed: false
    }
];

const tasks$: Observable<Task[]> = of(...tasks);

tasks$.subscribe((nextTasks) => console.log(nextTasks));
Enter fullscreen mode Exit fullscreen mode

Output

  {
    name: "Cleanup and prune system"
    id: 1,
    completed: false
  }
  {
    name: "Daily backup",
    id: 2,
    completed: true
  }
  {
    name: "Execute security scripts",
    id: 3,
    completed: false
  }
Enter fullscreen mode Exit fullscreen mode

What is a pipe operator?

Pipe operator is like a tweak that manipulates the observable input and returns another.

This time I want to recover only the pending tasks. For that I will use the pipe operator reduce that works same as Array.reduce().

import { Observable, of } from "rxjs";
import { reduce } from "rxjs/operators";

interface Task {
    name: string;
    id: number;
    completed: boolean;
}

// Imagine this comes from the back-end
const tasks: Task[] = [
    {
        name: "Cleanup and prune system",
        id: 1,
        completed: false
    },
    {
        name: "Daily backup",
        id: 2,
        completed: true
    },
    {
        name: "Execute security scripts",
        id: 3,
        completed: false
    }
];

const tasks$: Observable<Task[]> =
    of(...tasks)
        .pipe(
            reduce<Task, Task[]>((pendingTasks, nextTask) => (
                !nextTask.completed
                    ? [...pendingTasks, nextTask]
                    : pendingTasks
            ), [])
        );


tasks$.subscribe((nextTasks) => console.log(nextTasks));
Enter fullscreen mode Exit fullscreen mode

Output

[
  {
    name: "Cleanup and prune system"
    id: 1,
    completed: false
  },
  {
    name: "Execute security scripts",
    id: 3,
    completed: false
  }
]
Enter fullscreen mode Exit fullscreen mode

Great! That's a better approach. Now, imagine that the back-end developer adds a new feature to the tasks: priority, then we need to filter the pending tasks by high priority only, so I added a functionality using filter operator.

import { Observable, of } from "rxjs";
import { filter, reduce } from "rxjs/operators";

interface Task {
    name: string;
    id: number;
    completed: boolean;
    priority: Priority;
}

enum Priority {
    HIGH,
    MEDIUM,
    LOW
}

// Imagine this comes from the back-end
const tasks: Task[] = [
    {
        name: "Cleanup and prune system",
        id: 1,
        completed: false,
        priority: Priority.LOW
    },
    {
        name: "Daily backup",
        id: 2,
        completed: true,
        priority: Priority.HIGH
    },
    {
        name: "Execute security scripts",
        id: 3,
        completed: false,
        priority: Priority.MEDIUM
    },
    {
        name: "Verify tests",
        id: 4,
        completed: false,
        priority: Priority.HIGH
    }
];

const tasks$: Observable<Task[]> =
    of(...tasks)
        .pipe(
            filter((task) => (task.priority === Priority.HIGH)),
            reduce<Task, Task[]>((pendingTasks, nextTask) => (
                !nextTask.completed
                    ? [...pendingTasks, nextTask]
                    : pendingTasks
            ), [])
        );


tasks$.subscribe((nextTasks) => console.log(nextTasks));
Enter fullscreen mode Exit fullscreen mode

The pipe method could chain mutiple operators, it follows the order of the operators. In this case I decided to filter first, then, with the filtered values I'm creating a list of unfinished tasks. I could chain the number of operators as I need.

Output

[
  {
    name; "Verify tests",
    id: 4,
    completed: false
  }
]
Enter fullscreen mode Exit fullscreen mode

Real-world example (Progress Bar)

I hope you find useful the brief introduction of RxJS. I shown a couple of RxJS examples but I want to show the full path of React + RxJS with simple but powerful implementation. Let's create a simple Progress Bar.

For this implementarion I'll just need three things:

  1. A React component.
  2. A service which observe the state of the window
  3. A SCSS file.

First, the Observable and service. I need a data source, so, I'm using fromEvent which will observe the document's scroll in this case.

import {fromEvent} from "rxjs";

const scroll$ = fromEvent(document, "scroll");
Enter fullscreen mode Exit fullscreen mode

I need to manipulate that values to return a number, so, I created another Observable called progress$. The scroll$ observable is returning an Event, so I will analyze that Event with the map operator. Inside the map I'm just calculating the percentage of the user progress at the document.

import {fromEvent, Observable} from "rxjs";
import {map} from "rxjs/operators";

const scroll$ = fromEvent(document, "scroll");

export const progress$ = (): Observable<number> => scroll$
    .pipe(
        map(
          (event) => {
            const {scrollTop, scrollHeight, clientHeight} = (event.target as Document)?.documentElement;
            return (scrollTop / (scrollHeight - clientHeight)) * 100;
          }
        )
    );
Enter fullscreen mode Exit fullscreen mode

When I scroll, the output will be recalculated and he result is a number between 1 and 100. Great! That's what I wanted. Next step: The React component.

import * as React from "react";
import "./ProgressBar.component.scss";
import { useObservable } from "rxjs-hooks";
import { progress$ } from "./progressBar.service";

export default ProgressBar;

function ProgressBar(): JSX.Element {

    const progress = useObservable(progress$, 0);

    return (
        <div className="ProgressBar" style={{width: `${progress}%`}/>
    );
}
Enter fullscreen mode Exit fullscreen mode

Notice that I'm using again the custom useObservable hook to subscribe and get the last value of progress$. This seems good, the last step is to add the styles.

.ProgressBar {
  position: fixed;
  bottom: 0;
  left: 0;
  height: 0.3em;
  background: red;
  transition: width;
}
Enter fullscreen mode Exit fullscreen mode

That simple. No actions, no reducers, just Observables. You see it?

Last thoughts

State managment is a primordial topic on an application. Using ReactiveX on React applications changed my vision and how to develop my apps in a modern way, functional and reactive. I think that every React developer needs to give a chance to RxJS and maybe, just maybe, avoid Redux. Thanks for reading.

Discussion (3)

Collapse
christopher2k profile image
Christopher N. KATOYI

Hi ! Thanks for this great article. It's cool to see someone bringing pure Rx to a React app :)

In terms of complexity do you prefer Rx over Redux ? Rx comes with data streams and operators. Operators are enough complex to justify this website rxmarbles.com/.
I'm not a Redux fan but I've to admit that for a newcomer to web (or React) ecosystem, Redux is one of the best way to learn how to solve stores & data problems !

Collapse
charlintosh profile image
Carlos Gómez Suárez Author

Hi! Thanks for reading my article and sorry for the delay.

Yes, in my opinion I do prefer Rx over Redux, but I don't pretend to be radical. This article is just to show a different way to develop React apps. What I like from RxJS it that you could use operators as complex as you need it or as simple as you want.

Don't get me wrong, I agree that Redux is a good way to start (I was a newcomer when I used it too) and it helped me a lot to solve state problems as you said. I do prefer use Redux instead a React App without any tool to manage the app state.

Collapse
ca513zn profile image
CarlosZ92

Yeah, this is a great way to develop a React app and avoid Redux boilerplate headaches.

Forem Open with the Forem app