• Stars
    star
    2,181
  • Rank 21,148 (Top 0.5 %)
  • Language
    TypeScript
  • Created about 6 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

React hooks for RxJS

React hooks for RxJS

CircleCI codecov npm version

Installation

Using npm:

$ npm i --save rxjs-hooks rxjs

Or yarn:

$ yarn add rxjs-hooks rxjs

Quick look

import React from "react";
import ReactDOM from "react-dom/client";
import { useObservable } from "rxjs-hooks";
import { interval } from "rxjs";
import { map } from "rxjs/operators";

function App() {
  const value = useObservable(() => interval(500).pipe(map((val) => val * 3)));

  return (
    <div className="App">
      <h1>Incremental number: {value}</h1>
    </div>
  );
}
import React from "react";
import ReactDOM from "react-dom/client";
import { useEventCallback } from "rxjs-hooks";
import { map } from "rxjs/operators";

function App() {
  const [clickCallback, [description, x, y]] = useEventCallback((event$) =>
    event$.pipe(
      map((event) => [event.target.innerHTML, event.clientX, event.clientY]),
    ),
    ["nothing", 0, 0],
  )

  return (
    <div className="App">
      <h1>click position: {x}, {y}</h1>
      <h1>"{description}" was clicked.</h1>
      <button onClick={clickCallback}>click me</button>
      <button onClick={clickCallback}>click you</button>
      <button onClick={clickCallback}>click him</button>
    </div>
  );
}

Apis

useObservable

export type InputFactory<State> = (state$: Observable<State>) => Observable<State>
export type InputFactoryWithInputs<State, Inputs> = (
  state$: Observable<State>,
  inputs$: Observable<RestrictArray<Inputs>>,
) => Observable<State>

export function useObservable<State>(inputFactory: InputFactory<State>): State | null
export function useObservable<State>(inputFactory: InputFactory<State>, initialState: State): State
export function useObservable<State, Inputs>(
  inputFactory: InputFactoryWithInputs<State, Inputs>,
  initialState: State,
  inputs: RestrictArray<Inputs>,
): State

Examples:

import React from 'react'
import ReactDOM from 'react-dom'
import { useObservable } from 'rxjs-hooks'
import { of } from 'rxjs'

function App() {
  const value = useObservable(() => of(1000))
  return (
    // render twice
    // null and 1000
    <h1>{value}</h1>
  )
}

With default value:

import React from 'react'
import ReactDOM from 'react-dom'
import { useObservable } from 'rxjs-hooks'
import { of } from 'rxjs'

function App() {
  const value = useObservable(() => of(1000), 200)
  return (
    // render twice
    // 200 and 1000
    <h1>{value}</h1>
  )
}

Observe props change:

import React from 'react'
import ReactDOM from 'react-dom'
import { useObservable } from 'rxjs-hooks'
import { map } from 'rxjs/operators'

function App(props: { foo: number }) {
  const value = useObservable((_, inputs$) => inputs$.pipe(
    map(([val]) => val + 1),
  ), 200, [props.foo])
  return (
    // render three times
    // 200 and 1001 and 2001
    <h1>{value}</h1>
  )
}
const rootElement = document.querySelector("#app");
ReactDOM.createRoot(rootElement).render(<App foo={1000}/>);
ReactDOM.createRoot(rootElement).render(<App foo={2000}/>);

useObservable with state$

live demo

import React from 'react'
import ReactDOM from 'react-dom/client'
import { useObservable } from 'rxjs-hooks'
import { interval } from 'rxjs'
import { map, withLatestFrom } from 'rxjs/operators'

function App() {
  const value = useObservable((state$) => interval(1000).pipe(
    withLatestFrom(state$),
    map(([_num, state]) => state * state),
  ), 2)
  return (
    // 2
    // 4
    // 16
    // 256
    // ...
    <h1>{value}</h1>
  )
}

useEventCallback

Examples:

import React from 'react'
import ReactDOM from 'react-dom'
import { useEventCallback } from 'rxjs-hooks'
import { mapTo } from 'rxjs/operators'

function App() {
  const [clickCallback, value] = useEventCallback((event$: Observable<React.SyntheticEvent<HTMLButtonElement>>) =>
    event$.pipe(
      mapTo(1000)
    )
  )
  return (
    // render null
    // click button
    // render 1000
    <>
      <h1>{value}</h1>
      <button onClick={clickCallback}>click me</button>
    </>
  )
}

