Skip to content

Commit

Permalink
init commit: replace transducer with most
Browse files Browse the repository at this point in the history
  • Loading branch information
jcouyang committed Dec 26, 2015
0 parents commit 499de2a
Show file tree
Hide file tree
Showing 26 changed files with 1,052 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
@@ -0,0 +1,13 @@
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
39 changes: 39 additions & 0 deletions .gitignore
@@ -0,0 +1,39 @@
# Created by https://www.gitignore.io/api/node

### Node ###
# Logs
logs
*.log
npm-debug.log*
##########idea
.idea/
# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules

examples/todomvc/public/*
!examples/todomvc/public/index.html


transdux.js
public
23 changes: 23 additions & 0 deletions LICENSE.txt
@@ -0,0 +1,23 @@
/**
The MIT License (MIT)

Copyright (c) 2015 Jichao Ouyang

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
182 changes: 182 additions & 0 deletions README.org
@@ -0,0 +1,182 @@
* Transdux

I'm trying to documenting in more detail, but if you have any question, feel free to
#+ATTR_HTML: title="Join the chat at https://gitter.im/jcouyang/transdux"
[[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]]

Circle CI [[https://circleci.com/gh/jcouyang/transdux][https://circleci.com/gh/jcouyang/transdux.svg?style=svg]]

[[http://blog.oyanglul.us/javascript/react-transdux-the-clojure-approach-of-flux.html][>中文<]]

#+BEGIN_QUOTE
Finnally a flux like framework don't even need a tutorial, our [[./examples/todomvc][TodoMVC]] will tell you all.
#+END_QUOTE

Managing React Component state in *Elegant & Functional* way with transducers and channels from ClojureScript

** Rationale
flux and redux are great and solve state management and communication pretty well.

*But* they are too complicated, users have to know toomany things about store, dispatcher, actions which they *shouldn't*

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:

For component who has *actions*, only thing it have to define is what can it do.

For component who want to call other component's action, it directly *dispatch* a message to that component.

SO, all user have to know is to
- define *actions* for your component
- *dispatch* messages to *any component* you want to talk to

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.

*The Big Picture*
[[https://www.evernote.com/l/ABe_8eE6o2dGlZMCmNnBap_fXy83GvJe6gcB/image.jpg]]

We're using Channels/Actors Model from Clojure -- core.async, It's compile to JS by [[http://github.com/jcouyang/conjs][conjs]]

*Basic Idea*
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.

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.

** Install
In your React project
#+BEGIN_SRC sh
npm install transdux --save
#+END_SRC

** Usage
to wire in transdux, only 4 place you have to pay attention to.
*** 1. wrap you app with Transdux
#+BEGIN_SRC html
<Transdux>
<App/>
</Transdux>
#+END_SRC
*** 2. define what your component can do
#+BEGIN_SRC js
// MainSection.jsx
let actions = {
complete(msg, state){
return {
todos:state.todos.map(todo=>{
if(todo.id==msg.id)
todo.completed = !todo.completed
return todo
})
}
},
clear(msg,state){
return {
todos: state.todos.filter(todo=>todo.completed==false)
}
}
}
#+END_SRC
*** for Mixin lover
**** 3. mixin Transdux Mixin and Bind Actions
#+BEGIN_SRC js
// MainSection.jsx
import {TxMixin} from 'transdux'
let MainSection = React.createClass({
mixins: [TxMixin],
componentDidMount(){
this.bindActions(actions)
},
...
})

#+END_SRC

**** 4. mixin and dispatch a message
#+BEGIN_SRC jsx
//TodoItem.jsx
import MainSection from './MainSection'
let TodoItem = React.createClass({
mixins: [TxMixin],
render(){
<input className="toggle"
type="checkbox"
checked={todo.completed}
onChange={() => this.dispatch(MainSection, 'complete',{id:todo.id})} />

}
})
#+END_SRC

*** for ES6 class lover
**** 3. mixin transdux into Class
#+BEGIN_SRC js
// TodoItem.jsx
import {mixin} from 'transdux'
let actions = {
...
}
class TodoItem extends React.Component {
constructor(props){
super(props);
this.state = {editing:false};
}
...
}
export default mixin(TodoItem, actions)

#+END_SRC

**** 4. dispatch a message
#+BEGIN_SRC jsx
//TodoItem.jsx
import MainSection from './MainSection'
class TodoItem extends React.Component {
...
render(){
<input className="toggle"
type="checkbox"
checked={todo.completed}
onChange={() => this.dispatch(MainSection, 'complete',{id:todo.id})} />

}
...
})
export default mixin(TodoItem)
#+END_SRC
** Examples
- [[http://oyanglul.us/transdux/todomvc/][todomvc]]
- source: [[./examples]]

** API
[[./docs/api.org]]

** Performance
for dispatching *1023 messages* at the same time, here is the Memory Usage and Time elapsed

tested on /Macbook Pro 13, CPU 2.9GHz Intel Core i5, Mem 16GB 1867MHz DDR3/

*** transdux
#+BEGIN_EXAMPLE
Memory Usage Before: { rss: 43307008, heapTotal: 18550784, heapUsed: 11889192 }
Memory Usage After: { rss: 46444544, heapTotal: 30921984, heapUsed: 15307800 }
Elapsed 51ms
#+END_EXAMPLE

*** setTimeout
#+BEGIN_EXAMPLE
Memory Usage Before: { rss: 45432832, heapTotal: 17518848, heapUsed: 12664416 }
Memory Usage After: { rss: 46772224, heapTotal: 19570688, heapUsed: 10927824 }
Elapsed 7ms
#+END_EXAMPLE

*** redux
#+BEGIN_EXAMPLE
Memory Usage Before: { rss: 21647360, heapTotal: 9275392, heapUsed: 4559616 }
Memory Usage After: { rss: 22638592, heapTotal: 9275392, heapUsed: 5472112 }
Elapsed 4ms
#+END_EXAMPLE

Yeah, I know, it's slower then redux, and I'm working on it.
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.

** TODOS
[[./ROADMAP.org]]
9 changes: 9 additions & 0 deletions ROADMAP.org
@@ -0,0 +1,9 @@
* RoadMap [33%]

- [X] using Atom managing state
- [-] smoke testing
- [X] benchmark
- [ ] smoke
- [ ] unsubscribe when component unmount
- [ ] improve performance

32 changes: 32 additions & 0 deletions benchmark/redux-immutable.js
@@ -0,0 +1,32 @@
var createStore = require('redux').createStore
var timer = require('./timer')
var time = timer.time
var CYCLE = timer.CYCLE
var immutable = require('immutable')
var initState = [0]
for(var i=0;i<1000;i++)
initState.push(i)

function counter(state, action) {
state=immutable.fromJS(state||initState)
switch (action.type) {
case 'INCREMENT':
return state.map(function(item){return item+1}).toJS()
}
}

var store = createStore(counter)

time(function(done){
store.subscribe(() => {
done(store.getState()[0])
})
for(var i=0;i<CYCLE+1;i++){
store.dispatch({ type: 'INCREMENT' });
}
})
/**
Memory Usage Before: { rss: 21942272, heapTotal: 9275392, heapUsed: 4559784 }
Memory Usage After: { rss: 22929408, heapTotal: 9275392, heapUsed: 5473240 }
Elapsed 4ms
*/
28 changes: 28 additions & 0 deletions benchmark/redux.js
@@ -0,0 +1,28 @@
var createStore = require('redux').createStore
var timer = require('./timer')
var time = timer.time
var CYCLE = timer.CYCLE

