DEV Community

Cover image for React doesn't need RxJS
Mike Pearson
Mike Pearson

Posted on • Updated on

React doesn't need RxJS

RxJS is amazing. It simplifies asynchronous logic with its 100+ operators, such as retry and debounce.

But RxJS isn't just a bag of cool async utilities. RxJS also lets you write async code declaratively:

// Without RxJS
// Logic is imperative and scattered
let results = [];

function handleSearch(search: string) {
  fetchData(search).then(data => results = data);
}

// With RxJS
// Logic is self-contained
const results$ = search$.pipe(switchMap(fetchData));
Enter fullscreen mode Exit fullscreen mode

This is an under-appreciated superpower of RxJS, because centralizing logic with state dramatically reduces bugs.

When I learned React the first thing I did was create custom hooks for RxJS. At the time I couldn't find any examples to follow. I wondered why RxJS hadn't grown as popular in React as it had in Angular. But it turns out there was a good reason.

Hooks

In 2018 I found myself at a React meetup in Lehi, Utah, looking at something really cool:
Component class vs hooks

Not exactly this, but similar. Image Source: Jonathan Wieben

This is color-coded by feature/state, so it shows how class components (left) scatter your logic everywhere, whereas hooks (right) allow you to put your logic next to the state or feature it controls.

This is the same benefit RxJS provides, as you saw above.

Both of these were in my brain for years, but I only realized last month that hooks are actually equivalent to RxJS operators! Yes, internally they are different, but they allow you to structure your code the same way, and that is what matters.

RxJS operators as hooks: typeahead

Let’s start with a simple example: Typeahead! Here is how a typeahead would look with RxJS:

const search$ = new Subject<string>();
const results$ = search$.pipe(
  debounceTime(500),
  filter(search => !!search.length),
  distinctUntilChanged(),
  switchMap(search => fetchItems(search}),
);
Enter fullscreen mode Exit fullscreen mode

How do we translate that into hooks?

debounceTime: A few people have published debounce hooks. Here's one.

filter: Hooks cannot be conditionally called, so you can put this condition inside a useEffect. Or, if using React Query, you can pass in { enabled: false } and it will not make the request.

distinctUntilChanged: useEffect will only run when search changes. And React Query’s useQuery stores queries by key, so if the same search term is passed in, it reuses the original query.

switchMap: If you want to implement cancellation, in your own custom hook you can use useEffect like this:

function useFetchItems(search: string) {
  const [result, setResult] = useState<Result>(initialResult);

  useEffect(() => {
    let req;
    if (search) req = fetchItems(search).then(setResult);
    return () => req?.cancel(); // Or whatever it is for the fetching client you’re using
  }, [search]);

  return result;
}
Enter fullscreen mode Exit fullscreen mode

When a new search term comes in, the previous request is canceled and a new one is created.

(For React Query, useQuery won’t cancel previous requests, but it will return the latest one, even if the server responds out of order, because it organizes by query key.)

Putting it all together, we get something just as reactive and declarative as RxJS:

const [search, setSearch] = useState(‘’);
const debouncedSearch = useDebounce(search, 500);
const result = useFetchItems(debouncedSearch);
Enter fullscreen mode Exit fullscreen mode

Now look at that! Just a bunch of declarative code, like RxJS! Beautiful.

Why hooks are enough

RxJS streams are not stateless, pure functions; it’s just that the state is internal. How do you think you still have access to the previous value of each input stream when using combineLatest? What do you think happens to a value while the stream is waiting for delayTime to output? RxJS just takes care of this internal state for you, so all of your code can be declarative and reactive.

React hooks also abstract away the messy, asynchronous side effects so your components can stay simple and declarative. But each step in the state 'pipeline' is not hidden from you, but out there for you to use and see. This makes you come up with stupid names like debouncedValue, but it also allows for much easier debugging than RxJS allows.

Speaking of combineLatest, what would it look like with hooks? First, here’s RxJS:

const a$ = new BehaviorSubject(1);
const b$ = new BehaviorSubject(2);
const total$ = combineLatest(a$, b$).pipe(
  map(([a, b]) => a + b),
);
Enter fullscreen mode Exit fullscreen mode

And with hooks:

