├── .travis.yml
├── test
├── mocha.opts
└── index.spec.js
├── .npmignore
├── .gitignore
├── .babelrc
├── examples
└── basic
│ ├── .babelrc
│ ├── index.html
│ ├── index.js
│ ├── server.js
│ ├── components
│ ├── App.js
│ └── Example.js
│ ├── webpack.config.js
│ └── package.json
├── webpack.config.js
├── LICENSE.md
├── package.json
├── src
└── index.js
└── README.md
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "iojs"
4 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --compilers js:babel-register
2 | --recursive
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | src
4 | test
5 | examples
6 | coverage
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | lib
6 | coverage
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": [
4 | "transform-object-rest-spread",
5 | "transform-class-properties"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import Subscribe from '../src';
3 |
4 | describe(' ', () => {
5 | it.skip('should work #YOLO', () => {
6 |
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/examples/basic/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": [
4 | "transform-object-rest-spread",
5 | "transform-class-properties",
6 | "transform-decorators-legacy"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Example
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/basic/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { browserHistory, Router, Route, Link } from 'react-router';
4 | import App from './components/App';
5 |
6 | render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/examples/basic/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var WebpackDevServer = require('webpack-dev-server');
3 | var config = require('./webpack.config');
4 |
5 | new WebpackDevServer(webpack(config), {
6 | publicPath: config.output.publicPath,
7 | hot: true,
8 | historyApiFallback: true,
9 | stats: {
10 | colors: true
11 | }
12 | }).listen(3000, 'localhost', function (err) {
13 | if (err) {
14 | console.log(err);
15 | }
16 |
17 | console.log('Listening at localhost:3000');
18 | });
19 |
--------------------------------------------------------------------------------
/examples/basic/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Observable } from 'rxjs/Observable';
3 | import 'rxjs/add/observable/interval';
4 | import 'rxjs/add/operator/startWith';
5 | import Example from './Example';
6 |
7 | export default
8 | class App extends Component {
9 | render() {
10 | // However you get your stream, e.g. redux or traditional state management
11 | // Can be any Observable that has observable[Symbol.observable]() which
12 | // is part of the TC39 observable spec for interop
13 | const stream = Observable.interval(100).startWith('--');
14 | return (
15 |
16 |
Example of Observable streaming
17 |
18 |
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var webpack = require('webpack');
4 |
5 | var plugins = [
6 | new webpack.optimize.OccurenceOrderPlugin(),
7 | new webpack.DefinePlugin({
8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
9 | })
10 | ];
11 |
12 | if (process.env.NODE_ENV === 'production') {
13 | plugins.push(
14 | new webpack.optimize.UglifyJsPlugin({
15 | compressor: {
16 | screw_ie8: true,
17 | warnings: false
18 | }
19 | })
20 | );
21 | }
22 |
23 | module.exports = {
24 | module: {
25 | loaders: [{
26 | test: /\.js$/,
27 | loaders: ['babel-loader'],
28 | exclude: /node_modules/
29 | }]
30 | },
31 | output: {
32 | library: 'library-boilerplate',
33 | libraryTarget: 'umd'
34 | },
35 | plugins: plugins,
36 | resolve: {
37 | extensions: ['', '.js']
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/examples/basic/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'eval',
6 | entry: [
7 | 'webpack-dev-server/client?http://localhost:3000',
8 | 'webpack/hot/only-dev-server',
9 | './index'
10 | ],
11 | output: {
12 | path: path.join(__dirname, 'dist'),
13 | filename: 'bundle.js',
14 | publicPath: '/static/'
15 | },
16 | plugins: [
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | resolve: {
21 | alias: {
22 | 'react-observable-subscribe': path.join(__dirname, '..', '..', 'src')
23 | },
24 | extensions: ['', '.js']
25 | },
26 | module: {
27 | loaders: [{
28 | test: /\.js$/,
29 | loaders: ['react-hot', 'babel'],
30 | exclude: /node_modules/,
31 | include: __dirname
32 | }, {
33 | test: /\.js$/,
34 | loaders: ['babel'],
35 | include: path.join(__dirname, '..', '..', 'src')
36 | }]
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/examples/basic/components/Example.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import 'rxjs/add/operator/map';
3 | import 'rxjs/add/operator/throttleTime';
4 | import Subscribe from 'react-observable-subscribe';
5 |
6 | export default
7 | class Example extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
Every 100ms:
13 |
14 | {this.props.stream}
15 |
16 |
17 |
18 |
Every 1s:
19 |
20 | {this.props.stream.throttleTime(1000)}
21 |
22 |
23 |
24 |
Every 100ms, returning an <input> element:
25 |
26 |
27 | {this.props.stream.map(
28 | num =>
29 | )}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-present Jay Phelps
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-observable-subscribe-example",
3 | "version": "1.0.0",
4 | "description": "react-observable-subscribe-example-description",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/jayphelps/react-observable-subscribe.git"
12 | },
13 | "keywords": [
14 | "react-observable-subscribe-keywords"
15 | ],
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/jayphelps/react-observable-subscribe/issues"
19 | },
20 | "homepage": "https://github.com/jayphelps/react-observable-subscribe",
21 | "dependencies": {
22 | "react": "^15.0.1",
23 | "react-dom": "^15.0.1",
24 | "react-router": "^2.3.0",
25 | "rxjs": "^5.0.0-beta.6"
26 | },
27 | "devDependencies": {
28 | "babel-cli": "^6.7.7",
29 | "babel-core": "^6.7.7",
30 | "babel-loader": "^6.2.4",
31 | "babel-preset-es2015": "^6.6.0",
32 | "babel-preset-react": "^6.5.0",
33 | "node-libs-browser": "^0.5.2",
34 | "react-hot-loader": "^1.2.7",
35 | "webpack": "^1.9.11",
36 | "webpack-dev-server": "^1.9.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-observable-subscribe",
3 | "version": "0.1.7",
4 | "description": " component to automatically consume observables declaratively in JSX",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "clean": "rimraf lib dist",
8 | "build": "babel src --out-dir lib",
9 | "build:umd": "webpack src/index.js dist/react-observable-subscribe.js && NODE_ENV=production webpack src/index.js dist/react-observable-subscribe.min.js",
10 | "test": "NODE_ENV=test mocha",
11 | "test:watch": "NODE_ENV=test mocha --watch",
12 | "test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha",
13 | "prepublish": "npm run test && npm run clean && npm run build && npm run build:umd"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/jayphelps/react-observable-subscribe.git"
18 | },
19 | "keywords": [
20 | "react",
21 | "observable",
22 | "subscribe",
23 | "JSX"
24 | ],
25 | "author": "Jay Phelps ",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/jayphelps/react-observable-subscribe/issues"
29 | },
30 | "homepage": "https://github.com/jayphelps/react-observable-subscribe",
31 | "devDependencies": {
32 | "babel-cli": "^6.7.7",
33 | "babel-core": "^6.7.7",
34 | "babel-loader": "^6.2.4",
35 | "babel-plugin-transform-class-properties": "^6.6.0",
36 | "babel-plugin-transform-object-rest-spread": "^6.6.5",
37 | "babel-preset-es2015": "^6.6.0",
38 | "babel-preset-react": "^6.5.0",
39 | "expect": "^1.18.0",
40 | "isparta": "^4.0.0",
41 | "mocha": "^2.4.5",
42 | "react": "^15.0.1",
43 | "react-dom": "^15.0.1",
44 | "react-router": "^2.3.0",
45 | "rimraf": "^2.5.2",
46 | "webpack": "^1.13.0",
47 | "webpack-dev-server": "^1.14.1"
48 | },
49 | "peerDependencies": {
50 | "react": "*"
51 | },
52 | "dependencies": {
53 | "exenv": "1.2.1",
54 | "symbol-observable": "0.2.4"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Children, isValidElement } from 'react';
2 | import $$observable from 'symbol-observable';
3 | import { canUseDOM, canUseWorkers } from 'exenv';
4 |
5 | const ERROR_NOT_AN_OBSERVABLE = ' only accepts a single child, an Observable that conforms to observable[Symbol.observable]()';
6 |
7 | function childrenToObservable(children) {
8 | if (typeof children === 'array') {
9 | if (children.length > 1) {
10 | throw new TypeError(ERROR_NOT_AN_OBSERVABLE);
11 | }
12 |
13 | children = children[0];
14 | }
15 |
16 | if ($$observable in children === false) {
17 | throw new TypeError(ERROR_NOT_AN_OBSERVABLE);
18 | }
19 |
20 | return children[$$observable]();
21 | }
22 |
23 | export default
24 | class Subscribe extends Component {
25 | subscription = null;
26 |
27 | state = {
28 | element: null
29 | };
30 |
31 | setupSubscription() {
32 | const { children } = this.props;
33 | if (children !== undefined && children !== null) {
34 | // Observables may be scheduled async or sync, so this subscribe callback
35 | // might immediately run or it it might not.
36 | this.subscription = childrenToObservable(children).subscribe(element => {
37 | if (Array.isArray(element)) {
38 | throw new TypeError(' streams cannot return arrays because of React limitations');
39 | }
40 |
41 | this.setState({ element });
42 | });
43 | }
44 | }
45 |
46 | teardownSubscription() {
47 | if (this.subscription) {
48 | this.subscription.unsubscribe();
49 | }
50 | }
51 |
52 | componentWillMount() {
53 | this.setupSubscription();
54 |
55 | // When server-side rendering only this lifecycle hook is used so
56 | // componentWillUnmount() is NEVER run to dispose of subscription. It's also
57 | // pointless to wait for any async values since they won't be rendered.
58 | if (!canUseDOM && !canUseWorkers) {
59 | this.teardownSubscription();
60 | }
61 | }
62 |
63 | componentWillReceiveProps(nextProps) {
64 | if (nextProps.children !== this.props.children) {
65 | this.teardownSubscription();
66 | this.setupSubscription();
67 | }
68 | }
69 |
70 | componentWillUmount() {
71 | this.teardownSubscription();
72 | }
73 |
74 | render() {
75 | const { element } = this.state;
76 | return (
77 | isValidElement(element)
78 | ? element
79 | : {element}
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-observable-subscribe
2 | =========================
3 |
4 | Subscribe to an Observable declaratively with the `{observable} ` component.
5 |
6 | ## Install
7 |
8 | ```bash
9 | npm install --save react-observable-subscribe
10 | ```
11 |
12 | ## Usage
13 |
14 | This library's default export is a `Subscribe` component which you can use in your JSX to declaratively subscribe and render Observable streams. Just pass the observable as the only child to the the component.
15 |
16 | You can apply any operators you want to your observable before passing it to ``--you don't have to use [RxJS v5.0](https://github.com/ReactiveX/rxjs), the only requirement is the the observable supports `observable[Symbol.observable]()` [from the proposed Observable spec](https://github.com/zenparsing/es-observable#observable).
17 |
18 | *This library doesn't come with any actual Observable implementation itself.*
19 |
20 | ```jsx
21 | import Subscribe from 'react-observable-subscribe';
22 | // ...other imports
23 |
24 | class Example extends Component {
25 | render() {
26 | return (
27 |
28 |
29 |
Every 100ms:
30 |
31 | {this.props.stream}
32 |
33 |
34 |
35 |
Every 1s:
36 |
37 | {this.props.stream.throttleTime(1000)}
38 |
39 |
40 |
41 |
Every 100ms w/ <input> element:
42 |
43 |
44 | {this.props.stream.map(
45 | value =>
46 | )}
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
55 | // This Observable will emit an incrementing
56 | // number every 100ms
57 | let stream = Observable.interval(100);
58 | ReactDOM.render( , container);
59 |
60 | ```
61 | 
62 |
63 | The observable can emit simple primitives (e.g. strings, numbers) or you can even emit JSX elements! Each "onNext" just needs to be a single value, arrays are not supported because of React limitations.
64 |
65 | When the observable emits new values, only the content inside `` will re-render, not the component in which you declare it, so it's very efficient.
66 |
67 | Depending on your preferences, you might find it helpful to use a shorter name for `Subscribe` when you import it. Since it's the default export, what you name it is totally up to you:
68 |
69 | ```jsx
70 | import Sub from 'react-observable-subscribe';
71 |
72 | // etc
73 |
74 |
75 | {stream.throttleTime(1000)}
76 |
77 | ```
78 | ## Server-side rendering
79 |
80 | If you do Server-side rendering with `React.renderToString`, it's important to note that since React doesn't support asynchronous rendering `` will `subscribe()` to the stream but then immediately `unsubscribe()`, so any *synchronously emitted* value will be rendered, otherwise nothing. One approach to emit a synchronous value in RxJS v5 is the [`startWith(value)`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-startWith) operator, e.g. you might emit some "Loading..." text or `