Hitchhiker’s guide to Reactive Hooks

Dima Doronin
ITNEXT
Published in
5 min readAug 6, 2019

--

The Reactive Hooks library connects RxJS Galaxy with React Universe.

Web Applications front-end (FE) development is all about events management and DOM manipulations. RxJS was designed to handle complex async flows with race conditions. React helps to minimize expensive DOM operations. At Reonomy we’ve developed a library called Reactive Hooks to leverage the power of both.

What is the Reactive Hooks library?

This is a library for rendering RxJS Observables using React Hooks.

How to use Reactive Hooks?

This library provides a set of useful hooks that can be imported directly into React components, e.g.:

import React from "react";
import { useRxState } from "@reonomy/reactive-hooks";
import { BehaviorSubject } from "rxjs";
const count$ = new BehaviorSubject(0);function Example() {
const [count, setCount] = useRxState(count$);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

API reference

Here is a list of all hooks, the most frequently used are marked with stars.

  • useRx (internal)
  • useRxDebug
  • useRxState ⭐⭐⭐️⭐️⭐️
  • useRxStateResult ⭐⭐️
  • useRxStateAction
  • useRxEffect ⭐️⭐️⭐️
  • useRxAjax ⭐️⭐️⭐️⭐️
  • useRxDebounce
  • useMountEffect ⭐️

useRx (internal)

useRx<T>(observable$: Observable<T>, observer: NextObserver<T>):void

Subscribes on a given observable when component is mounted, unsubscribes when it becomes unmounted and keeps track of all active subscriptions. The active subscriptions count will be printed to the web console when URL hash ends with debug (debug mode), e.g. https://foo.bar#debug. This could be useful for performance tuning.

This hook is internal, so probably you don’t wanna use it. But still it’s good to know, because useRx is a foundation for all other hooks.

useRxDebug

useRxDebug<T>(observable$: Observable<T>, tag: string): void

Subscribes on a given observable and logs all changes to the console (debug mode).

import React from "react";
import { useRxDebug } from "@reonomy/reactive-hooks";
import { interval } from "rxjs";
const interval$ = interval(1000);function Example() {
useRxDebug(interval$, "interval$");
return (
<div>
URL should end with #debug
</div>
);
}

useRxState

Let’s start with an example:

const [foo, setFoo] = useRxState(foo$);

A simplified version of this hook could be described as follows:

useRxState<S extends Observable<T>>(subject$: Observable<T>): Out<T>

Subscribes on a given observable of type S and based on the type returns a tuple of the current value of type T and a callback to set this value.

+--------------------+-------------------------------------+
| Input Type S | Output Type |
+--------------------+-------------------------------------+
| BehaviorSubject<T> | [T, (value: T) => void] |
| Subject<T> | [T | undefined, (value: T) => void] |
| Observable<T> | [T | undefined ] |
+--------------------+-------------------------------------+

It’s implemented with Typescript Conditional Types:

useRxState<
S extends Observable<any> |
Subject<any> |
BehaviorSubject<any>
>(subject$: S):
S extends BehaviorSubject<infer T> ? [T, (value: T) => void]:
S extends Subject<infer T> ? [T | undefined, (value: T) => void]:
S extends Observable<infer T> ? [T | undefined]:
never

The output type is defined dynamically. The algorithm behind it can be expressed as follows:

if (S is BehaviorSubject<T>)
return [T, (value: T) => void]
if (S is Subject<T>)
return [T | undefined, (value: T) => void]
if (S is Observable<T>) {
return [T | undefined]
}

Here is an example of two-way data-binding implemented with useRxState:

import React from "react";
import { useRxState } from "@reonomy/reactive-hooks";
import { BehaviorSubject } from "rxjs";
const fullName$ = new BehaviorSubject('');function FullName() {
const [fullName, setFullName] = useRxState(fullName$);
const onChange = e => setFullName(e.target.value);
return (
<div>
<p>Full Name: {fullName}</p>
<input
type="text"
onChange={onChange}
value={fullName} />
</div>
);
}

useRxStateResult

useRxStateResult<T>(observable$: Observable<T>): T | undefined

A readonly version of useRxState that returns a current value.

import React from "react";
import { useRxStateResult } from "@reonomy/reactive-hooks";
import { interval } from "rxjs";
const interval$ = interval(1000);function Interval() {
const interval = useRxStateResult(interval$);
return (
<div>
Interval: {interval}
</div>
);
}

useRxStateAction

useRxStateAction<T>(subject$: Subject<T>): (value: T) => void

Returns a callback that modifies a provided subject.

Here is an example that will print “foo is Foo” to the web console. When you click on a button it will be “foo is Bar”.

import React from "react";
import { useRxStateAction } from "@reonomy/reactive-hooks";
import { BehaviorSubject } from "rxjs";
const foo$ = new BehaviorSubject('Foo');
foo$.subscribe({
next: val => console.log("foo is ", val);
})
function FooBar() {
const setFoo = useRxStateAction(foo$);
return (
<div>
<p>{fullName}</p>
<button type="button" onClick={() => setFoo("Bar")}>
Make it Bar
</button>
</div>
);
}

useRxEffect


useRxEffect<T>(
observable$: Observable<T>,
next: (value: T) => void
): void

Similar to useEffect, but should be used as a reaction to observable changes. It reminds me “watch” in angularJS, so be careful!

Example:

import React from "react";
import { useRxEffect } from "@reonomy/reactive-hooks";
import { interval } from "rxjs";
const interval$ = interval(1000);function WatchInterval() {
useRxEffect(interval$, interval => {
// do something
console.log("interval is ", interval);
});
return (
<p>
Look at the console!
</p>
);
}

useRxAjax

useRxAjax<Req, Res>(
api$: (req: Req) => Observable<Res>,
next?: (response: Http<Req, Res>) => void
): [Http<Req, Res> | undefined, (req: Req) => void]interface Http<Req, Res> {
status: Status; // 'succeeded' | 'failed' | 'pending';
req: Req;
res?: Res;
error?: any;
}

Invokes data api calls (fetch requests) and returns a tuple with a response object and a request dispatcher. Also provides a callback to react on requests in status pending, succeeded or failed.

Example:

import React from 'react';
import { useRxAjax } from '@reonomy/reactive-hooks';
import { ajax } from ‘rxjs/ajax’;
function weatherStatus({ apiKey }) {
return ajax({
method: "GET",
url: `https://api.openweathermap.org/data/2.5/weather?zip=10017,us&appid=${apikey}`)
};
}
function WeatherWidget() {
const [weather, getWeather] = useRxAjax(api.weather);
return (
<article>
<h1>Weather Status</h1>
<button onClick={() => getWeather({apiKey: 'xxxxxx'})}>
Refresh
</button>
<section>
{ weather &&
weather.status === 'pending' &&
<p>Pending...</p> }
{ weather &&
weather.status === 'failed' &&
<p>Failed: {JSON.stringify(weather.error)}</p> }
{ weather &&
weather.status === 'succeeded' &&
<p>Succeeded {JSON.stringify(weather.res)}</p> }
</section>
</article>
);
}

useRxDebounce

useRxDebounce<In, Out>(     func$: (input: In) => Observable<Out>, 
next?: (val: Out) => void,
debounce: number = DEBOUNCE
): [Out | undefined, (input: In) => void]

Invokes a callback with a given debounce timeout. Useful for autosuggests.

import React, { useState } from 'react';
import { useRxDebounce } from '@reonomy/reactive-hooks';
import { ajax } from 'rxjs/ajax';
function addressLookup({ address }) {
return ajax({
method: "GET",
url: "..."
};
}
function AddressAutosuggest() {
const [address, setAddress] = useState('');
const [suggestions, updateSuggestions] =
useRxDebounce(addressLookup$);
const onChange = e => {
setAddress(e.target.value);
updateSuggestions(e.target.value);
};
return (
<article>
<h1>Search by address</h1>
<input type="text" onChange={onChange} value={address}/>
{ suggestions &&
<ul>
{ suggestions.map({id, text} =>
<li key={id}>{text}</li>
)}
</ul> }
</article>
);
}

useMountEffect

useMountEffect(onMountCallback: VoidFunction): void

Invokes a callback when component is mounted. Very useful when you need to invoke an http request on initial render.

import React from 'react';
import { useRxAjax, useMountEffect } from '@reonomy/reactive-hooks';
import { ajax } from 'rxjs/ajax';
function weatherStatus({ apiKey }) {
return ajax({
method: "GET",
url: `https://api.openweathermap.org/data/2.5/weather?zip=10017,us&appid=${apikey}`)
};
}
function WeatherWidget() {
const [weather, getWeather] = useRxAjax(api.weather);
useMountEffect(() => getWeather({apiKey: 'xxxxxx'})); return (
<article>
<h1>Weather Status</h1>
<button onClick={() => getWeather({apiKey: 'xxxxxx'})}>
Refresh
</button>
<section>
{ weather &&
weather.status === 'pending' &&
<p>Pending...</p> }
{ weather &&
weather.status === 'failed' &&
<p>Failed: {JSON.stringify(weather.error)}</p> }
{ weather &&
weather.status === 'succeeded' &&
<p>Succeeded {JSON.stringify(weather.res)}</p> }
</section>
</article>
);
}

RxJS Integrated Seamlessly

React ant RxJS are great libraries that help to deal with DOM and async events. With the Reactive Hooks library they can smoothly work together ensuring the most optimal DOM operations and deterministic behavior of the entire application.

--

--