├── .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 | ![ezgif-79206338](https://cloud.githubusercontent.com/assets/762949/14999593/166d7bbe-113f-11e6-9097-69dd24b76781.gif) 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 `