Safely Integrating RxJS with React

George Hulpoi3 min read · December 31, 2024

In this article, we'll explore the scenarios where using RxJS with React is advantageous. A solid understanding of React and RxJS is recommended before diving in.

Summary

React is a popular JavaScript library for building user interfaces with a component-based architecture. It efficiently updates and renders components when data changes. RxJS, on the other hand, is a library for reactive programming using Observables, which simplifies handling asynchronous or callback-based code.

While React is inherently reactive, updating components automatically when data changes, RxJS provides powerful tools to manage complex asynchronous operations.

React has its own idiomatic way of handling tasks. For updating the DOM, you use props or state, and for component communication, context is the way to go. It's crucial to familiarize yourself with React's documentation to fully embrace its paradigm.

Nonetheless, React doesn't inherently offer solutions for orchestrating complex asynchronous operations.

React offers two primary methods for event handling:

  1. Passing event handlers as props.
  2. Using hooks like useEffect or useLayoutEffect for event listening.

These methods work well for straightforward asynchronous tasks, such as handling onClick events.

Movable circle on the screen, responding to mouse events

Consider a scenario where you need to create a movable circle on the screen, responding to mouse events. Here's a React implementation without RxJS:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
export default function MoveableCircleWithNative() {
  const ref = useRef<HTMLDivElement | null>(null);
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const el = ref.current;

    if (el) {
      let startX = 0;
      let startY = 0;
      let requestId: number | undefined;

      const onMouseMove = (moveEvent: MouseEvent) => {
        const dx = moveEvent.clientX - startX;
        const dy = moveEvent.clientY - startY;

        requestId = requestAnimationFrame(() => {
          setPosition((prevPos) => ({
            x: prevPos.x + dx,
            y: prevPos.y + dy,
          }));
          requestId = undefined;
        });

        startX = moveEvent.clientX;
        startY = moveEvent.clientY;
      };

      const onMouseUp = () => {
        window.removeEventListener('mousemove', onMouseMove);
        window.removeEventListener('mouseup', onMouseUp);
      };

      const onMouseDown = (startEvent: MouseEvent) => {
        startX = startEvent.clientX;
        startY = startEvent.clientY;

        window.addEventListener('mousemove', onMouseMove);
        window.addEventListener('mouseup', onMouseUp);
      };

      el.addEventListener('mousedown', onMouseDown);

      return () => {
        el.removeEventListener('mousedown', onMouseDown);
        window.removeEventListener('mouseup', onMouseUp);
        window.removeEventListener('mousemove', onMouseMove);

        if (requestId) {
          cancelAnimationFrame(requestId);
        }
      };
    }
  }, []);

  return (
    <div
      ref={ref}
      className="moveableCircle"
      style={{ transform: `translate(${position.x}px, ${position.y}px)` }}
    ></div>
  );
}

This code is functional but, using native Browser APIs for handling complex asynchronous interactions can lead to verbose and intricate code, making it difficult to maintain and debug.

This approach is error-prone, with a higher risk of bugs like memory leaks and incorrect state updates. The resulting code can be hard to read and understand, complicating collaboration in team environments.

Additionally, native APIs often lack modularity and reusability, leading to code duplication and increased maintenance efforts.

Here's how the same functionality can be achieved using RxJS:

12345678910111213141516171819202122232425262728293031323334353637383940414243
export default function MoveableCircleWithRxJS() {
  const ref = useRef<HTMLDivElement | null>(null);
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const el = ref.current;

    if (el) {
      const sub = fromEvent<MouseEvent>(el, 'mousedown')
        .pipe(
          switchMap((event) =>
            fromEvent<MouseEvent>(window, 'mousemove').pipe(
              takeUntil(fromEvent(window, 'mouseup')),
              startWith(event),
              pairwise(),
              observeOn(animationFrameScheduler)
            )
          )
        )
        .subscribe(([prevEvent, currEvent]) => {
          const dx = currEvent.clientX - prevEvent.clientX;
          const dy = currEvent.clientY - prevEvent.clientY;

          if (dx || dy) {
            setPosition((prevPos) => ({
              x: prevPos.x + dx,
              y: prevPos.y + dy,
            }));
          }
        });

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

  return (
    <div
      ref={ref}
      className="moveableCircle"
      style={{ transform: `translate(${position.x}px, ${position.y}px)` }}
    ></div>
  );
}

By utilizing RxJS, your code becomes more concise and easier to understand, especially if you're familiar with RxJS operators. This approach not only reduces the lines of code but also enhances reusability. RxJS operators like switchMap, takeUntil, and pairwise can be reused across different parts of your application, promoting DRY (Don't Repeat Yourself) principles.

Moreover, RxJS is tree-shakeable, meaning that any unused code is automatically removed during the build process. This leads to a smaller bundle size, improving load times and performance, especially for complex applications.

In summary, the more you use RxJS, the more you benefit from reduced code complexity, increased reusability, and potentially smaller and more efficient application bundles.

While some developers use RxJS for state management, it often contradicts React's design philosophy. If a problem can be efficiently solved using React's built-in features, it's advisable to stick with them. However, for particularly complex scenarios, RxJS can be a valuable tool.

By adhering to React's principles and leveraging RxJS for handling complex asynchronous tasks, you can enhance both performance and code quality. Remember, the key is to use the right tool for the right job, based on your specific needs.