paint-brush
React is Not Really Reactive. Let's Fix That With RxJS!by@romanismagilov301290
119 reads

React is Not Really Reactive. Let's Fix That With RxJS!

by Roman IsmagilovOctober 4th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

React is great for UI, but RxJS makes managing complex asynchronous data, API calls, and user events easier. It adds powerful reactive programming capabilities to React, helping you handle advanced logic and build more efficient, scalable apps.
featured image - React is Not Really Reactive. Let's Fix That With RxJS!
Roman Ismagilov HackerNoon profile picture

React is a technology (library) that allows you to create interfaces both in the browser and in mobile applications. The component approach and one-way data flow, as a basic concept, have been appreciated in the world of web development and are now used as the main tool for building interfaces in many projects.


As applications become more complex, it can be difficult to handle state and asynchronous data correctly. That's where RxJS (reactive extensions for JavaScript) can help us. RxJS provides powerful tools for working with asynchronous data streams. In this article, we'll look at how to make React more reactive by mixing it with RxJS and share some techniques to get the most out of this combination.

Understanding the “reactivity“ of React

React’s “reactivity” revolves around changes in a component's state. When a component's state updates, React re-renders that component along with its child elements. While this model works well for most scenarios, it can get tricky when you’re dealing with more complex asynchronous tasks, such as:


  • Handling multiple API calls
  • Managing real-time data streams
  • Coordinating events from various user interactions


Sure, React’s hooks like useState and useEffect can help with these situations. But I’d like to show you another way to tackle these challenges using RxJS. Who knows, maybe you’ll find it even better).

Introduction to RxJS: Observables, Subjects and more

RxJS (Reactive Extensions for JavaScript) is a library for reactive programming that uses so-called Observables. They allow you to work with asynchronous code in a simpler way.

Observables

Thus, observable objects are essentially data streams that you can “observe" over time. You can create them from different data sources and then use different operators to combine or modify them as needed.


import { Observable } from 'rxjs';

const observable$ = new Observable(subscriber => {
  subscriber.next('Hello');
  subscriber.next('World');
  subscriber.complete();
});

observable$.subscribe({
  next: value => console.log(value), // 'Hello', then 'World'
  complete: () => console.log('Completed') // 'Completed'
}); 

Subjects

Subjects in RxJS are kind of special because they work as both an Observable and an Observer. This means they can broadcast values to multiple Observers at once. They're often used when you want to represent some shared resource, like a stream of events or data that many parts of your app need to listen to.

import { Subject } from 'rxjs';

const subject$ = new Subject();

subject$.subscribe(value => console.log('Observer 1:', value)); // 'Observer 1: Hello', then 'Observer 1: World'
subject$.subscribe(value => console.log('Observer 2:', value)); // 'Observer 2: Hello', then 'Observer 2: World'

subject$.next('Hello');
subject$.next('World');

BehaviorSubject

A BehaviorSubject is a type of Subject that always holds the current value. It needs an initial value when you create it, and whenever someone subscribes to it, they immediately get the latest value.

import { BehaviorSubject } from 'rxjs';

const behaviorSubject$ = new BehaviorSubject('Initial Value');

behaviorSubject$.subscribe(value => console.log('Observer 1:', value)); //  'Observer 1: Initial Value'

behaviorSubject$.next('Hello'); //  'Observer 1: Hello'

behaviorSubject$.subscribe(value => console.log('Observer 2:', value)); //  'Observer 2: Hello'

behaviorSubject$.next('World'); //  'Observer 1: World', then 'Observer 2: World'


Other Subjects

  • ReplaySubject: This one sends all past values to any new subscribers so they get the full history.
  • AsyncSubject: Only sends the last - value, but only after the observable has been completed.

Why the Angular Team Uses RxJS

The Angular team chose RxJS because it helps them handle reactive programming for things like HTTP requests, forms, and routing. As applications get bigger and more complex, RxJS makes it easier to work with asynchronous processes. It is a great tool for managing the state and side effects of web applications, which is why it is popular in large projects.

Practical Examples

A useObservable hook:

Before moving on to the examples, let's create a useObservable hook. In React, it is recommended to separate some types of behavior into custom hooks, and we will need this hook later for examples.

const useObservable = (observable$, initialValue) => {
  const [value, setValue] = useState(initialValue);

  useEffect(() => {
    const subscription = observable$.subscribe((newValue) => {
      setValue(newValue);
    });
    return () => subscription.unsubscribe();
  }, [observable$]);

  return value;
};

Hook’s explanation:

The useObservable function connects to the RxJS observable inside the React component. It allows you to use real-time data from observable in the component.


Parameters:

  • observable$: This is an observable object that outputs values over time. We use $ at the end, just to show that this can be observed.
  • initialValue: This is the initial value before the observed object starts sending new data.

Initializing the state:

  • value: The current value obtained from the observed object.
  • setValue: A function to update the state with a new value from the observed object.


The useEffect function ensures that we subscribe to the observable when the component is connected, and deletes the subscription when disconnected. This avoids memory leaks. Each time the observed object sends a new value, it updates the state, which causes re-render, so the component always displays the latest data.

Examples

Now let's look at three practical examples in which RxJS can improve your React applications:

1. Debounced search input

