├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .gitmodules ├── .npmignore ├── README.md ├── browser.config.js ├── build └── index.js ├── dist ├── fluxette.js └── fluxette.min.js ├── gulpfile.js ├── index.js ├── lib └── index.js ├── npm.config.js ├── package.json ├── src ├── index.js └── util.js ├── test.config.js ├── test ├── dispatch.js ├── hooks.js ├── index.js ├── middleware.js ├── react.js ├── rehydration.js ├── state.js └── store.js └── testem.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/rules 2 | { 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "plugins": [ 9 | "react" 10 | ], 11 | "rules": { 12 | "strict": 0, 13 | "indent": [2, 0], 14 | "quotes": [2, "single"], 15 | "no-underscore-dangle": 0, 16 | "no-unused-vars": 1, 17 | "no-unused-expressions": 0, 18 | "react/jsx-no-undef": 2, 19 | "new-cap": 0, 20 | "no-shadow": 0, 21 | "no-use-before-define": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/fluxette-todo"] 2 | path = examples/fluxette-todo 3 | url = https://github.com/edge/fluxette-todo 4 | [submodule "examples/flux-comparison"] 5 | path = examples/flux-comparison 6 | url = https://github.com/edge/flux-comparison 7 | [submodule "examples/fluxette-react-router"] 8 | path = examples/fluxette-react-router 9 | url = https://github.com/edge/fluxette-react-router 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | examples 4 | src 5 | test 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fluxette 2 | 3 | `fluxette` is a minimalist yet powerful Flux implementation, inspired by ideas from [Dan Abramov (@gaearon)'s talk on Redux](https://www.youtube.com/watch?v=xsSnOQynTHs). 4 | 5 | ## Table of Contents 6 | 7 | * [Why?](#why) 8 | * [Install](#install) 9 | * [Getting Started](#getting-started) 10 | * [API](#api) 11 | * [Glossary](#glossary) 12 | * [The Law of Functional Flux](#the-law-of-functional-flux) 13 | * [Middleware](#middleware) 14 | * [Rehydration](#rehydration) 15 | * [Debugging](#debugging) 16 | * [SSR](#ssr) 17 | * [Asynchronous](#asynchronous) 18 | * [Optimistic Updates](#optimistic-updates) 19 | * [Store Dependencies](#store-dependencies) 20 | * [Store Composition](#store-composition) 21 | * [Examples](#examples) 22 | * [Testing](#testing) 23 | * [Todo](#todo) 24 | * [Influences](#Influences) 25 | 26 | ## Why? 27 | 28 | Why fluxette? (We used to have a list of buzzwords here.) 29 | 30 | "fluxette" means "little flux". That's exactly what it is, coming in at a tiny 1.4 kB, minified (even smaller gzipped!). It is a library that combines the advantages of orthogonal code with the robust design of Facebook Flux. Through simple rules, it allows for the creation and exploitation of very advanced design patterns. It relies only on the basic convention that your stores are pure functions in the form of `(State, Action) => State` (see [The Law of Functional Flux](#the-law-of-functional-flux)). As a result, its dispatcher exists as [one simple line](https://github.com/edge/fluxette/blob/master/src/index.js#L10). fluxette makes migrating from other implementations in the family of functional Flux very easy (e.g. copy-paste reducers/listeners from redux). 31 | 32 | fluxette abstracts away many of the headaches that you may have had with React and other Flux implementations, such as Store dependencies (`waitFor`), superfluous `setState`s ("did the state really change?" and "I'm not done updating yet!"), listening to finer and coarser-grained updates, [`Uncaught Error: Invariant Violation: Dispatch.dispatch(...)`](http://i.imgur.com/YnI9TIJ.jpg), and more. 33 | 34 | ## Install 35 | 36 | ```sh 37 | npm install --save fluxette 38 | ``` 39 | [Browser builds (umd) are also available.](https://github.com/edge/fluxette/tree/master/dist) 40 | 41 | You'll probably want [`reducer`](https://github.com/edge/reducer) as well for common reducer compositions. 42 | ```sh 43 | npm install --save reducer 44 | ``` 45 | 46 | If you're working with React, you should grab the [React bindings](https://github.com/edge/fluxette-react). 47 | ```sh 48 | npm install --save fluxette-react 49 | ``` 50 | 51 | ## Getting Started 52 | 53 | Say you have a simple React component that you want to refactor with Flux: 54 | 55 | ```js 56 | class Updater extends React.Component { 57 | constructor() { 58 | super(); 59 | this.state = { 60 | text: '' 61 | }; 62 | } 63 | change(e) { 64 | this.setState({ text: e.target.value }); 65 | } 66 | render() { 67 | return ( 68 |
69 | 70 |
{ this.state.text }
71 |
72 | ); 73 | } 74 | } 75 | 76 | React.render(, document.getElementById('root')); 77 | ``` 78 | 79 | This is a simple component that shows you the text that you've typed into a textbox right below it. We can interpret our event as an action of type `UPDATE` that carries the value of the textbox. 80 | 81 | ```js 82 | // constants 83 | const UPDATE = 'UPDATE'; 84 | 85 | // actions 86 | const update = value => ({ type: UPDATE, value }); 87 | ``` 88 | 89 | We'll also need a Reducer to manage our state. 90 | 91 | ```js 92 | import Flux from 'fluxette'; 93 | import Leaf from 'reducer/leaf'; 94 | 95 | // leaf reducer 96 | const updater = Leaf('', { 97 | [UPDATE]: (state, action) => action.value 98 | }); 99 | 100 | // flux interface 101 | const flux = Flux(updater); 102 | ``` 103 | 104 | `Leaf` creates a pure function that looks at your actions and uses them to determine how to operate on the state. Our Reducer's default value is an empty string, just like in our `Updater` component. It listens to any actions of the type `UPDATE`, and uses the value that the action carries as our new state. 105 | 106 | We also create a Flux object, which we can now integrate into our component. 107 | 108 | **Putting it all together** 109 | 110 | Here we import two things from the React bindings: the `@connect` decorator and the `Context` component. `Context` provides flux on `this.context` to all of its children, which `@connect` then utilizes to manage listeners on your component (also attaches `dispatch` to the component, for convenience). 111 | 112 | ```js 113 | import React from 'react'; 114 | import Flux from 'fluxette'; 115 | import Leaf from 'reducer/leaf'; 116 | import { Context, connect } from 'fluxette-react' 117 | 118 | // constants 119 | const UPDATE = 'UPDATE'; 120 | 121 | // actions 122 | const update = value => ({ type: UPDATE, value }); 123 | 124 | // reducer 125 | const updater = Leaf('', { 126 | [UPDATE]: (state, action) => action.value 127 | }); 128 | 129 | // flux interface 130 | const flux = Flux(updater); 131 | 132 | @connect(state => ({ text: state })) 133 | class Updater extends React.Component { 134 | change(e) { 135 | this.dispatch(update(e.target.value)); 136 | } 137 | render() { 138 | return ( 139 |
140 | 141 |
{ this.state.text }
142 |
143 | ); 144 | } 145 | } 146 | 147 | React.render( 148 | 149 | { () => } 150 | , 151 | document.getElementById('root') 152 | ); 153 | ``` 154 | 155 | ## API 156 | 157 | **Flux(reducer)** 158 | 159 | Creates your central Flux object that manages the dispatcher and state. Its methods can be called anonymously. 160 | 161 | ```js 162 | import Flux from 'fluxette'; 163 | import reducers from './reducers'; 164 | 165 | const flux = Flux(reducers); 166 | ``` 167 | 168 | **flux.dispatch(actions, [update = true])** 169 | 170 | Processes `actions` and calls all listeners if `update` is true. `actions` can be any Object, or array of Objects, which can be nested. If you use middleware, that includes Functions, Promises, and others. You can call `dispatch` without any arguments to call all listeners, if you streamed or buffered updates to the dispatcher. It also returns the array of actions that were processed by the dispatch pipeline, making techniques like Promise chaining easy. 171 | 172 | ```js 173 | flux.dispatch({ type: 'MY_ACTION_TYPE', data: 'x' }); 174 | 175 | // thunks 176 | import thunk from 'fluxette-thunk'; 177 | flux = flux.using(thunk); 178 | flux.dispatch(({ dispatch }) => { 179 | // useful if this function came from somewhere else 180 | dispatch({ type: 'MY_ACTION_TYPE', data: 'x' }); 181 | }) 182 | ``` 183 | 184 | **flux.using(...middleware)** 185 | 186 | Takes an argument list of middleware folds them over the internal dispatcher, on a new Flux object. This can be called multiple times. 187 | 188 | ```js 189 | flux = flux.using(thunk, promise); 190 | ``` 191 | 192 | **flux.state()** 193 | 194 | Returns the state. 195 | 196 | ```js 197 | flux.state(); 198 | ``` 199 | 200 | **flux.hook(fn)** 201 | 202 | Registers a function as a listener. 203 | 204 | ```js 205 | flux.hook(state => console.log(state)); 206 | ``` 207 | 208 | **flux.unhook(fn)** 209 | 210 | Deregisters a function that was previously registered as a listener. 211 | 212 | ```js 213 | let fn = ::console.log; 214 | 215 | flux.hook(fn); 216 | 217 | flux.unhook(fn); 218 | ``` 219 | 220 | ## Glossary 221 | 222 | **State** 223 | 224 | A *State* is any value that is representative of your application. It can be a primitive, Object, Array, or anything else. If you wish to implement de/rehydration, you may want to keep JSON serialization in mind. 225 | 226 | **Action** 227 | 228 | An *Action* is an Object that contains information on how to update the state. Customarily, they have a `type` property, along with any other data that your reducers may need to operate on the state. 229 | 230 | **Action Creator** 231 | 232 | *Action Creators* are not internally known by fluxette, but are usually used in Flux applications to parametrize actions. By the norm of Functional Flux, they are functions that return *Actions*. 233 | 234 | **Reducer** 235 | 236 | A *Reducer* is a function that accepts a state and an action, which it combines, or *reduces*, to create a new state. All Reducers should have the signature `(State, Action) => State`. The `Shape`, `Reducer`, and `Filter` facilities all return a Reducer. There are Pure Reducers and Dirty Reducers. Pure Reducers reduce based only on the state and action provided, while Dirty Reducers pull data from outside, or have side effects. 237 | 238 | **Hook** 239 | 240 | *Hooks* (or *Listeners*) are functions that respond to a change in the state. They have a wide spectrum of uses; they are similar to the `change` event listeners of a traditional MVC framework. They have a signature of `(?State) => void`. The `connect` decorator uses a hook to subscribe your components to state changes. 241 | 242 | **Selector** 243 | 244 | A *Selector* is a function that takes a state and makes it more specific. Selectors are very useful in React components, to keep your `render` method DRY and orthogonal, and to take advantage of caching features. For more advanced caching, you can use the `select` facility, which also returns a Selector. Selectors have the signature `(State) => State`. 245 | 246 | **Deriver** 247 | 248 | The *Deriver* is an concept specific to the `select` facility. `select` takes a Selector or an array of Selectors, and passes the results of each to the deriver to create a logical (as opposed to raw) data object that your application uses. Derivers are functions that expect the results of Selectors and returns a State. 249 | 250 | ## The Law of Functional Flux 251 | In the most general sense, Functional Flux relies on reducing actions into the state. Therefore, Stores or Reducers are pure functions with the signature `(State, Action) => State`. If a Store processes an action that it listens to, which results in a different state, it returns a value or reference that differs from the state that it was called with. This recursively cascades down to the root of the state tree. At the end of the dispatch, all listeners are called. Any of which that depend on data that could have possibly changed are called with new values or references. Thus, listeners can simply maintain a reference to the old state and compare with the new one to determine whether the state has changed. 252 | 253 | ## Middleware 254 | Middleware can extend the functionality of the dispatcher by accommodating functions, Promises, and other data structures, allowing for advanced asynchronous and multiplexing functionality. Middleware do not require knowledge of other middleware, which makes them easily composable. To use middleware, create a new flux object that implements them, by passing a list to `flux.using`. 255 | 256 | ```js 257 | import thunk from 'fluxette-thunk'; 258 | import promise from 'fluxette-promise'; 259 | 260 | flux = flux.using(thunk, promise); 261 | ``` 262 | 263 | ### Writing Middleware 264 | Middleware is implemented as a *creator* function that takes a `next` argument, which returns a *dispatcher* function that takes an `action` argument. The middleware is bound to the Flux object, so methods such as `dispatch` are available on `this`. To accommodate this, a *creator* cannot be an arrow function. Unless you need to break the dispatch pipeline, you should call `next` to pass the action on to the next *dispatcher* function. A valid breakage of the pipeline can be seen in the Promise middleware, where it passes `this.dispatch` to the `Promise#then`. It is good practice to return the value of your last call, whether it may be a `next`, `then`, or other call. 265 | 266 | ## Rehydration 267 | `fluxette` supports both imperative and declarative rehydration. You may pick whichever suits your needs more. 268 | 269 | **Imperative** 270 | ```js 271 | import Flux from 'fluxette'; 272 | import Shape from 'reducer/shape'; 273 | import History from 'reducer/history'; 274 | 275 | let flux = Flux(Shape({ 276 | history: History(), 277 | // ... 278 | })); 279 | 280 | flux.dispatch(actions); 281 | // dehydrating 282 | let { history } = flux.state(); 283 | sendToServer({ history }); 284 | // rehydrating 285 | let { history } = getFromServer(); 286 | flux.dispatch(history); 287 | ``` 288 | 289 | **Declarative** 290 | ```js 291 | import Flux from 'fluxette'; 292 | import Shape from 'reducer/shape'; 293 | import History from 'reducer/history'; 294 | import Hydrate from 'reducer/hydrate'; 295 | 296 | let flux = Flux(Hydrate(Shape({ 297 | history: History(), 298 | // ... 299 | }))); 300 | 301 | flux.dispatch(actions); 302 | // dehydrating 303 | let state = flux.state(); 304 | sendToServer({ state }); 305 | // rehydrating 306 | let { state } = getFromServer(); 307 | flux.dispatch({ type: Hydrate.type, state }); 308 | ``` 309 | 310 | If, for any reason you wanted to use both methods, this is one possible way: 311 | 312 | **Mux** 313 | ```js 314 | import Flux from 'fluxette'; 315 | import Shape from 'reducer/shape'; 316 | import History from 'reducer/history'; 317 | import Hydrate from 'reducer/hydrate'; 318 | 319 | let flux = Flux(Hydrate(Shape({ 320 | history: History(), 321 | // ... 322 | }))); 323 | 324 | let state = flux.state(); 325 | flux.dispatch(actions); 326 | // dehydrating 327 | let history = state.history; 328 | sendToServer({ state, history }); 329 | // rehydrating 330 | let { state, history } = getFromServer(); 331 | flux.dispatch([{ type: Hydrate.type, state }, ...history]); 332 | ``` 333 | 334 | ## Debugging 335 | 336 | **To log all actions in raw form, as `dispatch` is directly called with:** 337 | Add a logger to the beginning of your middleware chain. This will log all actions before they are possibly transformed by other middleware. 338 | 339 | ```js 340 | flux = flux.using( 341 | function(next) { 342 | return action => { 343 | console.log(this.state(), action); 344 | next(action); 345 | } 346 | }, 347 | ...middleware 348 | ); 349 | ``` 350 | 351 | **To log all actions just before they are reduced:** Add a logger to the end of your middleware chain. This will log only actions that the reducers see, and will be logged to the history. 352 | 353 | ```js 354 | flux = flux.using( 355 | ...middleware, 356 | function(next) { 357 | return action => { 358 | console.log(this.state(), action); 359 | next(action); 360 | } 361 | } 362 | ); 363 | ``` 364 | 365 | 366 | **To log all actions after they are reduced:** Pass your logger/debugger to `hook` and `unhook` when you're done. 367 | 368 | ```js 369 | let logger = (state, actions) => { 370 | console.log(state, actions); 371 | sendToDashboard(state); 372 | alertTimeMachine(actions); 373 | } 374 | 375 | flux.hook(logger); 376 | 377 | flux.unhook(logger); 378 | ``` 379 | 380 | ## SSR 381 | Universal flux is very easy. Because `fluxette` doesn't force you to use use singletons, you can create new instances on the fly. 382 | 383 | ```js 384 | import Flux from 'fluxette'; 385 | import reducers from './reducers'; 386 | import App from './views'; 387 | 388 | server.route('/', (req, res) => { 389 | let flux = Flux(reducers); 390 | res.end(React.renderToString( 391 | 392 | { () => } 393 | 394 | )); 395 | }) 396 | ``` 397 | 398 | ## Asynchronous 399 | fluxette by itself is completely synchronous, but is engineered with asynchronous functionality in mind. [`thunk`](https://github.com/edge/fluxette-thunk) and [`promise`](https://github.com/edge/fluxette-promise) are two middleware available in the ecosystem. 400 | 401 | Mix and match the many possible styles to your liking. 402 | 403 | One way to use the thunk middleware is to use an async action that combines actions: 404 | ```js 405 | import thunk from 'fluxette-thunk'; 406 | 407 | flux = flux.using(thunk); 408 | let { dispatch } = flux; 409 | 410 | // actions 411 | let resource = { 412 | request: () => ({ type: RESOURCE.REQUEST }), 413 | success: data => ({ type: RESOURCE.SUCCESS, data }), 414 | failure: err => ({ type: RESOURCE.FAILURE, err }) 415 | }; 416 | 417 | // async action 418 | let getResource = url => { 419 | ({ dispatch }) => { 420 | dispatch(resource.request()); 421 | asyncRequest(url, (err, data) => { 422 | if (err) { 423 | dispatch(resource.failure(err)); 424 | } 425 | else { 426 | dispatch(resource.success(data)); 427 | } 428 | }); 429 | } 430 | } 431 | 432 | dispatch(getResource(data)); 433 | ``` 434 | 435 | Another way is to return an array of actions directly from the action creator: 436 | ```js 437 | import thunk from 'fluxette-thunk'; 438 | 439 | flux = flux.using(thunk); 440 | 441 | let getResource = url => [ 442 | { type: RESOURCE.REQUEST }, 443 | ({ dispatch }) => { 444 | asyncRequest(url, (err, data) => { 445 | if (err) { 446 | dispatch({ type: RESOURCE.FAILURE, err }); 447 | } 448 | else { 449 | dispatch({ type: RESOURCE.SUCCESS, data }); 450 | } 451 | }); 452 | } 453 | ]; 454 | 455 | flux.dispatch(getResource(data)); 456 | ``` 457 | 458 | Or, if your asynchronous actions can be represented as Promises (usually the most concise and reasonable way): 459 | ```js 460 | import thunk from 'fluxette-thunk'; 461 | 462 | flux = flux.using(thunk); 463 | 464 | let getResource = data => 465 | ({ dispatch }) => { 466 | dispatch({ type: RESOURCE.REQUEST }); 467 | asyncRequest(url) 468 | .then(data => ({ type: RESOURCE.SUCCESS, data })) 469 | .catch(err => ({ type: RESOURCE.FAILURE, err })) 470 | .then(dispatch); 471 | } 472 | 473 | flux.dispatch(getResource(data)); 474 | ``` 475 | 476 | ## Optimistic Updates 477 | Using the design pattern outlined in [Asynchronous](#asynchronous), the preemptive *request* action can be used for optimistic updates on an asynchronous write from the client. If the write completed successfully, the *success* action can then be dispatched, without the user experiencing any latency. If the write failed, a *failure* action can be dispatched to easily rollback the changes from the optimistic update. 478 | 479 | ```js 480 | import thunk from 'fluxette-thunk'; 481 | import Leaf from 'reducer/leaf'; 482 | 483 | flux = flux.using(thunk); 484 | 485 | let setMessage = (messages, { message }) => ({ 486 | ...messages, 487 | [message.id]: message 488 | }); 489 | 490 | let messageReducer = Leaf({}, { 491 | [MESSAGE.REQUEST]: setMessage, 492 | [MESSAGE.SUCCESS]: setMessage, 493 | [MESSAGE.FAILURE]: (messages, { message }) => { 494 | let { [message.id]: remove, ...rollback } = messages; 495 | return rollback; 496 | } 497 | }); 498 | 499 | let sendMessage = message => 500 | ({ dispatch }) => { 501 | dispatch({ type: MESSAGE.REQUEST, message }); 502 | sendToServerPromise(message) 503 | .then(() => ({ type: RESOURCE.SUCCESS, message })) 504 | .catch(err => ({ type: RESOURCE.FAILURE, err })) 505 | .then(dispatch); 506 | }; 507 | 508 | flux.dispatch(sendMessage(message)); 509 | ``` 510 | 511 | ## Store Dependencies 512 | Store dependencies in vanilla flux is handled by using `waitFor`, but `waitFor` is hard to trace, and may result in unpredictable application behavior, such as infinite loops. The fluxette way to handle reducer dependencies is to instead split an action into two semantic actions, which will be dispatched in order. This is known as action splitting, and it allows for declarative reducer dependencies, simultaneously improving clarity and preventing any possibility of mutual locks. In most Flux implementations, this would not be a viable solution, as dispatching twice results in an extra setState on each component. Since fluxette allows you to dispatch arrays of actions, atomic action handling is possible, and only one setState is called once the array has been fully processed. A dependency refactor usually should not involve changing component code; you can just make the relevant action creator return an array of actions instead. If you do not want to use action splitting, you can implement a short custom Store instead (see [Store Composition](#store-composition)). 513 | 514 | ## Store Composition 515 | Because Stores are just pure functions, they can easily be composed and reused. 516 | 517 | ```js 518 | import Leaf from 'reducer/leaf'; 519 | 520 | let player = Leaf({ points: 0 }, { 521 | [PLAYER.ADDPOINTS]: (state, action) => ({ ...state, points: state.points + action.points }) 522 | }); 523 | 524 | let game = (state = { left: {}, right: {} }, action) => { 525 | if (action.player === LEFT) { 526 | let left = player(state.left); 527 | if (left !== state.left) { 528 | return { ...state, left }; 529 | } 530 | } 531 | else if (action.player === RIGHT) { 532 | let right = player(state.right); 533 | if (right !== state.right) { 534 | return { ...state, right }; 535 | } 536 | } 537 | return state; 538 | } 539 | ``` 540 | 541 | For simpler use cases, just plug Reducers into other Reducers. (You should write a Reducer to do this). 542 | 543 | ```js 544 | import Shape from 'reducer/shape'; 545 | 546 | let users = {}; 547 | 548 | let dispatchCount = (state = 0) => state + 1; 549 | 550 | let userReducer = Shape(users); 551 | 552 | // user_029347 joined 553 | users['user_029347'] = dispatchCount; 554 | 555 | // after 5 dispatches 556 | state() 557 | // { 558 | // user_029347: 5 559 | // } 560 | 561 | // user_120938 joined 562 | users['user_120938'] = dispatchCount; 563 | 564 | // after 2 dispatches 565 | state() 566 | // { 567 | // user_029347: 7, 568 | // user_120938: 2 569 | // } 570 | ``` 571 | 572 | ## Examples 573 | Examples can be found [here](https://github.com/edge/fluxette/tree/master/examples). 574 | 575 | ## Testing 576 | ```sh 577 | npm i -g testem mocha 578 | npm test 579 | ``` 580 | 581 | ## Todo 582 | * write async data dependencies example 583 | * add code coverage, CI, badges, etc. 584 | * submit to HN 585 | 586 | ## Influences 587 | * Unix 588 | * @gaearon 589 | * [Alan Baker/Occam's Razor](http://plato.stanford.edu/archives/sum2011/entries/simplicity/#OntPar) 590 | -------------------------------------------------------------------------------- /browser.config.js: -------------------------------------------------------------------------------- 1 | import { optimize as oz } from 'webpack'; 2 | 3 | export default production => { 4 | let filename = production 5 | ? 'fluxette.min.js' 6 | : 'fluxette.js'; 7 | 8 | let plugins = [ 9 | new oz.DedupePlugin(), 10 | new oz.OccurenceOrderPlugin() 11 | ].concat(production 12 | ? [ 13 | new oz.UglifyJsPlugin({ 14 | compressor: { 15 | screw_ie8: true, 16 | warnings: false 17 | } 18 | }), 19 | new oz.AggressiveMergingPlugin() 20 | ] : [] 21 | ); 22 | 23 | return { 24 | entry: './src/index.js', 25 | output: { 26 | path: './dist', 27 | filename, 28 | library: 'fluxette', 29 | libraryTarget: 'umd' 30 | }, 31 | module: { 32 | loaders: [{ test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }] 33 | }, 34 | plugins 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | let gulp = require('gulp'), 2 | webpack = require('webpack'), 3 | browser = require('../browser.config'), 4 | npm = require('../npm.config'), 5 | test = require('../test.config'); 6 | 7 | let log = next => 8 | (err, stats) => { 9 | if (err) { 10 | throw new Error(err); 11 | } 12 | console.log(stats.toString()); 13 | next(); 14 | }; 15 | 16 | gulp.task('build', ['npm', 'browser']); 17 | 18 | gulp.task('npm', next => webpack(npm, log(next))); 19 | gulp.task('browser', ['min', 'dev']); 20 | 21 | gulp.task('min', next => webpack(browser(true), log(next))); 22 | gulp.task('dev', next => webpack(browser(false), log(next))); 23 | 24 | gulp.task('test', next => webpack(test, log(next))); 25 | -------------------------------------------------------------------------------- /dist/fluxette.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["fluxette"] = factory(); 8 | else 9 | root["fluxette"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | 'use strict'; 58 | 59 | Object.defineProperty(exports, '__esModule', { 60 | value: true 61 | }); 62 | 63 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 64 | 65 | var _util = __webpack_require__(1); 66 | 67 | exports['default'] = function (reducer) { 68 | var _state = arguments.length <= 1 || arguments[1] === undefined ? reducer() : arguments[1]; 69 | 70 | return (function () { 71 | var hooks = [], 72 | status = 0; 73 | 74 | var makeDispatch = function makeDispatch(reduce) { 75 | return function (actions) { 76 | var call = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; 77 | 78 | status++; 79 | actions = (0, _util.normalize)(actions).map(reduce); 80 | status--; 81 | if (call && status === 0) { 82 | for (var i = 0; i < hooks.length; i++) { 83 | hooks[i](_state); 84 | } 85 | } 86 | return actions; 87 | }; 88 | }; 89 | 90 | return ({ 91 | reduce: function reduce(action) { 92 | _state = reducer(_state, action); 93 | return action; 94 | }, 95 | using: function using() { 96 | var flux = _extends({}, this); 97 | 98 | for (var _len = arguments.length, middleware = Array(_len), _key = 0; _key < _len; _key++) { 99 | middleware[_key] = arguments[_key]; 100 | } 101 | 102 | flux.dispatch = makeDispatch(flux.reduce = middleware.reduceRight(function (next, ware) { 103 | return ware.call(flux, next); 104 | }, flux.reduce)); 105 | return flux; 106 | }, 107 | state: function state() { 108 | return _state; 109 | }, 110 | hook: hooks.push.bind(hooks), 111 | unhook: function unhook(fn) { 112 | (0, _util.remove)(hooks, fn); 113 | } 114 | }).using(); 115 | })(); 116 | }; 117 | 118 | module.exports = exports['default']; 119 | 120 | /***/ }, 121 | /* 1 */ 122 | /***/ function(module, exports) { 123 | 124 | "use strict"; 125 | 126 | Object.defineProperty(exports, "__esModule", { 127 | value: true 128 | }); 129 | var $normalize = function $normalize(arr, into) { 130 | for (var i = 0; i < arr.length; i++) { 131 | var val = arr[i]; 132 | if (Array.isArray(val)) { 133 | $normalize(val, into); 134 | } else { 135 | if (val instanceof Object) { 136 | into.push(val); 137 | } 138 | } 139 | } 140 | }; 141 | 142 | var normalize = function normalize(arr) { 143 | if (Array.isArray(arr)) { 144 | var norm = []; 145 | $normalize(arr, norm); 146 | return norm; 147 | } else { 148 | return [arr]; 149 | } 150 | }; 151 | 152 | exports.normalize = normalize; 153 | // Delete object from array 154 | var remove = function remove(array, obj) { 155 | var index = array.indexOf(obj); 156 | if (index !== -1) { 157 | array.splice(index, 1); 158 | } 159 | }; 160 | exports.remove = remove; 161 | 162 | /***/ } 163 | /******/ ]) 164 | }); 165 | ; -------------------------------------------------------------------------------- /dist/fluxette.min.js: -------------------------------------------------------------------------------- 1 | !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports.fluxette=r():e.fluxette=r()}(this,function(){return function(e){function r(n){if(t[n])return t[n].exports;var u=t[n]={exports:{},id:n,loaded:!1};return e[n].call(u.exports,u,u.exports,r),u.loaded=!0,u.exports}var t={};return r.m=e,r.c=t,r.p="",r(0)}([function(e,r,t){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n=Object.assign||function(e){for(var r=1;ru;u++)t[u]=arguments[u];return e.dispatch=a(e.reduce=t.reduceRight(function(r,t){return t.call(e,r)},e.reduce)),e},state:function(){return r},hook:t.push.bind(t),unhook:function(e){u.remove(t,e)}}.using()}()},e.exports=r.default},function(e,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var t=function o(e,r){for(var t=0;tu;u++)t[u]=arguments[u];return r.dispatch=a(r.reduce=t.reduceRight(function(e,t){return t.call(r,e)},r.reduce)),r},state:function(){return e},hook:t.push.bind(t),unhook:function(r){u.remove(t,r)}}.using()}()},r.exports=e.default},function(r,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=function o(r,e){for(var t=0;t { 4 | let hooks = [], 5 | status = 0; 6 | 7 | let makeDispatch = reduce => 8 | (actions, call = true) => { 9 | status++; 10 | actions = normalize(actions).map(reduce); 11 | status--; 12 | if (call && status === 0) { 13 | for (let i = 0; i < hooks.length; i++) { 14 | hooks[i](state); 15 | } 16 | } 17 | return actions; 18 | }; 19 | 20 | return { 21 | reduce: action => { 22 | state = reducer(state, action); 23 | return action; 24 | }, 25 | using(...middleware) { 26 | let flux = { ...this }; 27 | flux.dispatch = makeDispatch(flux.reduce = middleware.reduceRight( 28 | (next, ware) => flux::ware(next), flux.reduce 29 | )); 30 | return flux; 31 | }, 32 | state: () => state, 33 | hook: ::hooks.push, 34 | unhook: fn => { remove(hooks, fn); } 35 | }.using(); 36 | } 37 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | let $normalize = (arr, into) => { 2 | for (let i = 0; i < arr.length; i++) { 3 | let val = arr[i]; 4 | if (Array.isArray(val)) { 5 | $normalize(val, into); 6 | } 7 | else { 8 | if (val instanceof Object) { 9 | into.push(val); 10 | } 11 | } 12 | } 13 | }; 14 | 15 | export let normalize = arr => { 16 | if (Array.isArray(arr)) { 17 | let norm = []; 18 | $normalize(arr, norm); 19 | return norm; 20 | } 21 | else { 22 | return [arr]; 23 | } 24 | }; 25 | 26 | // Delete object from array 27 | export let remove = (array, obj) => { 28 | let index = array.indexOf(obj); 29 | if (index !== -1) { 30 | array.splice(index, 1); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /test.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | devtool: 'eval', 3 | entry: './test/', 4 | output: { 5 | path: './dist', 6 | filename: 'testem.js' 7 | }, 8 | module: { 9 | loaders: [{ test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/dispatch.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import chai, { expect } from 'chai'; 3 | import spies from 'chai-spies'; 4 | import Flux from '..'; 5 | import Shape from 'reducer/shape'; 6 | import Leaf from 'reducer/leaf'; 7 | 8 | chai.use(spies); 9 | 10 | const TYPES = { 11 | A: 'A', 12 | B: 'B', 13 | BOGUS: 'BOGUS' 14 | }; 15 | 16 | describe('dispatch', () => { 17 | it('should update state when called', () => { 18 | let { dispatch, state } = Flux(Shape({ 19 | a: Leaf(0, { 20 | [TYPES.A]: state => state + 1, 21 | [TYPES.B]: state => state - 1 22 | }), 23 | b: Leaf('', { 24 | [TYPES.A]: state => state + 'a', 25 | [TYPES.B]: state => state + 'b' 26 | }) 27 | })); 28 | 29 | dispatch({ type: TYPES.A }); 30 | expect(state()).to.deep.equal({ 31 | a: 1, 32 | b: 'a' 33 | }); 34 | dispatch({ type: TYPES.B }); 35 | expect(state()).to.deep.equal({ 36 | a: 0, 37 | b: 'ab' 38 | }); 39 | }); 40 | it('should dispatch arrays', () => { 41 | let { dispatch, state } = Flux(Shape({ 42 | a: Leaf(0, { 43 | [TYPES.A]: state => state + 1, 44 | [TYPES.B]: state => state - 1 45 | }), 46 | b: Leaf('', { 47 | [TYPES.A]: state => state + 'a', 48 | [TYPES.B]: state => state + 'b' 49 | }) 50 | })); 51 | 52 | dispatch([{ type: TYPES.A }, { type: TYPES.B }]); 53 | expect(state()).to.deep.equal({ 54 | a: 0, 55 | b: 'ab' 56 | }); 57 | }); 58 | it('should not call hooks when nothing is passed', () => { 59 | let { dispatch, hook } = Flux(Shape({ 60 | a: Leaf(0, { 61 | [TYPES.A]: state => state + 1, 62 | [TYPES.B]: state => state - 1 63 | }), 64 | b: Leaf('', { 65 | [TYPES.A]: state => state + 'a', 66 | [TYPES.B]: state => state + 'b' 67 | }) 68 | })); 69 | 70 | let spy = chai.spy(() => {}); 71 | hook(spy); 72 | dispatch(); 73 | expect(spy).not.to.have.been.called; 74 | }); 75 | it('should not notify hooks when non-Objects are passed', () => { 76 | let { dispatch, hook } = Flux(Shape({ 77 | a: Leaf(0, { 78 | [TYPES.A]: state => state + 1, 79 | [TYPES.B]: state => state - 1 80 | }), 81 | b: Leaf('', { 82 | [TYPES.A]: state => state + 'a', 83 | [TYPES.B]: state => state + 'b' 84 | }) 85 | })); 86 | 87 | let spy = chai.spy(() => {}); 88 | hook(spy); 89 | dispatch(undefined, [0, false, null]); 90 | expect(spy).not.to.have.been.called; 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/hooks.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import chai, { expect } from 'chai'; 3 | import spies from 'chai-spies'; 4 | import Flux from '..'; 5 | import Leaf from 'reducer/leaf'; 6 | 7 | chai.use(spies); 8 | 9 | const TYPES = { 10 | A: 'A', 11 | B: 'B', 12 | BOGUS: 'BOGUS' 13 | }; 14 | 15 | describe('hook', () => { 16 | it('should call listeners by the number of dispatches', () => { 17 | let { dispatch, hook } = Flux(Leaf(0, { 18 | [TYPES.A]: state => state + 1, 19 | [TYPES.B]: state => state - 1 20 | })); 21 | 22 | let spy = chai.spy(() => {}); 23 | hook(spy); 24 | dispatch({ type: TYPES.A }); 25 | dispatch(); 26 | dispatch([{ type: TYPES.A }, { type: TYPES.B }]); 27 | dispatch({ type: TYPES.A }); 28 | expect(spy).to.have.been.called.exactly(4); 29 | }); 30 | it('should call listeners with (state)', done => { 31 | let { dispatch, state: getState, hook } = Flux(Leaf(0, { 32 | [TYPES.A]: state => state + 1, 33 | [TYPES.B]: state => state - 1 34 | })); 35 | 36 | let spy = chai.spy(state => { 37 | expect(state).to.equal(getState()); 38 | done(); 39 | }); 40 | hook(spy); 41 | dispatch([{ type: TYPES.A }, { type: TYPES.B }, { type: TYPES.BOGUS }]); 42 | expect(spy).to.have.been.called.once; 43 | }); 44 | }); 45 | 46 | describe('unhook', () => { 47 | it('should not call listeners after unhook', () => { 48 | let { dispatch, hook, unhook } = Flux(Leaf(0, { 49 | [TYPES.A]: state => state + 1, 50 | [TYPES.B]: state => state - 1 51 | })); 52 | 53 | let spy = chai.spy(() => {}); 54 | hook(spy); 55 | dispatch([{ type: TYPES.A }, { type: TYPES.B }]); 56 | dispatch(); 57 | unhook(spy); 58 | dispatch({ type: TYPES.A }); 59 | expect(spy).to.have.been.called.twice; 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import { expect } from 'chai'; 3 | import Flux from '..'; 4 | import Shape from 'reducer/shape'; 5 | 6 | import './dispatch'; 7 | import './hooks'; 8 | import './middleware'; 9 | import './react'; 10 | import './rehydration'; 11 | import './state'; 12 | import './store'; 13 | 14 | describe('index', () => { 15 | it('should properly construct flux object', () => { 16 | let flux = Flux(Shape()); 17 | expect(flux).to.have.property('reduce') 18 | .that.is.an.instanceof(Function); 19 | expect(flux).to.have.property('dispatch') 20 | .that.is.an.instanceof(Function); 21 | expect(flux).to.have.property('state') 22 | .that.is.an.instanceof(Function); 23 | expect(flux).to.have.property('hook') 24 | .that.is.an.instanceof(Function); 25 | expect(flux).to.have.property('unhook') 26 | .that.is.an.instanceof(Function); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/middleware.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import chai, { expect } from 'chai'; 3 | import spies from 'chai-spies'; 4 | import Flux from '..'; 5 | import Leaf from 'reducer/leaf'; 6 | import thunk from 'fluxette-thunk'; 7 | import promise from 'fluxette-promise'; 8 | 9 | chai.use(spies); 10 | 11 | const TYPES = { 12 | A: 'A', 13 | B: 'B', 14 | BOGUS: 'BOGUS' 15 | }; 16 | 17 | describe('middleware', () => { 18 | describe('thunk', () => { 19 | it('should dispatch functions', done => { 20 | let flux = Flux(Leaf(0, { 21 | [TYPES.A]: state => state + 1, 22 | [TYPES.B]: state => state - 1 23 | })).using(thunk); 24 | flux.dispatch({ type: TYPES.A }); 25 | 26 | flux.hook(state => { 27 | expect(state).to.equal(2); 28 | done(); 29 | }); 30 | 31 | flux.dispatch(({ dispatch }) => { 32 | dispatch({ type: TYPES.A }); 33 | }); 34 | }); 35 | }); 36 | describe('promise', () => { 37 | it('should dispatch promises', done => { 38 | let flux = Flux(Leaf(0, { 39 | [TYPES.A]: state => state + 1, 40 | [TYPES.B]: state => state - 1 41 | })).using(promise); 42 | 43 | flux.dispatch({ type: TYPES.A }); 44 | 45 | let temporal = () => { 46 | flux.unhook(temporal); 47 | flux.hook(state => { 48 | expect(state).to.equal(2); 49 | done(); 50 | }); 51 | }; 52 | flux.hook(temporal); 53 | 54 | flux.dispatch( 55 | new Promise(res => setTimeout(() => res(TYPES.A), 10)) 56 | .then(type => ({ type })) 57 | ); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/react.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import React, { addons, PropTypes } from 'react/addons'; 3 | import chai, { expect } from 'chai'; 4 | import spies from 'chai-spies'; 5 | import Flux from '..'; 6 | import Shape from 'reducer/shape'; 7 | import Leaf from 'reducer/leaf'; 8 | import { Context, connect, select } from 'fluxette-react'; 9 | 10 | chai.use(spies); 11 | 12 | let { TestUtils: { Simulate, renderIntoDocument, findRenderedComponentWithType } } = addons; 13 | 14 | const USER = { 15 | SETNAME: 'USER_SETNAME', 16 | SETEMAIL: 'USER_SETEMAIL' 17 | }; 18 | 19 | describe('React', () => { 20 | 21 | it('should hook component without specifier', () => { 22 | let flux = Flux(Shape({ 23 | user: Leaf({ username: '', email: '' }, { 24 | [USER.SETNAME]: (state, action) => ({ ...state, username: action.name }), 25 | [USER.SETEMAIL]: (state, action) => ({ ...state, email: action.email }) 26 | }) 27 | })); 28 | 29 | @connect() 30 | class Component extends React.Component { 31 | submit() { 32 | let { dispatch } = this.context.flux; 33 | let username = React.findDOMNode(this.refs.username).value; 34 | let email = React.findDOMNode(this.refs.email).value; 35 | dispatch([{ type: USER.SETNAME, name: username }, { type: USER.SETEMAIL, email }]); 36 | } 37 | render() { 38 | let { user } = this.state; 39 | return ( 40 |
41 | 42 | 43 |
47 | ); 48 | } 49 | } 50 | 51 | let tree = renderIntoDocument( 52 | 53 | { () => } 54 | 55 | ); 56 | let c = findRenderedComponentWithType(tree, Component); 57 | React.findDOMNode(c.refs.username).value = 'fluxette'; 58 | React.findDOMNode(c.refs.email).value = 'fluxette@fluxette.github.io'; 59 | Simulate.click(React.findDOMNode(c.refs.submit)); 60 | expect(React.findDOMNode(c.refs.email_label).innerHTML).to.equal('fluxette@fluxette.github.io'); 61 | expect(React.findDOMNode(c.refs.username_label).innerHTML).to.equal('fluxette'); 62 | }); 63 | it('should hook component with specifier', () => { 64 | let flux = Flux(Shape({ 65 | user: Leaf({ username: '', email: '' }, { 66 | [USER.SETNAME]: (state, action) => ({ ...state, username: action.name }), 67 | [USER.SETEMAIL]: (state, action) => ({ ...state, email: action.email }) 68 | }) 69 | })); 70 | 71 | @connect(state => state.user) 72 | class Component extends React.Component { 73 | submit() { 74 | let { dispatch } = this.context.flux; 75 | let username = React.findDOMNode(this.refs.username).value; 76 | let email = React.findDOMNode(this.refs.email).value; 77 | dispatch([{ type: USER.SETNAME, name: username }, { type: USER.SETEMAIL, email }]); 78 | } 79 | render() { 80 | let user = this.state; 81 | return ( 82 |
83 | 84 | 85 |
89 | ); 90 | } 91 | } 92 | 93 | let tree = renderIntoDocument( 94 | 95 | { () => } 96 | 97 | ); 98 | let c = findRenderedComponentWithType(tree, Component); 99 | React.findDOMNode(c.refs.username).value = 'fluxette'; 100 | React.findDOMNode(c.refs.email).value = 'fluxette@fluxette.github.io'; 101 | Simulate.click(React.findDOMNode(c.refs.submit)); 102 | expect(React.findDOMNode(c.refs.email_label).innerHTML).to.equal('fluxette@fluxette.github.io'); 103 | expect(React.findDOMNode(c.refs.username_label).innerHTML).to.equal('fluxette'); 104 | }); 105 | 106 | it('should not rerender if data has not changed', () => { 107 | let spy = chai.spy(() => {}); 108 | let flux = Flux(Shape({ 109 | user: Leaf({ username: '', email: '' }, { 110 | [USER.SETNAME]: (state, action) => ({ ...state, username: action.name }), 111 | [USER.SETEMAIL]: (state, action) => ({ ...state, email: action.email }) 112 | }) 113 | })); 114 | 115 | @connect() 116 | class Component extends React.Component { 117 | submit() { 118 | let { dispatch } = this.context.flux; 119 | let username = React.findDOMNode(this.refs.username).value; 120 | let email = React.findDOMNode(this.refs.email).value; 121 | dispatch([{ type: USER.SETNAME, name: username }, { type: USER.SETEMAIL, email }]); 122 | } 123 | render() { 124 | spy(); 125 | let { user } = this.state; 126 | return ( 127 |
128 | 129 | 130 |
134 | ); 135 | } 136 | } 137 | 138 | let tree = renderIntoDocument( 139 | 140 | { () => } 141 | 142 | ); 143 | let c = findRenderedComponentWithType(tree, Component); 144 | React.findDOMNode(c.refs.username).value = 'fluxette'; 145 | React.findDOMNode(c.refs.email).value = 'fluxette@fluxette.github.io'; 146 | Simulate.click(React.findDOMNode(c.refs.submit)); 147 | expect(React.findDOMNode(c.refs.email_label).innerHTML).to.equal('fluxette@fluxette.github.io'); 148 | expect(React.findDOMNode(c.refs.username_label).innerHTML).to.equal('fluxette'); 149 | expect(spy).to.have.been.called.twice; 150 | flux.dispatch({ type: 'bogus-type' }); 151 | expect(spy).to.have.been.called.twice; 152 | }); 153 | 154 | it('should not rerender if data has not changed with selector', () => { 155 | let spy = chai.spy(() => {}); 156 | let flux = Flux(Shape({ 157 | user: Leaf({ username: '', email: '' }, { 158 | [USER.SETNAME]: (state, action) => ({ ...state, username: action.name }), 159 | [USER.SETEMAIL]: (state, action) => ({ ...state, email: action.email }) 160 | }) 161 | })); 162 | 163 | @connect(select( 164 | state => state.user, 165 | user => { 166 | return { user }; 167 | } 168 | )) 169 | class Component extends React.Component { 170 | submit() { 171 | let { dispatch } = this.context.flux; 172 | let username = React.findDOMNode(this.refs.username).value; 173 | let email = React.findDOMNode(this.refs.email).value; 174 | dispatch([{ type: USER.SETNAME, name: username }, { type: USER.SETEMAIL, email }]); 175 | } 176 | render() { 177 | spy(); 178 | let { user } = this.state; 179 | return ( 180 |
181 | 182 | 183 |
187 | ); 188 | } 189 | } 190 | 191 | let tree = renderIntoDocument( 192 | 193 | { () => } 194 | 195 | ); 196 | let c = findRenderedComponentWithType(tree, Component); 197 | React.findDOMNode(c.refs.username).value = 'fluxette'; 198 | React.findDOMNode(c.refs.email).value = 'fluxette@fluxette.github.io'; 199 | Simulate.click(React.findDOMNode(c.refs.submit)); 200 | expect(React.findDOMNode(c.refs.email_label).innerHTML).to.equal('fluxette@fluxette.github.io'); 201 | expect(React.findDOMNode(c.refs.username_label).innerHTML).to.equal('fluxette'); 202 | expect(spy).to.have.been.called.twice; 203 | flux.dispatch({ type: 'bogus-type' }); 204 | expect(spy).to.have.been.called.twice; 205 | }); 206 | 207 | it('should not fail on unmount', () => { 208 | let flux = Flux(Shape()); 209 | 210 | class Component extends React.Component { 211 | constructor() { 212 | super(); 213 | this.state = { 214 | show: true 215 | }; 216 | } 217 | toggle() { 218 | this.setState({ 219 | show: !this.state.show 220 | }); 221 | } 222 | render() { 223 | let toggle =