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

Merge rx-recompose into main project #196

Merged
merged 18 commits into from
Jun 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -626,3 +631,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