Skip to content

Latest commit

 

History

History
237 lines (179 loc) · 6.37 KB

README.md

File metadata and controls

237 lines (179 loc) · 6.37 KB

rxooks

GADZOOKS! RXOOKS!

(Basically every other iteration of "rxjs", "rx", "react", and "hooks" was taken)

This is a library of hooks that are useful with async code. Generally, these small, lightweight hooks are all you will need in most case.

What is this good for?

  1. async effects that are cancellable and give you values using:
  • promises (and async/await)
  • async iterators (async function*)
  • Observables! (obviously)
  1. creating updatable state that will give you something reactive, rather than firing a new render (ala useState)

  2. Dealing with any type that returns an "unsubscribable" { unsubscribe: () => void }, like an RxJS Subscription

Installation

npm i -S rxooks

Hooks

useAsyncValues

This is a hook that allows you to start an async effect that will self-cancel on dependency changes, pretty much like useEffect only it outputs a value, and you can use async function, async function* or any function that returns Promise, AsyncIterable, or Observable, (including things that implement Symbol.observable).

async functions

The code below will display "Loading..", and then "Hi!" after 2 seconds.

import { useAsyncValues } from 'rxooks';

function sleep(ms: number): Promise<void> {
	return new Promise((resolve) => setTimeout(resolve, ms));
}

export function MyComp() {
	const value = useAsyncValues(
		async function () {
			await sleep(2000);

			return 'Hi!';
		},
		[], // deps
		{ initialValue: 'Loading...' }
	);

	return <div>{value}</div>;
}

async iterables

The following creates a component that increments a counter once per second using an async generator.

import { useAsyncValues } from 'rxooks';

function sleep(ms: number): Promise<void> {
	return new Promise((resolve) => setTimeout(resolve, ms));
}

export function MyComp() {
	const value = useAsyncValues(
		async function* () {
			let n = 0;
			while (true) {
				yield n;
				await sleep(1000);
			}
		},
		[] // deps
	);

	return <div>Count: {value}</div>;
}

observables

The following creates a component that increments a counter once per second using an RxJS observable.

import { useAsyncValues } from 'rxooks';
import { interval } from 'rxjs';

export function MyComp() {
	const value = useAsyncValues(
		() => interval(1000),
		[] // deps
	);

	return <div>Count: {value}</div>;
}

useObservableState

This can be used to create a tuple of an observable of state changes, a setter to update the state, and a getter to get the state ad-hoc. Setting state with this hook will not cause a re-render. This hook is useful for when you want to wire a react event handler to an observable to build reactive flows.

Note that the returned observable will synchronously emit the current value on subscription. (Similar to a "BehaviorSubject" in RxJS, an "atom" in Recoil, or several other observable-like UI implementations folks use)

The setter allows the user to pass simple state updates, or they can pass a callback that gives them the previous state and returns the new state.

Using this is something that requires some knowledge of RxJS operators, generally. However it could be used without operators if you so choose.

Basic use

You might use useObservableState to do something like fetch data on a debounce.

import { useObservableState, useAsyncValues } from 'rxooks';
import { debounceTime, swtichMap } from 'rxjs';

function MyComp() {
	// an observable of searches, and a setter to set the current search
	const [searches, setSearch, getSearch] = useObservableState('');

	// Here we're going to compose some reactivity using RxJS,
	// and subscribe to the observable to get the search results out.
	const searchResults = useAsyncValues(
		() =>
			searches.pipe(
				debounceTime(500),
				switchMap((search) => getSearchResults(search))
			),
		[searches] // deps
	);

	const searchResultCount = searchResults?.length ?? 0;
	const hasSearchResults = searchResultCount > 0;

	const searchChange = (e) => setSearch(e.target.value);

	// Maybe there's some other side effect you'd like to do with the
	// current value of the observable state. You can use the getter
	// for that.
	const submitForm = () => {
		// Use the getter to get the most recent value
		const lastSearch = getSearch();
		doSomethingOnFormSubmit(lastSearch);
	};

	return (
		<form onSubmit={submitForm}>
			<div>
				<label htmlFor="search-input">Search</label>
				<input id="search-input" type="text" onChange={searchChange} />
			</div>
			{hasSearchResults && (
				<ul>
					{searchResults.map((result) => (
						<li key={result.id}>{result.text}</li>
					))}
				</ul>
			)}
		</form>
	);
}

useSubscription

Simply put, this just allows you to create a side effect, like useEffect, only you return an Unsubscribable instead. That is, you return something of this shape: { unsubscribe: () => void } and useSubscriptoin will handle the setup and teardown based off of dependency changes, just like useEffect does.

This hook mostly exists to support useAsyncValues, however it's generally useful enough we're exporting it.

Primitive "subscription" example

import { useState } from 'react';
import { useSubscription } from 'rxooks';

export function MyComp() {
	const [prefix, setPrefix] = useState('Hello!');
	const [message, setMessage] = useState('');

	useSubscription(() => {
		let n = 0;
		const id = setInterval(() => setMessage(`${prefix} ${n++}`), 1000);
		return {
			unsubscribe: () => clearInterval(id),
		};
	}, [prefix]);

	return (
		<div>
			<div>
				<label htmlFor="message-display">Message:</label>
				<output id="message-display">{message}</output>
			</div>
			<div>
				<label htmlFor="prefix-input">Message Prefix:</label>
				<input
					id="prefix-input"
					type="text"
					value={prefix}
					onChange={(e) => setPrefix(e.target.value)}
				/>
			</div>
		</div>
	);
}

RxJS Subscription example

Maybe you want to set up some effect that doesn't update your view, but uses a subscription. You can use this hook to set up and RxJS subscription that runs and does just that (unlike useAsyncValues, which will emit a value that triggers a render).

import { useSubscription } from 'rxooks';
import { interval } from 'rxjs';

export function MyComp() {
	useSubscription(
		() =>
			interval(1000).subscribe((n) => {
				console.log(`Tick ${n}`);
			}),
		[]
	);

	return <div>I'm logging in the background</div>;
}