Skip to content

LeetCode-OpenSource/rxjs-hooks

Folders and files

NameName
Last commit message
Last commit date
Aug 20, 2022
Apr 29, 2021
Mar 26, 2021
Jun 19, 2022
Aug 20, 2022
Mar 26, 2021
Mar 26, 2021
Jan 31, 2019
Mar 10, 2020
Aug 20, 2022
Jun 14, 2022
Oct 7, 2022
Nov 16, 2018
Aug 20, 2022
Oct 24, 2022

Repository files navigation

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)