Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

Commit

Permalink
Merge rx-recompose into main project (#196)
Browse files Browse the repository at this point in the history
* Merge rx-recompose into main project

* createComponent -> componentFromStream

* Use Observable constructor instead of Observable.create

* Remove global observable and use Symbol.observable instead

* configureObservable applies a pre-transform to all observables

* Empty, don't need it

* Fix rxjs import

* Configure transforms to and from observables

* Docs for configureObservable

* Typo

* No need to check if component has mounted

* No need to check if component has unmounted

* Remove Observable as global from eslintrc

* Don't need didReceiveProps check

* Add configs for popular stream libraries

* Add config for Kefir

* Implement Symbol.observable, for correctness

* Bump rxjs to latest beta
  • Loading branch information
acdlite authored Jun 16, 2016
1 parent 0957624 commit c32e089
Show file tree
Hide file tree
Showing 27 changed files with 596 additions and 395 deletions.
187 changes: 187 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ const PureComponent = pure(BaseComponent)
+ [`componentFromProp()`](#componentfromprop)
+ [`nest()`](#nest)
+ [`hoistStatics()`](#hoiststatics)
* [Observable utilities](#observable-utilities)
+ [`componentFromStream()`](#componentfromstream)
+ [`mapPropsStream()`](#mappropsstream)
+ [`createEventHandler()`](#createEventHandler)
+ [`setObservableConfig()`](#setobservableconfig)

## Higher-order components

Expand Down Expand Up @@ -630,3 +635,185 @@ hoistStatics(hoc: HigherOrderComponent): HigherOrderComponent
```

Augments a higher-order component so that when used, it copies static properties from the base component to the new component. This is helpful when using Recompose with libraries like Relay.

## Observable utilities

It turns out that much of the React Component API can be expressed in terms of observables:

- Instead of `setState()`, combine multiple streams together.
- Instead of `getInitialState()`, use `startWith()` or `concat()`.
- Instead of `shouldComponentUpdate()`, use `distinctUntilChanged()`, `debounce()`, etc.

Other benefits include:

- No distinction between state and props – everything is an stream.
- No need to worry about unsubscribing from event listeners. Subscriptions are handled for you.
- Sideways data loading is trivial – just combine the props stream with an external stream.
- Access to an ecosystem of observable libraries, such as RxJS.


**Recompose's observable utilities can be configured to work with any observable or stream-like library. See [`setObservableConfig()`](#setobservableconfig) below for details.**

### `componentFromStream()`

```js
componentFromStream(
propsToReactNode: (props$: Observable<object>) => Observable<ReactNode>
): ReactComponent
```

Creates a React component by mapping an observable stream of props to a stream of React nodes (vdom).

You can think of `propsToReactNode` as a function `f` such that

```js
const vdom$ = f(props$)
```

where `props$` is a stream of props and `vdom$` is a stream of React nodes. This formulation similar to the popular notion of React views as a function, often communicated as

```
v = f(d)
```

Example:

```js
const Counter = componentFromStream(props$ => {
const { handler: increment, stream: increment$ } = createEventHandler()
const { handler: decrement, stream: decrement$ } = createEventHandler()
const count$ = Observable.merge(
increment$.mapTo(1),
decrement$.mapTo(-1)
)
.startWith(0)
.scan((count, n) => count + n, 0)

return props$.combineLatest(
count$,
(props, count) =>
<div {...props}>
Count: {count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
})
```

### `mapPropsStream()`

```js
mapPropsStream(
ownerPropsToChildProps: (props$: Observable<object>) => Observable<object>,
BaseComponent: ReactElementType
): ReactComponent
```

A higher-order component version of `componentFromStream()` — accepts a function that maps an observable stream of owner props to a stream of child props, rather than directly to a stream of React nodes. The child props are then passed to a base component.

You may want to use this version to interoperate with other Recompose higher-order component helpers.

```js
const enhance = mapPropsStream(props$ => {
const timeElapsed$ = Observable.interval(1000).pluck('value')
props$.combineLatest(timeElapsed$, (props, timeElapsed) => ({
...props,
timeElapsed
}))
})

const Timer = enhance(({ timeElapsed }) =>
<div>Time elapsed: {timeElapsed}</div>
)
```

### `createEventHandler()`

```js
createEventHandler<T>(): {
handler: (value: T) => void
stream: Observable<T>,
}
```

Returns an object with properties `handler` and `stream`. `stream` is an observable sequence, and `handler` is a function that pushes new values onto the sequence. Useful for creating event handlers like `onClick`.

### `setObservableConfig()`

```js
setObservableConfig<Stream>({
fromESObservable<T>: ?(observable: Observable<T>) => Stream<T>,
toESObservable<T>: ?(stream: Stream<T>) => Observable<T>
})
```
Observables in Recompose are plain objects that conform to the [ES Observable proposal](https://github.com/zenparsing/es-observable). Usually, you'll want to use them alongside an observable library like RxJS so that you have access to its suite of operators. By default, this requires you to convert the observables provided by Recompose before applying any transforms:
```js
mapPropsStream($props => {
const $rxjsProps = Rx.Observable.from(props$)
// ...now you can use map, filter, scan, etc.
return $transformedProps
})
```
This quickly becomes tedious. Rather than performing this transform for each stream individually, `setObservableConfig()` sets a global observable transform that is applied automatically.
```js
import Rx from 'rxjs'
import { setObservableConfig } from 'recompose'

setObservableConfig({
// Converts a plain ES observable to an RxJS 5 observable
fromESObservable: Rx.Observable.from
})
```
In addition to `fromESObservable`, the config object also accepts `toESObservable`, which converts a stream back into an ES observable. Because RxJS 5 observables already conform to the ES observable spec, `toESObservable` is not necessary in the above example. However, it is required for libraries like RxJS 4 or xstream, whose streams do not conform to the ES observable spec.
Fortunately, you likely don't need to worry about how to configure Recompose for your favorite stream library, because Recompose provides drop-in configuration for you.
**Note: The following configuration modules are not included in the main export. You must import them individually, as shown in the examples.**
#### RxJS
```js
import rxjsconfig from 'recompose/rxjsObservableConfig'
setObservableConfig(rxjsconfig)
```
#### RxJS 4 (legacy)
```js
import rxjs4config from 'recompose/rxjs4ObservableConfig'
setObservableConfig(rxjs4config)
```
#### most
```js
import mostConfig from 'recompose/mostObservableConfig'
setObservableConfig(mostConfig)
```
#### xstream
```js
import xstreamConfig from 'recompose/xstreamObservableConfig'
setObservableConfig(xstreamConfig)
```
#### Bacon
```js
import baconConfig from 'recompose/baconObservableConfig'
setObservableConfig(baconConfig)
```
#### Kefir
```js
import kefirConfig from 'recompose/kefirObservableConfig'
setObservableConfig(kefirConfig)
```
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"postinstall": "babel-node scripts/installNestedPackageDeps.js"
},
"devDependencies": {
"ava": "git://github.com/avajs/ava.git#54e11064841c0169a88324f3daeda396d3e8889f",
"ava": "^0.15.2",
"babel-cli": "^6.5.1",
"babel-core": "^6.5.2",
"babel-eslint": "^6.0.4",
Expand Down Expand Up @@ -46,6 +46,7 @@
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-plugin-transform-regenerator": "^6.8.0",
"babel-polyfill": "^6.5.0",
"baconjs": "^0.7.84",
"chai": "^3.3.0",
"chalk": "^1.1.1",
"change-case": "^2.3.1",
Expand All @@ -59,15 +60,20 @@
"globby": "^4.0.0",
"gzip-size-cli": "^1.0.0",
"jsdom": "^8.4.0",
"kefir": "^3.2.3",
"most": "^0.19.7",
"nyc": "^6.4.0",
"react": "^15.0.0",
"react-addons-test-utils": "^15.0.0",
"react-dom": "^15.0.0",
"readline-sync": "^1.2.21",
"rimraf": "^2.4.3",
"rx": "^4.1.0",
"rxjs": "^5.0.0-beta.9",
"shelljs": "^0.6.0",
"sinon": "^1.17.1",
"webpack": "^1.12.0"
"webpack": "^1.12.0",
"xstream": "^5.0.5"
},
"devEngines": {
"node": "5.x",
Expand Down
64 changes: 64 additions & 0 deletions src/packages/recompose/__tests__/componentFromStream-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import test from 'ava'
import React from 'react'
import { mount } from 'enzyme'
import { Observable, Subject } from 'rxjs'
import setObservableConfig from '../setObservableConfig'
import rxjsConfig from '../rxjsObservableConfig'
import componentFromStream from '../componentFromStream'

setObservableConfig(rxjsConfig)

test('componentFromStream creates a component from a prop stream transformation', t => {
const Double = componentFromStream(props$ =>
props$.map(({ n }) => <div>{n * 2}</div>)
)
const wrapper = mount(<Double n={112} />)
const div = wrapper.find('div')
t.is(div.text(), '224')
wrapper.setProps({ n: 358 })
t.is(div.text(), '716')
})

test('componentFromStream unsubscribes from stream before unmounting', t => {
let subscriptions = 0
const vdom$ = new Observable(observer => {
subscriptions += 1
observer.next(<div />)
return {
unsubscribe() {
subscriptions -= 1
}
}
})
const Div = componentFromStream(() => vdom$)
const wrapper = mount(<Div />)
t.is(subscriptions, 1)
wrapper.unmount()
t.is(subscriptions, 0)
})

test('componentFromStream renders nothing until the stream emits a value', t => {
const vdom$ = new Subject()
const Div = componentFromStream(() => vdom$.mapTo(<div />))
const wrapper = mount(<Div />)
t.is(wrapper.find('div').length, 0)
vdom$.next()
t.is(wrapper.find('div').length, 1)
})

test('handler multiple observers of props stream', t => {
const Div = componentFromStream(props$ =>
// Adds three observers to props stream
props$.combineLatest(
props$, props$,
props1 => <div {...props1} />
)
)

const wrapper = mount(<Div value={1} />)
const div = wrapper.find('div')

t.is(div.prop('value'), 1)
wrapper.setProps({ value: 2 })
t.is(div.prop('value'), 2)
})
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import test from 'ava'
import { createEventHandler } from '../'

test('createEventHandler creates a subject that broadcasts new values when called as a function', t => {
test('createEventHandler creates an event handler and a corresponding stream', t => {
const result = []
const { stream, handler } = createEventHandler()
const subscription = stream.subscribe(v => result.push(v))
const subscription = stream.subscribe({ next: v => result.push(v) })

handler(1)
handler(2)
handler(3)

subscription.dispose()
subscription.unsubscribe()
t.deepEqual(result, [1, 2, 3])
})

test('handles multiple subscribers', t => {
const result1 = []
const result2 = []
const { handler, stream } = createEventHandler()
const subscription1 = stream.subscribe(v => result1.push(v))
const subscription2 = stream.subscribe(v => result2.push(v))
const subscription1 = stream.subscribe({ next: v => result1.push(v) })
const subscription2 = stream.subscribe({ next: v => result2.push(v) })

handler(1)
handler(2)
handler(3)

subscription1.dispose()
subscription2.dispose()
subscription1.unsubscribe()
subscription2.unsubscribe()

t.deepEqual(result1, [1, 2, 3])
t.deepEqual(result2, [1, 2, 3])
Expand Down
20 changes: 20 additions & 0 deletions src/packages/recompose/__tests__/mapPropsStream-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import test from 'ava'
import React from 'react'
import setObservableConfig from '../setObservableConfig'
import rxjsConfig from '../rxjsObservableConfig'
import { mount } from 'enzyme'
import { mapPropsStream } from '../'

setObservableConfig(rxjsConfig)

// Most of mapPropsStream's functionality is covered by componentFromStream
test('mapPropsStream creates a higher-order component from a stream', t => {
const Double = mapPropsStream(props$ =>
props$.map(({ n }) => ({ children: n * 2 }))
)('div')
const wrapper = mount(<Double n={112} />)
const div = wrapper.find('div')
t.is(div.text(), '224')
wrapper.setProps({ n: 358 })
t.is(div.text(), '716')
})
Loading

0 comments on commit c32e089

Please sign in to comment.