const [a, setA] = useState(1);
const [b, setB] = useState(2);
const total = a + b;
Enter fullscreen mode Exit fullscreen mode

I actually prefer that!

Challenge!

Give me something in RxJS and I will rewrite it with hooks!

In the future I might create a cheatsheet for all operators.

Performance

Okay, this is an issue. RxJS is precise and efficient, while React is chaotic and overreactive. Some event sources fire extremely rapidly (like mousemove), which can make React's inefficiencies noticeable. In these situations you will want to bail out of React and directly manipulate DOM elements, using RxJS as needed:

function MouseMoveExample() {
  const divRef = useRef();
  useEffect(() => {
    // Interact with the DOM element directly
    // Use RxJS for declarative async code
  }, [divRef])
  return <div ref={divRef}>asdf</div>
}
Enter fullscreen mode Exit fullscreen mode

Why React code still sucks

So if React can handle asynchronous logic in a completely declarative way, why is the default programming style still so often imperative? For example, in this comparison between Svelte and React, Svelte looks much cleaner and contains no imperative code:

React vs Svelte

Image Source: Emil Gawkowski

How is Svelte doing this???

First, notice that Svelte has provided special syntax for input events, whereas React has left us needing to drill down to event.target.value. Could React provide us a special hook specifically for input events? What would that hook look like?

We want our code to be completely reactive, so rather than calling callback functions that imperatively call setA or setB, we want something we can use like onChange={specialHook}. Here is the hook that I propose:

function useNumberInputState(initialState: number) {
  const [state, setState] = useState(initialState);
  return [
    state,
    (event: ChangeEvent<HTMLInputElement>) => setState(+event.target.value)
    setState,
  ];
}
Enter fullscreen mode Exit fullscreen mode

It can be used like this:

function Demo() {
  const [a, changeA] = useNumberInputState(1);
  const [b, changeB] = useNumberInputState(2);

  return (
    <>
      <input type=number value={a} onChange={changeA} />
      <input type=number value={b} onChange={changeB} />

      <p>{a} + {b} = {a + b}</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here’s Svelte for comparison:

<script>
  let a = 1;
  let b = 2;
</script>

<input type=”number” value={a} bind:value={a} />
<input type=”number” value={b} bind:value={b} />

<p>{a} + {b} = {a + b}</p>
Enter fullscreen mode Exit fullscreen mode

Not bad.

Svelte is still more minimal, but there’s an issue with the way it achieves its minimalism: React has purposefully shunned two-way data binding, and they were right. In 2016 the Angular team agreed and removed it from Angular (although they later added it back with special syntax because of popular, misguided demand). What's the problem with it? It creates messy code because often multiple pieces of state need to update in response to a single event. With Svelte and React, at least you can update downstream state reactively with no issue. {a + b} in the template is a simple example of that. However, sometimes independent pieces of state need to update in response to the same event, so you either need a callback function with individual, imperative setState calls, or some a way to react to unique event objects (like Redux actions), which React and Svelte do not have quick, reactive solutions for.

More on this in a future post!

Going forward

The promise of hooks has never been fully realized, so what do we do now?

We should focus on using the power of hooks to eliminate imperative code from our apps. I might start a series of posts on how to write hooks to avoid callbacks, since callbacks are containers for imperative code.

I do not believe React is the future of web development. It still has many years ahead in the limelight, but it is too inefficient out of the box. I'm surprised at how often performance concerns muddy up component code. RxJS is just more accurate.

But React might be the best option right now. I haven't seen a completely reactive framework yet. Svelte is a great idea, but declarative async code is only possible with RxJS, which admittedly is not difficult to use in Svelte; but Svelte wasn't designed to use RxJS as a primary technology, so the integration is slightly awkward to me.

RxJS itself has some issues too. I mentioned the debuggability issue above. Another issue is extremely verbose syntax for state managed with scan. I created StateAdapt as a remedy for this.

As for Angular, the best thing it could do is make all component lifecycle methods available as observables, as well as component inputs. Without this, Angular is one of the least reactive frameworks currently popular. As a long-time Angular developer, I would love to see them fix this, as well as a few other issues.


The history of web development has trended towards more and more reactive/declarative code since the very beginning, and that will not stop.

Embrace it!

Top comments (0)