├── .gitignore ├── .npmignore ├── .eslintrc ├── src ├── index.js ├── utils.js └── createReactiveClass.js ├── webpack.config.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | lib 4 | dist 5 | npm-debug.log 6 | examples 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | doc 2 | src 3 | examples 4 | .DS_Store 5 | .eslintrc 6 | .gitignore 7 | .npmignore 8 | webpack.config.js 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createReactiveClass from './createReactiveClass'; 3 | 4 | export function reactive(reactClass) { 5 | return createReactiveClass(reactClass); 6 | } 7 | 8 | export const dom = Object.keys(React.DOM).reduce((result, tag) => { 9 | result[tag] = createReactiveClass(tag); 10 | return result; 11 | }, {}); 12 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function isRxObservable(o) { 2 | const valid = ( 3 | typeof o === 'object' 4 | && typeof o.subscribeOnNext === 'function' 5 | ); 6 | 7 | return valid; 8 | } 9 | 10 | export function pickProps(props, validator) { 11 | const picked = {}; 12 | 13 | Object.keys(props).forEach(key => { 14 | const value = props[key]; 15 | if (validator(key, value)) { 16 | picked[key] = value; 17 | } 18 | }); 19 | 20 | return picked; 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var reactExternal = { 6 | root: 'React', 7 | commonjs2: 'react', 8 | commonjs: 'react', 9 | amd: 'react' 10 | }; 11 | 12 | var plugins = [ 13 | new webpack.DefinePlugin({ 14 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 15 | }), 16 | new webpack.optimize.OccurenceOrderPlugin() 17 | ]; 18 | 19 | if (process.env.NODE_ENV === 'production') { 20 | plugins.push( 21 | new webpack.optimize.UglifyJsPlugin({ 22 | compressor: { 23 | screw_ie8: true, 24 | warnings: false 25 | } 26 | }) 27 | ); 28 | } 29 | 30 | module.exports = { 31 | module: { 32 | loaders: [ 33 | {test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/} 34 | ] 35 | }, 36 | output: { 37 | library: 'ReactReactiveClass', 38 | libraryTarget: 'umd' 39 | }, 40 | externals: { 41 | react: reactExternal 42 | }, 43 | plugins: plugins, 44 | resolve: { 45 | extensions: ['', '.js'] 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-reactive-class", 3 | "version": "1.0.3", 4 | "description": "create reactive react classes", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jas-chen/react-reactive-class.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "reactjs", 13 | "reactive", 14 | "RxJS", 15 | "Event", 16 | "flux" 17 | ], 18 | "scripts": { 19 | "clean": "rimraf lib dist", 20 | "build:lib": "babel src --out-dir lib", 21 | "build:umd": "webpack src/index.js dist/react-reactive-class.js --display-modules --progress && NODE_ENV=production webpack src/index.js dist/react-reactive-class.min.js --display-modules --progress", 22 | "build": "npm run build:lib && npm run build:umd", 23 | "lint": "eslint src", 24 | "prepublish": "npm run clean && npm run build", 25 | "test": "echo \"Error: no test specified\" && exit 1" 26 | }, 27 | "author": "Jas Chen", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "babel-core": "^5.8.22", 31 | "babel-eslint": "^4.1.0", 32 | "babel-loader": "^5.3.2", 33 | "eslint": "^1.3.1", 34 | "eslint-config-airbnb": "0.0.8", 35 | "eslint-plugin-react": "^3.3.0", 36 | "react": "^0.14.0-rc1", 37 | "rimraf": "^2.4.3", 38 | "webpack": "^1.11.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/createReactiveClass.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {isRxObservable, pickProps} from './utils'; 3 | 4 | export default function createReactiveClass(tag) { 5 | class ReactiveClass extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.displayName = `ReactiveElement-${tag}`; 9 | this.state = pickProps(props, (key, value) => !isRxObservable(value)); 10 | this.state.mount = true; 11 | } 12 | 13 | componentWillMount() { 14 | this.subscribe(this.props); 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | this.subscribe(nextProps); 19 | } 20 | 21 | componentWillUnmount() { 22 | this.unsubscribe(); 23 | } 24 | 25 | addPropListener(name, prop$) { 26 | return prop$.subscribeOnNext((value) => { 27 | // don't re-render if value is the same. 28 | if (value === this.state[name]) { 29 | return; 30 | } 31 | 32 | const prop = {}; 33 | prop[name] = value; 34 | this.setState(prop); 35 | }); 36 | } 37 | 38 | subscribe(props) { 39 | if (this.subscriptions) { 40 | this.unsubscribe(); 41 | } 42 | 43 | this.subscriptions = []; 44 | 45 | Object.keys(props).forEach(key => { 46 | const value = props[key]; 47 | if (isRxObservable(value)) { 48 | const subscription = this.addPropListener(key, value); 49 | this.subscriptions.push(subscription); 50 | } 51 | }); 52 | } 53 | 54 | unsubscribe() { 55 | this.subscriptions.forEach(subscription => subscription.dispose()); 56 | this.subscriptions = null; 57 | } 58 | 59 | render() { 60 | if (!this.state.mount) { 61 | return null; 62 | } 63 | 64 | const finalProps = pickProps(this.state, (key) => key !== 'mount'); 65 | return React.createElement(tag, finalProps); 66 | } 67 | } 68 | 69 | return ReactiveClass; 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Reactive Class 2 | > A thin layer between RxJS and React. 3 | 4 | [](https://www.npmjs.com/package/react-reactive-class) 5 | 6 | ## What? 7 | With React Reactive Class, you can create Reactive Components, which 8 | subscribe Rx.Observables and re-render themselves. 9 | 10 | ## Counter example 11 | 12 | You can compare this example to [Counter example of Cycle.js](https://github.com/cyclejs/cyclejs/blob/master/examples/basic/counter/src/main.js) and [Counter example of Yolk](https://github.com/yolkjs/yolk#example). 13 | 14 | ```javascript 15 | import { Subject, Observable } from 'rx'; 16 | import React from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import { dom } from 'react-reactive-class'; 19 | 20 | const { span: Span } = dom; 21 | 22 | function Counter () { 23 | const plusClick$ = new Subject(); 24 | const minusClick$ = new Subject(); 25 | 26 | const action$ = Observable.merge( 27 | plusClick$.map(() => 1), 28 | minusClick$.map(() => -1) 29 | ); 30 | 31 | const count$ = action$.scan((x, y) => x + y, 0).startWith(0); 32 | 33 | return ( 34 |