├── .gitignore ├── LICENSE.txt ├── README.org ├── ROADMAP.org ├── benchmark ├── redux-immutable.js ├── redux.js ├── timer.js └── transdux-clj.js ├── ci ├── github_page.sh └── npm-login.sh ├── circle.yml ├── docs └── api.org ├── examples └── todomvc │ ├── bin │ └── compiler.jar │ ├── package.json │ ├── public │ └── index.html │ └── src │ ├── app.jsx │ └── components │ ├── Footer.jsx │ ├── Header.jsx │ ├── MainSection.action.js │ ├── MainSection.jsx │ ├── TodoItem.jsx │ ├── TodoTextInput.jsx │ └── __tests__ │ └── TodoItem-spec.jsx ├── lib ├── __tests__ │ └── transdux-test.jsx └── transdux.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | ##########idea 9 | .idea/ 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 32 | node_modules 33 | 34 | examples/todomvc/public/* 35 | !examples/todomvc/public/index.html 36 | 37 | 38 | transdux.js 39 | public -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 Jichao Ouyang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Transdux 2 | #+BEGIN_QUOTE 3 | For better Performance, please Try most.js version [[https://github.com/jcouyang/mostux][mostux]] 4 | #+END_QUOTE 5 | 6 | I'm trying to documenting in more detail, but if you have any question, feel free to 7 | #+ATTR_HTML: title="Join the chat at https://gitter.im/jcouyang/transdux" 8 | [[https://gitter.im/jcouyang/transdux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][file:https://badges.gitter.im/Join%20Chat.svg]] 9 | 10 | Circle CI [[https://circleci.com/gh/jcouyang/transdux][https://circleci.com/gh/jcouyang/transdux.svg?style=svg]] 11 | 12 | [[http://blog.oyanglul.us/javascript/react-transdux-the-clojure-approach-of-flux.html][>中文<]] 13 | 14 | #+BEGIN_QUOTE 15 | Finnally a flux like framework don't even need a tutorial, our [[./examples/todomvc][TodoMVC]] will tell you all. 16 | #+END_QUOTE 17 | 18 | Managing React Component state in *Elegant & Functional* way with transducers and channels from ClojureScript 19 | 20 | ** Rationale 21 | flux and redux are great and solve state management and communication pretty well. 22 | 23 | *But* they are too complicated, users have to know toomany things about store, dispatcher, actions which they *shouldn't* 24 | 25 | In reality what we have to do is actually just need to *talk* to *whatever* component I like and get my state from one *source of truth*, so the simplest way to do this is: 26 | 27 | For component who has *actions*, only thing it have to define is what can it do. 28 | 29 | For component who want to call other component's action, it directly *dispatch* a message to that component. 30 | 31 | SO, all user have to know is to 32 | - define *actions* for your component 33 | - *dispatch* messages to *any component* you want to talk to 34 | 35 | and leave all the other *dirty works* (stores, states) to our functional transducers, channels and pubsub that user don't really need to care about. 36 | 37 | *The Big Picture* 38 | [[https://www.evernote.com/l/ABe_8eE6o2dGlZMCmNnBap_fXy83GvJe6gcB/image.jpg]] 39 | 40 | We're using Channels/Actors Model from Clojure -- core.async, It's compile to JS by [[http://github.com/jcouyang/conjs][conjs]] 41 | 42 | *Basic Idea* 43 | we composed channels, subs, pubs, and transducers together like tunnels, every message goes through the tunnel will got process by the transducer with action function user provided. 44 | 45 | so, whenever a message is dispatch to input channel, you'll get new state from the corresponding output channel. you don't need to care about how the dispatching really happen in the framework. 46 | 47 | ** Install 48 | In your React project 49 | #+BEGIN_SRC sh 50 | npm install transdux --save 51 | #+END_SRC 52 | 53 | ** Usage 54 | to wire in transdux, only 4 place you have to pay attention to. 55 | *** 1. wrap you app with Transdux 56 | #+BEGIN_SRC html 57 | 58 | 59 | 60 | #+END_SRC 61 | *** 2. define what your component can do 62 | #+BEGIN_SRC js 63 | // MainSection.jsx 64 | let actions = { 65 | complete(msg, state){ 66 | return { 67 | todos:state.todos.map(todo=>{ 68 | if(todo.id==msg.id) 69 | todo.completed = !todo.completed 70 | return todo 71 | }) 72 | } 73 | }, 74 | clear(msg,state){ 75 | return { 76 | todos: state.todos.filter(todo=>todo.completed==false) 77 | } 78 | } 79 | } 80 | #+END_SRC 81 | *** for Mixin lover 82 | **** 3. mixin Transdux Mixin and Bind Actions 83 | #+BEGIN_SRC js 84 | // MainSection.jsx 85 | import {TxMixin} from 'transdux' 86 | let MainSection = React.createClass({ 87 | mixins: [TxMixin], 88 | componentDidMount(){ 89 | this.bindActions(actions) 90 | }, 91 | ... 92 | }) 93 | 94 | #+END_SRC 95 | 96 | **** 4. mixin and dispatch a message 97 | #+BEGIN_SRC jsx 98 | //TodoItem.jsx 99 | import MainSection from './MainSection' 100 | let TodoItem = React.createClass({ 101 | mixins: [TxMixin], 102 | render(){ 103 | this.dispatch(MainSection, 'complete',{id:todo.id})} /> 107 | 108 | } 109 | }) 110 | #+END_SRC 111 | 112 | *** for ES6 class lover 113 | **** 3. mixin transdux into Class 114 | #+BEGIN_SRC js 115 | // TodoItem.jsx 116 | import {mixin} from 'transdux' 117 | let actions = { 118 | ... 119 | } 120 | class TodoItem extends React.Component { 121 | constructor(props){ 122 | super(props); 123 | this.state = {editing:false}; 124 | } 125 | ... 126 | } 127 | export default mixin(TodoItem, actions) 128 | 129 | #+END_SRC 130 | 131 | **** 4. dispatch a message 132 | #+BEGIN_SRC jsx 133 | //TodoItem.jsx 134 | import MainSection from './MainSection' 135 | class TodoItem extends React.Component { 136 | ... 137 | render(){ 138 | this.dispatch(MainSection, 'complete',{id:todo.id})} /> 142 | 143 | } 144 | ... 145 | }) 146 | export default mixin(TodoItem) 147 | #+END_SRC 148 | ** Examples 149 | - [[http://oyanglul.us/transdux/todomvc/][todomvc]] 150 | - source: [[./examples]] 151 | 152 | ** API 153 | [[./docs/api.org]] 154 | 155 | ** Performance 156 | for dispatching *1023 messages* at the same time, here is the Memory Usage and Time elapsed 157 | 158 | tested on /Macbook Pro 13, CPU 2.9GHz Intel Core i5, Mem 16GB 1867MHz DDR3/ 159 | 160 | *** transdux 161 | #+BEGIN_EXAMPLE 162 | Memory Usage Before: { rss: 43307008, heapTotal: 18550784, heapUsed: 11889192 } 163 | Memory Usage After: { rss: 46444544, heapTotal: 30921984, heapUsed: 15307800 } 164 | Elapsed 51ms 165 | #+END_EXAMPLE 166 | 167 | *** setTimeout 168 | #+BEGIN_EXAMPLE 169 | Memory Usage Before: { rss: 45432832, heapTotal: 17518848, heapUsed: 12664416 } 170 | Memory Usage After: { rss: 46772224, heapTotal: 19570688, heapUsed: 10927824 } 171 | Elapsed 7ms 172 | #+END_EXAMPLE 173 | 174 | *** redux 175 | #+BEGIN_EXAMPLE 176 | Memory Usage Before: { rss: 21647360, heapTotal: 9275392, heapUsed: 4559616 } 177 | Memory Usage After: { rss: 22638592, heapTotal: 9275392, heapUsed: 5472112 } 178 | Elapsed 4ms 179 | #+END_EXAMPLE 180 | 181 | Yeah, I know, it's slower then redux, and I'm working on it. 182 | But, it's not bad, it's totally reasonable trade-off a little performance to get writing code which is more composable, reusable, testable and easy to reason about. 183 | 184 | ** TODOS 185 | [[./ROADMAP.org]] 186 | -------------------------------------------------------------------------------- /ROADMAP.org: -------------------------------------------------------------------------------- 1 | * RoadMap [33%] 2 | 3 | - [X] using Atom managing state 4 | - [-] smoke testing 5 | - [X] benchmark 6 | - [ ] smoke 7 | - [ ] unsubscribe when component unmount 8 | - [ ] improve performance 9 | 10 | -------------------------------------------------------------------------------- /benchmark/redux-immutable.js: -------------------------------------------------------------------------------- 1 | var createStore = require('redux').createStore 2 | var timer = require('./timer') 3 | var time = timer.time 4 | var CYCLE = timer.CYCLE 5 | var immutable = require('immutable') 6 | var initState = [0] 7 | for(var i=0;i<1000;i++) 8 | initState.push(i) 9 | 10 | function counter(state, action) { 11 | state=immutable.fromJS(state||initState) 12 | switch (action.type) { 13 | case 'INCREMENT': 14 | return state.map(function(item){return item+1}).toJS() 15 | } 16 | } 17 | 18 | var store = createStore(counter) 19 | 20 | time(function(done){ 21 | store.subscribe(() => { 22 | done(store.getState()[0]) 23 | }) 24 | for(var i=0;i { 18 | done(store.getState()) 19 | }) 20 | for(var i=0;i this.dispatch(MainSection, 'complete',{id:todo.id})} /> 35 | 36 | } 37 | ... 38 | } 39 | #+END_SRC 40 | 41 | #+BEGIN_QUOTE 42 | One thing you may need to notice is that message between component is *Class* level, not *Instance* level. For example, if you have multiple =MainSection= components, they will all reveive the same massage from =TodoItem=. 43 | #+END_QUOTE 44 | 45 | ** actions 46 | action is no more than just a normal javascript object, which contains all the action functions. 47 | 48 | but you have to pay a little attention to make all function in it. All functions should return object, which normally just like what you wanna put in the parameter of function =this.setState=. 49 | 50 | ** bindActions 51 | #+BEGIN_QUOTE 52 | for react mixin 53 | #+END_QUOTE 54 | 55 | =bindActions(actions:object, JsToImmutableFunction:function, ImmutableToJsFunction:function)= 56 | 57 | =JsToImmutableFunction= and =ImmutableToJsFunction= are *optional* 58 | 59 | example: 60 | #+BEGIN_SRC js 61 | import {TxMixin} from 'transdux' 62 | let MainSection = React.createClass({ 63 | mixins: [TxMixin], 64 | componentDidMount(){ 65 | this.bindActions(actions) 66 | }, 67 | ... 68 | }) 69 | #+END_SRC 70 | 71 | for mixin, you have to maually call =bindActions= some where, it's recommended to put it in =componentDidMount()= where not blocking your component's render. 72 | -------------------------------------------------------------------------------- /examples/todomvc/bin/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactive-react/transdux/d7f4014ae722c74f97b072ebffb3d48bba71bbff/examples/todomvc/bin/compiler.jar -------------------------------------------------------------------------------- /examples/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transdux-todomvc", 3 | "version": "0.0.1", 4 | "description": "", 5 | "browserify": { 6 | "transform": [ 7 | [ 8 | "babelify", 9 | { 10 | "extensions": [ 11 | ".jsx", 12 | ".js" 13 | ] 14 | } 15 | ] 16 | ] 17 | }, 18 | "scripts": { 19 | "build": "NODE_ENV=production browserify src/app.jsx --extension=.jsx | java -jar bin/compiler.jar > public/app.js", 20 | "start": "ecstatic -p 8000 public", 21 | "watch": "watchify -d src/app.jsx --extension=.jsx -o public/app.js -dv", 22 | "test": "jest" 23 | }, 24 | "jest": { 25 | "scriptPreprocessor": "/node_modules/babel-jest", 26 | "testFileExtensions": [ 27 | "es6", 28 | "js", 29 | "jsx" 30 | ], 31 | "moduleFileExtensions": [ 32 | "js", 33 | "json", 34 | "es6", 35 | "jsx" 36 | ] 37 | }, 38 | "dependencies": { 39 | "classnames": "^2.2.0", 40 | "transdux": "^0.1.0", 41 | "react": "^0.14.2", 42 | "react-dom": "^0.14.2" 43 | }, 44 | "devDependencies": { 45 | "babel": "^6.1.18", 46 | "babel-plugin-transform-react-jsx": "^6.1.18", 47 | "babel-preset-es2015": "^6.1.18", 48 | "babel-jest": "^6.0.0", 49 | "jest-cli": "^0.7.0", 50 | "babelify": "^7.2.0", 51 | "browserify": "^12.0.1", 52 | "ecstatic": "^1.3.1", 53 | "uglify-js": "^2.6.1", 54 | "watchify": "^3.6.1" 55 | }, 56 | "author": "Jichao Ouyang", 57 | "license": "ISC", 58 | "babel": { 59 | "presets": [ 60 | "es2015" 61 | ], 62 | "plugins": [ 63 | "transform-react-jsx" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/todomvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Template • TodoMVC 7 | 8 | 9 | 10 | 11 |
12 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/todomvc/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Header from './components/Header'; 3 | import MainSection from './components/MainSection'; 4 | import {render} from 'react-dom'; 5 | import Transdux from 'transdux' 6 | 7 | class App extends Component { 8 | render(){ 9 | return ( 10 |
11 |
12 | 13 |
14 | ) 15 | } 16 | } 17 | 18 | render( 19 | 20 | 21 | 22 | , document.getElementById('app')); 23 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | import MainSection from './MainSection' 4 | import {TxMixin} from 'transdux' 5 | const FILTER_TITLES = { 6 | 'SHOW_ALL': 'All', 7 | 'SHOW_ACTIVE': 'Active', 8 | 'SHOW_COMPLETED': 'Completed' 9 | } 10 | 11 | let Footer = React.createClass({ 12 | mixins: [TxMixin], 13 | renderTodoCount() { 14 | const { activeCount } = this.props 15 | const itemWord = activeCount === 1 ? 'item' : 'items' 16 | 17 | return ( 18 | 19 | {activeCount || 'No'} {itemWord} left 20 | 21 | ) 22 | }, 23 | 24 | renderFilterLink(filter) { 25 | const title = FILTER_TITLES[filter] 26 | const { filter: selectedFilter, onShow } = this.props 27 | 28 | return ( 29 | this.dispatch(MainSection, 'show', filter)}> 32 | {title} 33 | 34 | ) 35 | }, 36 | 37 | renderClearButton() { 38 | const { completedCount } = this.props 39 | if (completedCount > 0) { 40 | return ( 41 | 45 | ) 46 | } 47 | }, 48 | 49 | render() { 50 | return ( 51 |
52 | {this.renderTodoCount()} 53 |
    54 | {['SHOW_ALL', 'SHOW_ACTIVE', 'SHOW_COMPLETED' ].map(filter => 55 |
  • 56 | {this.renderFilterLink(filter)} 57 |
  • 58 | )} 59 |
60 | {this.renderClearButton()} 61 |
62 | ) 63 | }, 64 | }); 65 | 66 | 67 | export default Footer 68 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | let Header = React.createClass({ 4 | render() { 5 | return ( 6 |
7 |

todos

8 | 10 |
11 | ) 12 | }, 13 | }) 14 | 15 | export default Header 16 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/MainSection.action.js: -------------------------------------------------------------------------------- 1 | const actions = { 2 | complete(msg, state){ 3 | return { 4 | todos:state.todos.map(todo=>{ 5 | if(todo.id==msg.id) 6 | todo.completed = !todo.completed 7 | return todo 8 | }) 9 | } 10 | }, 11 | show(msg,state){ 12 | switch(msg){ 13 | case('SHOW_ALL'): 14 | return {filter: _=>_} 15 | case('SHOW_ACTIVE'): 16 | return {filter: todos=>todos.filter(todo=>!todo.completed)} 17 | case('SHOW_COMPLETED'): 18 | return {filter: todos=>todos.filter(todo=>todo.completed)} 19 | } 20 | }, 21 | clear(msg,state){ 22 | return { 23 | todos: state.todos.filter(todo=>todo.completed==false) 24 | } 25 | }, 26 | add(msg, state){ 27 | let todos = state.todos 28 | todos.unshift({id:todos.length+1, text:msg, completed:false}) 29 | return { 30 | todos: todos 31 | } 32 | }, 33 | edit(msg, state){ 34 | return { 35 | todos: state.todos.map(todo=>{ 36 | if(todo.id == msg.id){todo.text=msg.text} 37 | return todo; 38 | }) 39 | } 40 | }, 41 | delete(msg, state){ 42 | return { 43 | todos: state.todos.filter(todo=>{ 44 | return todo.id!=msg.id 45 | }) 46 | } 47 | } 48 | } 49 | export default actions 50 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/MainSection.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import TodoItem from './TodoItem' 3 | import Footer from './Footer' 4 | import {TxMixin} from 'transdux' 5 | import actions from './MainSection.action' 6 | const todos = [{ 7 | text: 'Dont Use Redux', 8 | completed: false, 9 | id: 0 10 | },{ 11 | text: 'Use transdux', 12 | completed: false, 13 | id: 1 14 | }]; 15 | 16 | let MainSection = React.createClass({ 17 | mixins: [TxMixin], 18 | getInitialState(){ 19 | return { 20 | todos: todos, 21 | filter: _=>_ 22 | } 23 | }, 24 | componentDidMount(){ 25 | this.bindActions(actions) 26 | }, 27 | 28 | handleShow(filter) { 29 | this.setState({ filter }) 30 | }, 31 | 32 | renderToggleAll(completedCount) { 33 | const { todos } = this.state 34 | if (todos.length > 0) { 35 | return ( 36 | 40 | ) 41 | } 42 | }, 43 | 44 | renderFooter(completedCount) { 45 | const { todos } = this.state 46 | const activeCount = todos.length - completedCount; 47 | 48 | if (todos.length) { 49 | return ( 50 |