With initial value:

import React from 'react'
import ReactDOM from 'react-dom'
import { useEventCallback } from 'rxjs-hooks'
import { mapTo } from 'rxjs/operators'

function App() {
  const [clickCallback, value] = useEventCallback((event$: Observable<React.SyntheticEvent<HTMLButtonElement>>) =>
    event$.pipe(
      mapTo(1000)
    ),
    200,
  )
  return (
    // render 200
    // click button
    // render 1000
    <>
      <h1>{value}</h1>
      <button onClick={clickCallback}>click me</button>
    </>
  )
}

With state$:

live demo

import React from "react";
import ReactDOM from "react-dom/client";
import { useEventCallback } from "rxjs-hooks";
import { map, withLatestFrom } from "rxjs/operators";

function App() {
  const [clickCallback, [description, x, y, prevDescription]] = useEventCallback(
    (event$, state$) =>
      event$.pipe(
        withLatestFrom(state$),
        map(([event, state]) => [
           event.target.innerHTML,
           event.clientX,
           event.clientY,
          state[0],
        ])
      ),
    ["nothing", 0, 0, "nothing"]
  );

  return (
    <div className="App">
      <h1>
        click position: {x}, {y}
      </h1>
      <h1>"{description}" was clicked.</h1>
      <h1>"{prevDescription}" was clicked previously.</h1>
      <button onClick={clickCallback}>click me</button>
      <button onClick={clickCallback}>click you</button>
      <button onClick={clickCallback}>click him</button>
    </div>
  );
}

A complex example: useEventCallback with both inputs$ and state$

live demo

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { useEventCallback } from "rxjs-hooks";
import { map, withLatestFrom, combineLatest } from "rxjs/operators";

import "./styles.css";

function App() {
  const [count, setCount] = useState(0);
  const [clickCallback, [description, x, y, prevDesc]] = useEventCallback(
    (event$, state$, inputs$) =>
      event$.pipe(
        map(event => [event.target.innerHTML, event.clientX, event.clientY]),
        combineLatest(inputs$),
        withLatestFrom(state$),
        map(([eventAndInput, state]) => {
          const [[text, x, y], [count]] = eventAndInput;
          const prevDescription = state[0];
          return [text, x + count, y + count, prevDescription];
        })
      ),
    ["nothing", 0, 0, "nothing"],
    [count]
  );

  return (
    <div className="App">
      <h1>
        click position: {x}, {y}
      </h1>
      <h1>"{description}" was clicked.</h1>
      <h1>"{prevDesc}" was clicked previously.</h1>
      <button onClick={clickCallback}>click me</button>
      <button onClick={clickCallback}>click you</button>
      <button onClick={clickCallback}>click him</button>
      <div>
        <p>
          click buttons above, and then click this `+++` button, the position
          numbers will grow.
        </p>
        <button onClick={() => setCount(count + 1)}>+++</button>
      </div>
    </div>
  );
}

Example of combining callback observables coming from separate elements - animation with start/stop button and rate controllable via slider

live demo

const Animation = ({ frame }) => {
  const frames = "|/-\\|/-\\|".split("");
  return (
    <div>
      <p>{frames[frame % frames.length]}</p>
    </div>
  );
};


const App = () => {
  const defaultRate = 5;

  const [running, setRunning] = useState(false);

  const [onEvent, frame] = useEventCallback(events$ => {
    const running$ = events$.pipe(
      filter(e => e.type === "click"),
      scan(running => !running, running),
      startWith(running),
      tap(setRunning)
    );

    return events$.pipe(
      filter(e => e.type === "change"),
      map(e => parseInt(e.target.value, 10)),
      startWith(defaultRate),
      switchMap(i => timer(200, 1000 / i)),
      withLatestFrom(running$),
      filter(([_, running]) => running),
      scan(frame => frame + 1, 0)
    );
  });

  return (
    <div className="App">
      <button onClick={onEvent}>{running ? "Stop" : "Start"}</button>
      <input
        type="range"
        onChange={onEvent}
        defaultValue={defaultRate}
        min="1"
        max="10"
      ></input>
      <Animation frame={frame} />
    </div>
  );
};

Known issues

If you are using React 18 + StrictMode under NODE_ENV=development, rxjs-hooks may not work properly. facebook/react#24502 (comment)