├── .bowerrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── README.md ├── app ├── index.html ├── robots.txt ├── scripts │ ├── actions.js │ ├── app.js │ ├── components │ │ └── App.js │ ├── reducer.js │ ├── rxflux.js │ └── utils.js └── styles │ └── main.scss ├── bower.json ├── gulpfile.js └── package.json /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "react" 4 | ], 5 | "ecmaFeatures": { 6 | "jsx": true, 7 | "modules": true 8 | }, 9 | "env": { 10 | "browser": true, 11 | "amd": true, 12 | "es6": true, 13 | "node": true 14 | }, 15 | "rules": { 16 | "comma-dangle": 1, 17 | "quotes": [ 1, "single" ], 18 | "no-undef": 1, 19 | "global-strict": 0, 20 | "no-extra-semi": 1, 21 | "no-underscore-dangle": 0, 22 | "no-console": 1, 23 | "no-unused-vars": 1, 24 | "no-trailing-spaces": [1, { "skipBlankLines": true }], 25 | "no-unreachable": 1, 26 | "no-alert": 0, 27 | "react/jsx-uses-react": 1 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .sass-cache/ 4 | app/bower_components/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 5.11.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.1.0' 4 | before_install: 5 | - currentfolder=${PWD##*/} 6 | - if [ "$currentfolder" != 'react-webpack-template' ]; then cd .. && eval "mv $currentfolder react-webpack-template" && cd react-webpack-template; fi 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rxflux 2 | 3 | A simple effective Redux style framework using Rx 4 | 5 | *This is an example of how you might create a simple Flux/Redux style framework using nothing other than [RxJS](http://reactivex.io/rxjs)* 6 | 7 | I am sharing this for educational purposes. 8 | 9 | ## Installation 10 | 11 | Switch to use node 5.11.1 or above. 12 | 13 | ```bash 14 | $ nvm use 15 | ``` 16 | 17 | Install dependencies 18 | 19 | ```bash 20 | $ npm i 21 | ``` 22 | 23 | ## Run 24 | 25 | Run development mode 26 | 27 | ```bash 28 | $ npm start 29 | ``` 30 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redurx 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/scripts/actions.js: -------------------------------------------------------------------------------- 1 | import { actionCreator } from './rxflux'; 2 | import { Observable } from 'rxjs/Rx'; 3 | import { map } from 'lodash'; 4 | 5 | export const loadGithubFollowers = actionCreator((payload) => { 6 | const url = `https://api.github.com/users/${payload.username}/followers`; 7 | return { 8 | type: 'GITHUB_FOLLOWERS_LOADING', 9 | payload: Observable.ajax(url) 10 | .map((xhr) => map(xhr.response, 'login')) 11 | .map((followers) => ({ 12 | type: 'GITHUB_FOLLOWERS_LOADED', 13 | payload: followers 14 | })) 15 | }; 16 | }); 17 | 18 | export const changeName = actionCreator((payload) => ({ 19 | type: 'NAME_CHANGED', 20 | payload 21 | })); 22 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | import { createStore } from './rxflux'; 6 | import { log } from './utils'; 7 | 8 | window.React = React; 9 | 10 | const container = document.getElementById('app'); 11 | 12 | const initState = { name: 'Harry' }; 13 | 14 | createStore(initState) 15 | .do(log) 16 | .subscribe((state) => 17 | ReactDOM.render(, container) 18 | ); 19 | 20 | -------------------------------------------------------------------------------- /app/scripts/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | import { changeName, loadGithubFollowers } from '../actions'; 4 | 5 | const handleChangeName = (data) => () => 6 | changeName(data); 7 | 8 | const handleLoadFollowers = (data) => () => 9 | loadGithubFollowers(data); 10 | 11 | 12 | function renderUsers(users) { 13 | if (!users) return; 14 | return ( 15 | 16 | ); 17 | } 18 | 19 | export default function App(props) { 20 | const { isLoading, name, users } = props; 21 | return ( 22 |
23 | { isLoading ? 24 |

Loading...

: 25 |

{ name }

} 26 | { renderUsers(users) } 27 | 28 | 29 |
30 | 31 |
32 | ); 33 | } 34 | 35 | App.propTypes = { 36 | name: PropTypes.string, 37 | users: PropTypes.array, 38 | isLoading: PropTypes.bool 39 | }; 40 | -------------------------------------------------------------------------------- /app/scripts/reducer.js: -------------------------------------------------------------------------------- 1 | export default function reducer(state, action) { 2 | switch (action.type) { 3 | case 'GITHUB_FOLLOWERS_LOADING': 4 | return { 5 | ...state, 6 | isLoading: true 7 | }; 8 | case 'GITHUB_FOLLOWERS_LOADED': 9 | return { 10 | ...state, 11 | isLoading: false, 12 | users: action.payload, 13 | }; 14 | case 'NAME_CHANGED': 15 | return { 16 | ...state, 17 | isLoading: false, 18 | name: action.payload 19 | }; 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/scripts/rxflux.js: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | import { Subject } from 'rxjs/Subject'; 3 | import 'rxjs/add/operator/do'; 4 | import 'rxjs/add/operator/mergeMap'; 5 | import 'rxjs/add/operator/scan'; 6 | import 'rxjs/add/operator/startWith'; 7 | import reducer from './reducer'; 8 | import { isObservable } from './utils'; 9 | 10 | const action$ = new Subject(); 11 | 12 | export const createStore = (initState) => 13 | action$ 14 | .flatMap((action) => isObservable(action) ? action : Observable.from([action])) 15 | .startWith(initState) 16 | .scan(reducer); 17 | 18 | export const actionCreator = (func) => (...args) => { 19 | const action = func.call(null, ...args); 20 | action$.next(action); 21 | if (isObservable(action.payload)) 22 | action$.next(action.payload); 23 | return action; 24 | }; 25 | -------------------------------------------------------------------------------- /app/scripts/utils.js: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | export const isObservable = obs => obs instanceof Observable; 3 | export const log = console.log.bind(console); 4 | 5 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background: #fafafa; 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | color: #333; 6 | } 7 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redurx", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var $ = require('gulp-load-plugins')(); 5 | var sync = $.sync(gulp).sync; 6 | var del = require('del'); 7 | var browserify = require('browserify'); 8 | var watchify = require('watchify'); 9 | var source = require('vinyl-source-stream'); 10 | 11 | var bundler = { 12 | w: null, 13 | init: function() { 14 | this.w = watchify(browserify({ 15 | entries: ['./app/scripts/app.js'], 16 | insertGlobals: true, 17 | cache: {}, 18 | packageCache: {} 19 | })); 20 | }, 21 | bundle: function() { 22 | return this.w && this.w.bundle() 23 | .on('error', $.util.log.bind($.util, 'Browserify Error')) 24 | .pipe(source('app.js')) 25 | .pipe(gulp.dest('dist/scripts')); 26 | }, 27 | watch: function() { 28 | this.w && this.w.on('update', this.bundle.bind(this)); 29 | }, 30 | stop: function() { 31 | this.w && this.w.close(); 32 | } 33 | }; 34 | 35 | gulp.task('styles', function() { 36 | return $.rubySass('app/styles/main.scss', { 37 | style: 'expanded', 38 | precision: 10, 39 | loadPath: ['app/bower_components'] 40 | }) 41 | .on('error', $.util.log.bind($.util, 'Sass Error')) 42 | .pipe($.autoprefixer('last 1 version')) 43 | .pipe(gulp.dest('dist/styles')) 44 | .pipe($.size()); 45 | }); 46 | 47 | gulp.task('scripts', function() { 48 | bundler.init(); 49 | return bundler.bundle(); 50 | }); 51 | 52 | gulp.task('html', function() { 53 | var assets = $.useref.assets(); 54 | return gulp.src('app/*.html') 55 | .pipe(assets) 56 | .pipe(assets.restore()) 57 | .pipe($.useref()) 58 | .pipe(gulp.dest('dist')) 59 | .pipe($.size()); 60 | }); 61 | 62 | gulp.task('images', function() { 63 | return gulp.src('app/images/**/*') 64 | .pipe($.cache($.imagemin({ 65 | optimizationLevel: 3, 66 | progressive: true, 67 | interlaced: true 68 | }))) 69 | .pipe(gulp.dest('dist/images')) 70 | .pipe($.size()); 71 | }); 72 | 73 | gulp.task('fonts', function() { 74 | return gulp.src(['app/fonts/**/*']) 75 | .pipe(gulp.dest('dist/fonts')) 76 | .pipe($.size()); 77 | }); 78 | 79 | gulp.task('extras', function () { 80 | return gulp.src(['app/*.txt', 'app/*.ico']) 81 | .pipe(gulp.dest('dist/')) 82 | .pipe($.size()); 83 | }); 84 | 85 | gulp.task('serve', function() { 86 | gulp.src('dist') 87 | .pipe($.webserver({ 88 | livereload: true, 89 | port: 9000 90 | })); 91 | }); 92 | 93 | gulp.task('set-production', function() { 94 | process.env.NODE_ENV = 'production'; 95 | }); 96 | 97 | gulp.task('minify:js', function() { 98 | return gulp.src('dist/scripts/**/*.js') 99 | .pipe($.uglify()) 100 | .pipe(gulp.dest('dist/scripts/')) 101 | .pipe($.size()); 102 | }); 103 | 104 | gulp.task('minify:css', function() { 105 | return gulp.src('dist/styles/**/*.css') 106 | .pipe($.minifyCss()) 107 | .pipe(gulp.dest('dist/styles')) 108 | .pipe($.size()); 109 | }); 110 | 111 | gulp.task('minify', ['minify:js', 'minify:css']); 112 | 113 | gulp.task('clean', del.bind(null, 'dist')); 114 | 115 | gulp.task('bundle', ['html', 'styles', 'scripts', 'images', 'fonts', 'extras']); 116 | 117 | gulp.task('clean-bundle', sync(['clean', 'bundle'])); 118 | 119 | gulp.task('build', ['clean-bundle'], bundler.stop.bind(bundler)); 120 | 121 | gulp.task('build:production', sync(['set-production', 'build', 'minify'])); 122 | 123 | gulp.task('serve:production', sync(['build:production', 'serve'])); 124 | 125 | gulp.task('default', ['build']); 126 | 127 | gulp.task('watch', sync(['clean-bundle', 'serve']), function() { 128 | bundler.watch(); 129 | gulp.watch('app/*.html', ['html']); 130 | gulp.watch('app/styles/**/*.scss', ['styles']); 131 | gulp.watch('app/images/**/*', ['images']); 132 | gulp.watch('app/fonts/**/*', ['fonts']); 133 | }); 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxflux", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "lodash": "^4.13.1", 6 | "react": "^15.1.0", 7 | "rxjs": "^5.0.0-beta.9" 8 | }, 9 | "devDependencies": { 10 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 11 | "babelify": "^6.0.2", 12 | "browserify": "^8.1.3", 13 | "del": "^1.1.1", 14 | "gulp": "^3.8.10", 15 | "gulp-autoprefixer": "^2.1.0", 16 | "gulp-bower": "^0.0.10", 17 | "gulp-cache": "^0.2.4", 18 | "gulp-imagemin": "^2.1.0", 19 | "gulp-load-plugins": "^0.8.0", 20 | "gulp-minify-css": "^0.4.5", 21 | "gulp-ruby-sass": "^1.0.0-alpha", 22 | "gulp-size": "^1.2.0", 23 | "gulp-sync": "^0.1.4", 24 | "gulp-uglify": "^1.1.0", 25 | "gulp-useref": "^1.1.1", 26 | "gulp-util": "^3.0.3", 27 | "gulp-webserver": "^0.9.0", 28 | "vinyl-source-stream": "^1.0.0", 29 | "watchify": "^2.3.0" 30 | }, 31 | "scripts": { 32 | "start": "gulp watch" 33 | }, 34 | "engines": { 35 | "node": ">=5.11.1" 36 | }, 37 | "browserify": { 38 | "transform": [ 39 | "babelify" 40 | ], 41 | "plugins": ["transform-object-rest-spread"] 42 | } 43 | } 44 | --------------------------------------------------------------------------------