├── .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 | [![npm version](https://img.shields.io/npm/v/react-reactive-class.svg?style=flat-square)](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 |
35 |
36 | 37 | 38 |
39 |
40 | Count: { count$ } 41 |
42 |
43 | ) 44 | } 45 | 46 | ReactDOM.render(, document.getElementById('root')); 47 | ``` 48 | 49 | ## Features 50 | 51 | - **Reactive DOM elements**: A set of reactive DOM elements (button, div, span, etc). 52 | 53 | - **Reactive wrapper**: A higher order component to wrap a React component to be a Reactive Component. 54 | 55 | ## Installation 56 | ``` 57 | npm install --save react-reactive-class 58 | ``` 59 | 60 | ## Usage 61 | 62 | ### Use reactive DOM elements 63 | 64 | Example: 65 | 66 | ```javascript 67 | import { Subject } from 'rx'; 68 | import React from 'react'; 69 | import ReactDOM from 'react-dom'; 70 | import { dom } from 'react-reactive-class'; 71 | 72 | const { div: Div, span: Span } = dom; 73 | 74 | window.style$ = new Subject(); 75 | window.text$ = new Subject(); 76 | 77 | class App extends React.Component { 78 | render() { 79 | console.log('App rendered.'); 80 | 81 | return ( 82 |
83 |

Demo

84 |
Hello
85 | {window.text$} 86 |
87 | ); 88 | } 89 | } 90 | 91 | ReactDOM.render(, document.getElementById('app')); 92 | 93 | // notice that App will not re-render, nice! 94 | window.style$.onNext({color: 'blue'}); 95 | window.text$.onNext('Reactive!'); 96 | // you can open your console and play around 97 | ``` 98 | 99 | ### Use Reactive wrapper 100 | 101 | Take full control of component lifecycle. 102 | 103 | ``` 104 | reactive(ReactClass): ReactClass 105 | ``` 106 | 107 | Example: 108 | 109 | ```javascript 110 | import { Observable } from 'rx'; 111 | import React from 'react'; 112 | import ReactDOM from 'react-dom'; 113 | import { reactive } from 'react-reactive-class'; 114 | 115 | class Text extends React.Component { 116 | componentWillMount() { 117 | console.log('Text will mount.'); 118 | } 119 | render() { 120 | console.log('Text rendered.'); 121 | 122 | return ( 123 |
{this.props.children}
124 | ); 125 | } 126 | componentWillUnmount() { 127 | console.log('Text will unmount.'); 128 | } 129 | } 130 | 131 | const ReactiveText = reactive(Text); 132 | 133 | 134 | const currentTime$ = Observable 135 | .interval(1000) 136 | .map(() => new Date().toLocaleString()); 137 | 138 | ReactDOM.render( 139 | { currentTime$ }, 140 | document.getElementById('root') 141 | ); 142 | ``` 143 | 144 | ### Mount/unmount Reactive Component 145 | 146 | You can use `mount` attribute to mount/unmount a component. 147 | 148 | ```javascript 149 | // Unmount this component if length of incoming text is 0. 150 | text.length) }> 151 | {text$} 152 | 153 | ``` 154 | 155 | ## Child component constraint 156 | Source must be the only child when using observable as child component. 157 | ```javascript 158 | // This will not work 159 | 160 | Hello {name$}, how are you? 161 | 162 | 163 | // This will work 164 | 165 | Hello {name$}, how are you? 166 | 167 | ``` 168 | 169 | ## Feedbacks are welcome! 170 | Feel free to ask questions or submit pull requests! 171 | 172 | ## License 173 | The MIT License (MIT) 174 | --------------------------------------------------------------------------------