mirror of
https://github.com/re-rxjs/react-rxjs.git
synced 2025-12-08 18:01:51 +00:00
358 lines
12 KiB
Markdown
358 lines
12 KiB
Markdown
<div align="center">
|
|
<h1>React-RxJS</h1>
|
|
|
|
<img
|
|
height="86"
|
|
width="86"
|
|
alt="React-RxJS Logo"
|
|
src="https://raw.githubusercontent.com/re-rxjs/react-rxjs/main/assets/logo-128.png"
|
|
/>
|
|
|
|
<p>React bindings for RxJS</p>
|
|
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<!-- prettier-ignore-start -->
|
|
[](https://travis-ci.org/re-rxjs/react-rxjs)
|
|
[](https://codecov.io/gh/re-rxjs/react-rxjs)
|
|
[](https://www.npmjs.com/package/react-rxjs)
|
|
[](https://github.com/re-rxjs/react-rxjs/blob/main/LICENSE)
|
|
[](#contributors-)
|
|
[](http://makeapullrequest.com)
|
|
[](https://github.com/re-rxjs/react-rxjs/blob/main/CODE_OF_CONDUCT.md)
|
|
<!-- prettier-ignore-end -->
|
|
|
|
Main features
|
|
-------------
|
|
- :cyclone: Truly Reactive
|
|
- :zap: Highly performant and free of memory-leaks
|
|
- :twisted_rightwards_arrows: First class support for React Suspense and [ready for Concurrent Mode](https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode#results)
|
|
- :scissors: Decentralized and composable, thus enabling optimal code-splitting
|
|
- :microscope: [Tiny and tree-shakeable](https://bundlephobia.com/result?p=react-rxjs)
|
|
- :muscle: Supports TypeScript
|
|
|
|
## Table of Contents
|
|
|
|
- [Installation](#installation)
|
|
- [API](#api)
|
|
- Core
|
|
- [connectObservable](#connectObservable)
|
|
- [connectFactoryObservable](#connectFactoryObservable)
|
|
- [shareLatest](#shareLatest)
|
|
- React Suspense Support
|
|
- [SUSPENSE](#SUSPENSE)
|
|
- [suspend](#suspend)
|
|
- [suspended](#suspended)
|
|
- [switchMapSuspended](#switchMapSuspended)
|
|
- Utils
|
|
- [createInput](#createInput)
|
|
- [Examples](#examples)
|
|
|
|
|
|
## Installation
|
|
npm install react-rxjs
|
|
|
|
## API
|
|
|
|
### connectObservable
|
|
```ts
|
|
const [useCounter, sharedCounter$] = connectObservable(
|
|
clicks$.pipe(
|
|
scan(prev => prev + 1, 0),
|
|
startWith(0),
|
|
)
|
|
)
|
|
```
|
|
Accepts: An Observable.
|
|
|
|
Returns `[1, 2]`
|
|
|
|
1. A React Hook that yields the latest emitted value of the observable. If the
|
|
Observable doesn't synchronously emit a value upon the first subscription, then
|
|
the hook will leverage React Suspense while it's waiting for the first value.
|
|
|
|
2. A `sharedLatest` version of the observable that does not complete. It can be
|
|
used for composing other streams that depend on it. The shared subscription is
|
|
closed as soon as there are no subscribers to that observable.
|
|
|
|
### connectFactoryObservable
|
|
```tsx
|
|
const [useStory, getStory$] = connectFactoryObservable(
|
|
(storyId: number) => getStoryWithUpdates$(storyId)
|
|
)
|
|
|
|
const Story: React.FC<{id: number}> = ({id}) => {
|
|
const story = useStory(id);
|
|
|
|
return (
|
|
<article>
|
|
<h1>{story.title}</h1>
|
|
<p>{story.description</p>
|
|
</article>
|
|
)
|
|
}
|
|
```
|
|
Accepts: A factory function that returns an Observable.
|
|
|
|
Returns `[1, 2]`
|
|
|
|
1. A React Hook function with the same parameters as the factory function. This hook
|
|
will yield the latest update from the observable returned from the factory function.
|
|
If the Observable doesn't synchronously emit a value upon the first subscription, then
|
|
the hook will leverage React Suspense while it's waiting for the first value.
|
|
|
|
2. A `sharedLatest` version of the observable returned by the factory function that
|
|
does not complete. It can be used for composing other streams that depend on it.
|
|
The shared subscription is closed as soon as there are no subscribers to that observable.
|
|
|
|
### shareLatest
|
|
```ts
|
|
const activePlanetName$ = planet$.pipe(
|
|
filter(planet => planet.isActive),
|
|
map(planet => planet.name),
|
|
shareLatest()
|
|
)
|
|
```
|
|
|
|
A RxJS pipeable operator which shares and replays the latest emitted value. It's
|
|
the equivalent of:
|
|
|
|
```ts
|
|
const shareLatest = <T>(): Observable<T> =>
|
|
source$.pipe(
|
|
multicast(() => new ReplaySubject<T>(1)),
|
|
refCount(),
|
|
)
|
|
```
|
|
|
|
The enhanced observables returned from `connectObservable` and `connectFactoryObservable`
|
|
have been enhanced with this operator, but do not complete. Meaning that the latest
|
|
emitted value will be available until the `refCount` drops to zero.
|
|
|
|
### SUSPENSE
|
|
|
|
```ts
|
|
const story$ = selectedStoryId$.pipe(
|
|
switchMap(id => concat(
|
|
SUSPENSE,
|
|
getStory$(id)
|
|
))
|
|
)
|
|
```
|
|
|
|
This is a special symbol that can be emitted from our observables to let the react hook
|
|
know that there is a value on its way, and that we want to leverage React Suspense
|
|
while we are waiting for that value.
|
|
|
|
### suspend
|
|
|
|
```ts
|
|
const story$ = selectedStoryId$.pipe(
|
|
switchMap(id => suspend(getStory$(id))
|
|
)
|
|
```
|
|
|
|
A RxJS creation operator that prepends a `SUSPENSE` on the source observable.
|
|
|
|
### suspended
|
|
|
|
```ts
|
|
const story$ = selectedStoryId$.pipe(
|
|
switchMap(id => getStory$(id).pipe(
|
|
suspended()
|
|
))
|
|
)
|
|
```
|
|
|
|
The pipeable version of `suspend`
|
|
|
|
### switchMapSuspended
|
|
|
|
```ts
|
|
const story$ = selectedStoryId$.pipe(
|
|
switchMapSuspended(getStory$)
|
|
)
|
|
```
|
|
|
|
Like `switchMap` but applying a `startWith(SUSPENSE)` to the inner observable.
|
|
|
|
### createInput
|
|
|
|
A couple examples are worth a thousand words:
|
|
|
|
```tsx
|
|
const [getCounter$, setCounter] = createInput(0)
|
|
|
|
const useCounter = connectFactoryObservable((id: string) => getCounter$(id))
|
|
|
|
const Counter: React.FC<{id: string}> = ({id}) => {
|
|
const counter = useCounter(id);
|
|
|
|
return (
|
|
<button onClick={() => setCounter$(id, x => x - 1)} />-</button>
|
|
{counter}
|
|
<button onClick={() => setCounter$(id, x => x + 1)} />+</button
|
|
)
|
|
}
|
|
```
|
|
|
|
or:
|
|
|
|
```tsx
|
|
const [getUpClicks$, onUpClick] = createInput()
|
|
const [getDownClicks$, onDownClick] = createInput()
|
|
|
|
const useCounter = connectFactoryObservable((id: string) =>
|
|
merge(
|
|
getUpClicks$(id).pipe(mapTo(1)),
|
|
getDownClicks$(id).pipe(mapTo(-1)),
|
|
).pipe(
|
|
scan((a, b) => a + b, 0),
|
|
startWith(0)
|
|
)
|
|
)
|
|
|
|
const Counter: React.FC<{id: string}> = ({id}) => {
|
|
const counter = useCounter(id);
|
|
|
|
return (
|
|
<button onClick={onDownClick} />-</button>
|
|
{counter}
|
|
<button onClick={onUpClick} />+</button
|
|
)
|
|
}
|
|
```
|
|
|
|
## Examples
|
|
- [This is a contrived example](https://codesandbox.io/s/crazy-wood-vn7gg?file=/src/fakeApi.js) based on [this example](https://reactjs.org/docs/concurrent-mode-patterns.html#reviewing-the-changes) from the React docs.
|
|
|
|
- A search for Github repos that highlights the most recently updated one:
|
|
|
|
```tsx
|
|
import React, { Suspense } from "react"
|
|
import { Subject } from "rxjs"
|
|
import { startWith, map } from "rxjs/operators"
|
|
import { connectObservable, switchMapSuspended } from "react-rxjs"
|
|
import { Header, Search, LoadingResults, Repo } from "./components"
|
|
|
|
interface Repo {
|
|
id: number
|
|
name: string
|
|
description: string
|
|
author: string
|
|
stars: number
|
|
lastUpdate: number
|
|
}
|
|
|
|
const searchInput$ = new Subject<string>()
|
|
const onSubmit = (value: string) => searchInput$.next(value)
|
|
|
|
const findRepos = (query: string): Promise<Repo[]> =>
|
|
fetch(`https://api.github.com/search/repositories?q=${query}`)
|
|
.then(response => response.json())
|
|
.then(rawData =>
|
|
(rawData.items ?? []).map((repo: any) => ({
|
|
id: repo.id,
|
|
name: repo.name,
|
|
description: repo.description,
|
|
author: repo.owner.login,
|
|
stars: repo.stargazers_count,
|
|
lastUpdate: Date.parse(repo.update_at),
|
|
})),
|
|
)
|
|
|
|
const [useRepos, repos$] = connectObservable(
|
|
searchInput$.pipe(
|
|
switchMapSuspended(findRepos),
|
|
startWith(null),
|
|
),
|
|
)
|
|
|
|
function Repos() {
|
|
const repos = useRepos()
|
|
|
|
if (repos === null) {
|
|
return null
|
|
}
|
|
|
|
if (repos.length === 0) {
|
|
return <div>No results were found.</div>
|
|
}
|
|
|
|
return (
|
|
<ul>
|
|
{repos.map(repo => (
|
|
<li key={repo.id}>
|
|
<Repo {...repo} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
}
|
|
|
|
const [useMostRecentlyUpdatedRepo] = connectObservable(
|
|
repos$.pipe(
|
|
map(repos =>
|
|
Array.isArray(repos) && repos.length > 0
|
|
? repos.reduce((winner, current) =>
|
|
current.lastUpdate > winner.lastUpdate ? current : winner,
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
)
|
|
|
|
function MostRecentlyUpdatedRepo() {
|
|
const mostRecent = useMostRecentlyUpdatedRepo()
|
|
|
|
if (mostRecent === null) {
|
|
return null
|
|
}
|
|
|
|
const { id, name } = mostRecent
|
|
return (
|
|
<div>
|
|
The most recently updated repo is <a href={`#${id}`}>{name}</a>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<>
|
|
<Header>Search Github Repos</Header>
|
|
<Search onSubmit={onSubmit} />
|
|
<Suspense fallback={<LoadingResults />}>
|
|
<MostRecentlyUpdatedRepo />
|
|
<Repos />
|
|
</Suspense>
|
|
</>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Contributors ✨
|
|
|
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
|
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
<!-- prettier-ignore-start -->
|
|
<!-- markdownlint-disable -->
|
|
<table>
|
|
<tr>
|
|
<td align="center"><a href="https://github.com/josepot"><img src="https://avatars1.githubusercontent.com/u/8620144?v=4" width="100px;" alt=""/><br /><sub><b>Josep M Sobrepere</b></sub></a><br /><a href="https://github.com/react-rxjs/react-rxjs/commits?author=josepot" title="Code">💻</a> <a href="#ideas-josepot" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-josepot" title="Maintenance">🚧</a> <a href="https://github.com/react-rxjs/react-rxjs/commits?author=josepot" title="Tests">⚠️</a> <a href="https://github.com/react-rxjs/react-rxjs/pulls?q=is%3Apr+reviewed-by%3Ajosepot" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/react-rxjs/react-rxjs/commits?author=josepot" title="Documentation">📖</a></td>
|
|
<td align="center"><a href="https://github.com/voliva"><img src="https://avatars2.githubusercontent.com/u/5365487?v=4" width="100px;" alt=""/><br /><sub><b>Víctor Oliva</b></sub></a><br /><a href="#ideas-voliva" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/react-rxjs/react-rxjs/pulls?q=is%3Apr+reviewed-by%3Avoliva" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/react-rxjs/react-rxjs/commits?author=voliva" title="Code">💻</a> <a href="https://github.com/react-rxjs/react-rxjs/commits?author=voliva" title="Tests">⚠️</a> <a href="https://github.com/react-rxjs/react-rxjs/commits?author=voliva" title="Documentation">📖</a></td>
|
|
<td align="center"><a href="http://www.clayforthcarr.com"><img src="https://avatars3.githubusercontent.com/u/6012083?v=4" width="100px;" alt=""/><br /><sub><b>Ed</b></sub></a><br /><a href="#design-clayforthcarr" title="Design">🎨</a></td>
|
|
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4" width="100px;" alt=""/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/react-rxjs/react-rxjs/commits?author=pgrimaud" title="Documentation">📖</a></td>
|
|
<td align="center"><a href="https://github.com/bhavesh-desai-scratch"><img src="https://avatars3.githubusercontent.com/u/15194540?v=4" width="100px;" alt=""/><br /><sub><b>Bhavesh Desai</b></sub></a><br /><a href="https://github.com/react-rxjs/react-rxjs/pulls?q=is%3Apr+reviewed-by%3Abhavesh-desai-scratch" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/react-rxjs/react-rxjs/commits?author=bhavesh-desai-scratch" title="Documentation">📖</a> <a href="https://github.com/react-rxjs/react-rxjs/commits?author=bhavesh-desai-scratch" title="Tests">⚠️</a></td>
|
|
</tr>
|
|
</table>
|
|
|
|
<!-- markdownlint-enable -->
|
|
<!-- prettier-ignore-end -->
|
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
|
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|