function counter(state, action) {
state=state||0
switch (action.type) {
case 'INCREMENT':
return state + 1
}
}

var store = createStore(counter)

time(function(done){
store.subscribe(() => {
done(store.getState())
})
for(var i=0;i<CYCLE+1;i++){
store.dispatch({ type: 'INCREMENT' });
}
})
/**
Memory Usage Before: { rss: 21942272, heapTotal: 9275392, heapUsed: 4559784 }
Memory Usage After: { rss: 22929408, heapTotal: 9275392, heapUsed: 5473240 }
Elapsed 4ms
*/
13 changes: 13 additions & 0 deletions benchmark/timer.js
@@ -0,0 +1,13 @@
var CYCLE = 1023
exports.time = function(f) {
console.log('Memory Usage Before:', process.memoryUsage())
var s = new Date();
f(function(index){
if(index==CYCLE){
console.log('Memory Usage After:', process.memoryUsage())
console.log("Elapsed "+((new Date()).valueOf()-s.valueOf())+"ms");
}

});
}
exports.CYCLE = CYCLE
43 changes: 43 additions & 0 deletions benchmark/transdux-clj.js
@@ -0,0 +1,43 @@
var conjs = require('con.js');
var tx = require('../transdux.js')
var txmixin = tx.TxMixin
var timer = require('./timer')
var time = timer.time
var CYCLE = timer.CYCLE
var toJs = conjs.toJs
var toClj = conjs.toClj
var inputChan = conjs.async.chan()
var outputChan = conjs.async.chan()
var context = {
transduxChannel: inputChan,
transduxPublication: conjs.async.pub(inputChan, function(_){return _['action']}),
}

var initState = [0]
for(var i=0;i<1000;i++)
initState.push(i)

time(function(done){
function Target(){
return {
getInitialState: function(){return initState},
context:context,
state: [0],
setState: function(f){
this.state=f(this.state)
done(this.state[0])
},
constructor: Target
}
}
var target = new Target()
txmixin.bindActions.call(target, {
increment: function(msg,state){
return conjs.map(function(m){return msg+1}, state)
}
}, toClj, toJs)

for(var i=0;i<CYCLE+1;i++){
txmixin.dispatch.call(target, Target, 'increment', i)
}
})

0 comments on commit 499de2a

Please sign in to comment.