We will create a SearchComponent that will extract data from the API while the user enters text. But instead of calling the API for each keystroke, we'll cancel the input to wait a bit before making the call.


import React from 'react';
import { Subject } from 'rxjs';
import { debounceTime, switchMap } from 'rxjs/operators';
import useObservable from './useObservable';

const searchSubject$ = new Subject();

const fetchResults = (query) => {
  return fetch(`https://api.example.com/search?q=${query}`).then((res) =>
    res.json()
  );
};

const results$ = searchSubject$.pipe(
  debounceTime(500),
  switchMap((query) => fetchResults(query))
);

const SearchComponent = () => {
  const results = useObservable(results$, []);

  const onChange = (e) => {
    searchSubject$.next(e.target.value);
  };

  return (
    <div>
      <input type="text" onChange={onChange} />
      <ul>
        {results.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default SearchComponent;


Explanation:

  • We use searchSubject$ the instance of Subject to send values from the input field.
  • debounceTime waits 500 ms without typing before sending the last value.
  • switchMap cancels any previous API calls if a new value is emitted.
  • Subscribes to results$ and updates the component when new data arrives.

2. Form handling with RxJS

When you have complex forms, especially with multiple inputs and checks, RxJS can simplify the job by treating the form fields as data streams.


import React from 'react';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import useObservable from './useObservable';

const firstName$ = new BehaviorSubject('');
const lastName$ = new BehaviorSubject('');
const email$ = new BehaviorSubject('');

const isValidEmail = (email) => /\S+@\S+\.\S+/.test(email);

const formValid$ = combineLatest([firstName$, lastName$, email$]).pipe(
  map(([firstName, lastName, email]) => {
    return (
      firstName.trim() !== '' &&
      lastName.trim() !== '' &&
      isValidEmail(email)
    );
  })
);

const FormComponent = () => {
  const isFormValid = useObservable(formValid$, false);

  const handleFirstNameChange = (e) => firstName$.next(e.target.value);
  const handleLastNameChange = (e) => lastName$.next(e.target.value);
  const handleEmailChange = (e) => email$.next(e.target.value);

  const handleSubmit = () => {
    console.log('Form Submitted');
  };

  return (
    <div>
      <input type="text" placeholder="First Name" onChange={handleFirstNameChange} />
      <input type="text" placeholder="Last Name" onChange={handleLastNameChange} />
      <input type="email" placeholder="Email" onChange={handleEmailChange} />
      <button disabled={!isFormValid} onClick={handleSubmit}>
        Submit
      </button>
    </div>
  );
};

export default FormComponent;


Explanation:

  • We use BehaviorSubject for each form field to save its current value.
  • combineLatest: combines the latest values of the form fields for validation.
  • The map operator contains the validation logic that updates the isFormValid observable.
  • Subscribes to formValid$ to enable or disable the submit button.

3. Real-time data updates with WebSockets

For applications such as chat systems or interactive dashboards that require real-time data, managing website connections and data flows can be difficult. RxJS simplifies this.


import React from 'react';
import { webSocket } from 'rxjs/webSocket';
import { scan } from 'rxjs/operators';
import useObservable from './useObservable';

const socket$ = webSocket('wss://example.com/socket');

const messages$ = socket$.pipe(
  scan((acc, message) => [...acc, message], [])
);

const ChatComponent = () => {
  const messages = useObservable(messages$, []);

  const sendMessage = (msg) => {
    socket$.next({ type: 'message', data: msg });
  };

  return (
    <div>
      <ul>
        {messages.map((msg, idx) => (
          <li key={idx}>{msg.data}</li>
        ))}
      </ul>
      <button onClick={() => sendMessage('Hello World!')}>Send</button>
    </div>
  );
};

export default ChatComponent;


Explanation:

  • The webSocket function creates a WebSocket subject for sending and receiving messages.
  • The scan operator collects incoming messages in an array to maintain the chat history.
  • The useObservable hook subscribes to $messages and displays new messages as they arrive.

The RxJS and React integration libraries

We’ve gone over how RxJS works with React and covered the basics, but there are also some really helpful npm packages out there that make hooking RxJS into React way easier. These libraries simplify the process and give you tools that are specifically made for React, making the whole integration smoother.


rxjs-hooks:

A set of React hooks that simplify working with RxJS observables in components.


Features:

  • Simplifies subscription management.
  • Provides hooks such as use Observable, useEventCallback and others.


react-rxjs:

A library that offers a concise way to bind RxJS observables to React components.


Features:

  • Automatic subscription and unsubscription.
  • Memorization to prevent unnecessary re-renders.
  • Simple state exchange between components.

Conclusion

RxJS is a powerful but complex technology, and you might not need it for simpler applications. However, if you’re dealing with complex logic, like managing multiple API calls, handling user events that depend on each other, or watching for changes across different parts of your app, RxJS can be a great addition. It’s worth having a brainstorming session with your team before diving into RxJS to make sure it’s the right fit for your project.


When used in the right scenarios, integrating RxJS with React can greatly improve how you manage asynchronous data flows and state handling. By following best practices—like careful subscription management, custom hooks, and using RxJS operators—you can make your React applications more efficient and scalable. RxJS gives you the power of reactive programming, making your code more flexible and easier to maintain, especially in complex, data-driven environments.