├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── prepublish.js ├── src ├── components │ ├── Provider.js │ └── connectWithState.js └── index.js ├── test ├── test-Provider.js └── test-connectWithState.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": [ 4 | "transform-decorators-legacy", 5 | "transform-runtime" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | webpack.config*.js 3 | build/* 4 | prepublish.js 5 | dist/* 6 | lib/* 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "dacz", 3 | rules: { 4 | 'no-param-reassign': 0 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.env 3 | *.log 4 | npm-debug.log 5 | .DS_Store 6 | dist 7 | lib 8 | coverage 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dacz/rxr-react/b79d9b6d2a319fcb680013e20f8776b3687d1543/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | script: 5 | - npm run lint 6 | - npm test 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | All notable changes are described on the [Releases](https://github.com/dacz/rxr-react/releases) page. 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We are open to, and grateful for, any contributions made by the community. By contributing to rxr-react, you agree to abide by the [code of conduct](https://github.com/dacz/rxr-react/blob/master/CODE_OF_CONDUCT.md). 3 | 4 | ## Reporting Issues and Asking Questions 5 | Before opening an issue, please search the [issue tracker](https://github.com/dacz/rxr-react/issues) to make sure your issue hasn't already been reported. 6 | 7 | ## Development 8 | 9 | Visit the [Issue tracker](https://github.com/dacz/rxr-react/issues) to find a list of open issues that need attention. 10 | 11 | Fork, then clone the repo: 12 | ``` 13 | git clone https://github.com/your-username/rxr-react.git 14 | ``` 15 | 16 | ### Building 17 | 18 | Running the `build` task will create both a CommonJS module-per-module build and a UMD build. 19 | ``` 20 | npm run build 21 | ``` 22 | 23 | To create just a CommonJS module-per-module build: 24 | ``` 25 | npm run build:lib 26 | ``` 27 | 28 | To create just a UMD build: 29 | ``` 30 | npm run build:umd 31 | npm run build:umd:min 32 | ``` 33 | 34 | ### Testing and Linting 35 | 36 | To run the tests: 37 | ``` 38 | npm run test 39 | ``` 40 | 41 | To continuously watch and run tests, run the following: 42 | ``` 43 | npm run test:watch 44 | ``` 45 | 46 | To perform linting with `eslint`, run the following: 47 | ``` 48 | npm run lint 49 | ``` 50 | 51 | ###New Features 52 | 53 | Please open an issue with a proposal for a new feature or refactoring before starting on the work. We don't want you to waste your efforts on a pull request that we won't want to accept. 54 | 55 | 56 | ## Submitting Changes 57 | 58 | * Open a new issue in the [Issue tracker](https://github.com/dacz/rxr-react/issues). 59 | * Fork the repo. 60 | * Create a new feature branch based off the `master` branch. 61 | * Make sure all tests pass and there are no linting errors. 62 | * Submit a pull request, referencing any issues it addresses. 63 | 64 | Please try to keep your pull request focused in scope and avoid including unrelated commits. 65 | 66 | After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements. 67 | 68 | Thank you for contributing! 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Cizek 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rxr-react 2 | 3 | [![Build Status](https://travis-ci.org/dacz/rxr-react.svg?branch=master)](https://travis-ci.org/dacz/rxr-react) 4 | [![npm](https://img.shields.io/npm/v/rxr-react.svg?maxAge=2592000)](https://www.npmjs.com/package/rxr-react) 5 | 6 | React bindings for [rxr](https://github.com/dacz/rxr) (RxJS the Redux way). 7 | 8 | > Use [RxJS](https://github.com/ReactiveX/rxjs) with React the way similar to Redux. If you know Redux, rxr introduces very similar concept using into RxJS. It allows to **rewrite Redux app** to use pure RxJS with rxr fast with **most of the main code intact**. This is huge benefit when you want just play with something you already have. To learn, to play... ;) 9 | > 10 | >I'm sure there are different ways how to use RxJS with React. Redux is very popular (and I like it a lot!) and it established some code structure and thinking about the app. Applying RxJS the Redux way has a lot of benefits. We may build on what we know and therefore it's easier to learn RxJS. 11 | 12 | ... [read more about rxr](https://dacz.github.io/rxr/). 13 | 14 | ... demo example with commented differences between Redux and RxR in the docs: [rxr-redux-example](https://github.com/dacz/rxr-redux-example). 15 | 16 | 17 | ## Basic usage 18 | 19 | ```javascript 20 | import React from 'react'; 21 | import { render } from 'react-dom'; 22 | import { Provider } from 'rxr-react'; 23 | import { createState, createLoggerStream, startLogging, messageStreamsMonitor$ } from 'rxr'; 24 | 25 | import styles from './index.css'; 26 | 27 | import App from './components/App'; 28 | 29 | // our RxR reducers 30 | import reducer$ from './reducers'; 31 | 32 | // we create initial state here 33 | const initialState = { 34 | clients: { data: [], ts: 0, status: undefined }, 35 | filter: '', 36 | selectedClient: '', 37 | }; 38 | 39 | // and because in RxR is no need of store, we create state directly 40 | const state$ = createState(reducer$, initialState); 41 | 42 | // we will log all state changes and messageStreams events to console 43 | const loggerStream$ = createLoggerStream(state$, messageStreamsMonitor$); 44 | startLogging(loggerStream$); 45 | 46 | // RxR-React provides similar Provider component as React-Redux 47 | render( 48 | 49 | 50 | , document.getElementById('index') 51 | ); 52 | ``` 53 | 54 | and to connect the component... 55 | 56 | ```javascript 57 | import { connectWithState } from 'rxr-react'; 58 | import MyContainer from './MyContainer'; 59 | // lets's suppose that our userSelected$ stream was bound with next() 60 | // (see RxR createPushMessageFunctions) and is part of userActions 61 | // structure 62 | import userActions from './userActions'; 63 | 64 | const selector = (state) => ({ 65 | itemsSelected: state.itemsSelected, 66 | userSelected: myMessageStreams.userSelected, 67 | }); 68 | 69 | const MyHoCContainer = connectWithState(selector)(MyContainer); 70 | ``` 71 | 72 | The props `MyContainer` gets are `itemsSelected` (array to display) and `userSelected` - action that is supposed to be invoked somehow like `... onClick={ userSelected('itemA') } ...`. 73 | 74 | ### Working Demo 75 | 76 | Example with commented differences between Redux and RxR in the docs: [rxr-redux-example](https://github.com/dacz/rxr-redux-example). 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxr-react", 3 | "version": "0.1.2", 4 | "description": "React bindings for rxr - RxJS", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "dist", 8 | "lib", 9 | "src" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf lib dist es", 13 | "lint": "eslint src test", 14 | "test": "cross-env NODE_ENV=test ava", 15 | "test:watch": "npm test -- --watch -v", 16 | "check:src": "npm run lint && npm run test", 17 | "build:lib": "babel src --out-dir lib", 18 | "build:umd": "cross-env NODE_ENV=development webpack src/index.js dist/rxr-react.js", 19 | "build:umd:min": "cross-env NODE_ENV=production webpack src/index.js dist/rxr-react.min.js", 20 | "build": "npm run build:lib && npm run build:umd && npm run build:umd:min && node ./prepublish", 21 | "prepublish": "npm run clean && npm run build" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/dacz/rxr-react.git" 26 | }, 27 | "keywords": [ 28 | "react", 29 | "reactjs", 30 | "rxjs", 31 | "reactive", 32 | "rxr", 33 | "redux", 34 | "reducer", 35 | "state", 36 | "functional" 37 | ], 38 | "author": "David Cizek (https://twitter.com/dadc)", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/dacz/rxr-react/issues" 42 | }, 43 | "homepage": "https://github.com/dacz/rxr-react", 44 | "ava": { 45 | "files": [ 46 | "test/**/test-*.js" 47 | ], 48 | "require": [ 49 | "babel-register" 50 | ], 51 | "babel": "inherit" 52 | }, 53 | "devDependencies": { 54 | "ava": "^0.15.2", 55 | "babel": "^6.5.2", 56 | "babel-cli": "^6.10.1", 57 | "babel-core": "^6.10.4", 58 | "babel-eslint": "^6.1.2", 59 | "babel-loader": "^6.2.4", 60 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 61 | "babel-plugin-transform-runtime": "^6.9.0", 62 | "babel-preset-es2015": "^6.9.0", 63 | "babel-preset-react": "^6.11.1", 64 | "babel-preset-stage-0": "^6.5.0", 65 | "babel-register": "^6.9.0", 66 | "cross-env": "^2.0.0", 67 | "enzyme": "^2.4.1", 68 | "es3ify": "^0.2.2", 69 | "eslint": "^3.1.1", 70 | "eslint-config-dacz": "^0.2.0", 71 | "eslint-plugin-import": "^1.11.0", 72 | "eslint-plugin-jsx-a11y": "^2.0.1", 73 | "eslint-plugin-react": "^5.2.2", 74 | "jsdom": "^9.4.1", 75 | "react": "^15.2.1", 76 | "react-addons-test-utils": "^15.2.1", 77 | "react-dom": "^15.2.1", 78 | "rimraf": "^2.5.3", 79 | "webpack": "^1.13.1" 80 | }, 81 | "dependencies": { 82 | "is-observable": "^0.2.0", 83 | "rxjs": "^5.0.0-beta.10", 84 | "rxjs-es": "^5.0.0-beta.10", 85 | "symbol-observable": "^1.0.1" 86 | }, 87 | "peerDependencies": { 88 | "react": "^15.2.1" 89 | }, 90 | "npmName": "rxr-react", 91 | "npmFileMap": [ 92 | { 93 | "basePath": "/dist/", 94 | "files": [ 95 | "*.js" 96 | ] 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /prepublish.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob') 2 | var fs = require('fs') 3 | var es3ify = require('es3ify') 4 | 5 | glob('./@(lib|dist)/**/*.js', function (err, files) { 6 | if (err) { 7 | throw err 8 | } 9 | 10 | files.forEach(function (file) { 11 | fs.readFile(file, 'utf8', function (err, data) { 12 | if (err) { 13 | throw err 14 | } 15 | 16 | fs.writeFile(file, es3ify.transform(data), function (err) { 17 | if (err) { 18 | throw err 19 | } 20 | 21 | console.log('es3ified ' + file) // eslint-disable-line no-console 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/components/Provider.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes, Children } from 'react'; 2 | 3 | export default class Provider extends Component { 4 | 5 | static propTypes = { 6 | state$: PropTypes.object.isRequired, 7 | children: PropTypes.element.isRequired, 8 | }; 9 | 10 | static childContextTypes = { 11 | state$: PropTypes.object.isRequired, 12 | } 13 | 14 | constructor(props, context) { 15 | super(props, context); 16 | this.state$ = props.state$; 17 | } 18 | 19 | getChildContext() { 20 | return { state$: this.state$ }; 21 | } 22 | 23 | render() { 24 | return Children.only(this.props.children); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/connectWithState.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import deepEqual from 'deep-equal'; 3 | 4 | const connectWithState = (selector = (state) => state) => (WrappedComponent) => 5 | class ConnectWithState extends React.Component { 6 | 7 | static contextTypes = { 8 | state$: PropTypes.object, 9 | }; 10 | 11 | constructor(props, context) { 12 | super(props, context); 13 | this.state$ = context.state$; 14 | } 15 | 16 | componentWillMount() { 17 | this.subscription = this.state$ 18 | .map(selector) 19 | .distinctUntilChanged((a, b) => deepEqual(a, b)) 20 | .subscribe(::this.setState); 21 | } 22 | 23 | componentWillUnmount() { 24 | this.subscription.unsubscribe(); 25 | } 26 | 27 | render() { 28 | return ( 29 | 30 | ); 31 | } 32 | }; 33 | 34 | export default connectWithState; 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Provider from './components/Provider'; 2 | import connectWithState from './components/connectWithState'; 3 | 4 | export { Provider, connectWithState }; 5 | -------------------------------------------------------------------------------- /test/test-Provider.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import React, { PropTypes } from 'react'; 3 | import { mount } from 'enzyme'; 4 | import Provider from '../src/components/Provider'; 5 | 6 | import jsdom from 'jsdom'; 7 | const doc = jsdom.jsdom(''); 8 | global.document = doc; 9 | global.window = doc.defaultView; 10 | 11 | const MyComponent = (props, context) =>
{ context.state$.some }
; 12 | MyComponent.contextTypes = { 13 | state$: PropTypes.object, 14 | }; 15 | 16 | test('provides state$ as a context', t => { 17 | const state = { some: 'value' }; 18 | const WrappedComponent = ; 19 | const renderedComponent = mount(WrappedComponent); 20 | t.is(renderedComponent.text(), state.some); 21 | }); 22 | -------------------------------------------------------------------------------- /test/test-connectWithState.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import React from 'react'; 3 | import Rx from 'rxjs'; 4 | import { mount } from 'enzyme'; 5 | import connectWithState from '../src/components/connectWithState'; 6 | import Provider from '../src/components/Provider'; 7 | 8 | import jsdom from 'jsdom'; 9 | const doc = jsdom.jsdom(''); 10 | global.document = doc; 11 | global.window = doc.defaultView; 12 | 13 | const MyComponent = (props) =>
{ JSON.stringify(props) }
; 14 | 15 | 16 | test('connects with state', t => { 17 | const state$ = new Rx.ReplaySubject(1); 18 | 19 | // initial state 20 | state$.next({}); 21 | const WrappedComponent = connectWithState()(MyComponent); 22 | const renderedComponent = mount(); 23 | 24 | let text = JSON.parse(renderedComponent.find('.myDiv').text()); 25 | t.deepEqual(text, {}); 26 | 27 | // value 28 | const nextState = { value: 'myValue' }; 29 | state$.next(nextState); 30 | text = JSON.parse(renderedComponent.find('.myDiv').text()); 31 | t.deepEqual(text, nextState); 32 | }); 33 | 34 | 35 | test('connects with state with selector', t => { 36 | const state$ = new Rx.ReplaySubject(1); 37 | 38 | // initial state 39 | state$.next({}); 40 | const selector = (state) => ({ 41 | wantProp: state.fromProp ? state.fromProp * 2 : 0, 42 | }); 43 | const WrappedComponent = connectWithState(selector)(MyComponent); 44 | const renderedComponent = mount(); 45 | 46 | // empty 47 | let text = JSON.parse(renderedComponent.find('.myDiv').text()); 48 | t.deepEqual(text, { wantProp: 0 }, 'empty'); 49 | 50 | // not wanted state prop 51 | let nextState = { value: 'myValue' }; 52 | state$.next(nextState); 53 | text = JSON.parse(renderedComponent.find('.myDiv').text()); 54 | t.deepEqual(text, { wantProp: 0 }, 'not wanted state prop'); 55 | 56 | // wanted state prop (transformed) 57 | nextState = { fromProp: 5 }; 58 | state$.next(nextState); 59 | text = JSON.parse(renderedComponent.find('.myDiv').text()); 60 | t.deepEqual(text, { wantProp: 10 }, 'wanted and transformed'); 61 | }); 62 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var webpack = require('webpack') 4 | var env = process.env.NODE_ENV 5 | 6 | var reactExternal = { 7 | root: 'React', 8 | commonjs2: 'react', 9 | commonjs: 'react', 10 | amd: 'react' 11 | } 12 | 13 | var rxrExternal = { 14 | root: 'rxr', 15 | commonjs2: 'rxr', 16 | commonjs: 'rxr', 17 | amd: 'rxr' 18 | } 19 | 20 | var config = { 21 | externals: { 22 | 'react': reactExternal, 23 | 'rxr': rxrExternal 24 | }, 25 | module: { 26 | loaders: [ 27 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } 28 | ] 29 | }, 30 | output: { 31 | library: 'ReactReactive', 32 | libraryTarget: 'umd' 33 | }, 34 | plugins: [ 35 | { 36 | apply: function apply(compiler) { 37 | compiler.parser.plugin('expression global', function expressionGlobalPlugin() { 38 | this.state.module.addVariable('global', "(function() { return this; }()) || Function('return this')()") 39 | return false 40 | }) 41 | } 42 | }, 43 | new webpack.optimize.OccurenceOrderPlugin(), 44 | new webpack.DefinePlugin({ 45 | 'process.env.NODE_ENV': JSON.stringify(env) 46 | }) 47 | ] 48 | } 49 | 50 | if (env === 'production') { 51 | config.plugins.push( 52 | new webpack.optimize.UglifyJsPlugin({ 53 | compressor: { 54 | pure_getters: true, 55 | unsafe: true, 56 | unsafe_comps: true, 57 | screw_ie8: true, 58 | warnings: false 59 | } 60 | }) 61 | ) 62 | } 63 | 64 | module.exports = config 65 | --------------------------------------------------------------------------------