├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .watchmanconfig ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gulpfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── Flux.js ├── Flux.min.js ├── FluxUtils.js └── FluxUtils.min.js ├── docs ├── Dispatcher.ko-KR.md ├── Dispatcher.md ├── Flux-Utils.md ├── In-Depth-Overview.md ├── Overview.ko-KR.md ├── Overview.md ├── Related-Libraries.md ├── Videos.ko-KR.md └── Videos.md ├── examples ├── README.md ├── flux-async │ ├── .babelrc │ ├── .flowconfig │ ├── .gitignore │ ├── README.md │ ├── flow │ │ ├── flux-utils.js │ │ ├── flux.js │ │ ├── immutable.js │ │ └── misc.js │ ├── index.html │ ├── package.json │ ├── server │ │ └── app.js │ ├── src │ │ ├── TodoActions.js │ │ ├── TodoDispatcher.js │ │ ├── containers │ │ │ └── AppContainer.js │ │ ├── data_managers │ │ │ ├── TodoAPI.js │ │ │ └── TodoDataManager.js │ │ ├── load_object │ │ │ ├── LoadObject.js │ │ │ ├── LoadObjectMap.js │ │ │ └── LoadObjectState.js │ │ ├── records │ │ │ └── Todo.js │ │ ├── root.js │ │ ├── stores │ │ │ ├── TodoDraftStore.js │ │ │ ├── TodoListStore.js │ │ │ ├── TodoLoggerStore.js │ │ │ └── TodoStore.js │ │ ├── utils │ │ │ └── FakeID.js │ │ └── views │ │ │ └── AppView.js │ ├── todomvc-common │ │ ├── .bower.json │ │ ├── base.css │ │ ├── bg.png │ │ ├── bower.json │ │ └── readme.md │ └── webpack.config.js ├── flux-concepts │ ├── README.md │ └── flux-simple-f8-diagram-with-client-action-1300w.png ├── flux-flow │ ├── .babelrc │ ├── .flowconfig │ ├── .gitignore │ ├── README.md │ ├── flow │ │ ├── flux-utils.js │ │ └── flux.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── AppActions.js │ │ ├── AppContainer.js │ │ ├── AppDispatcher.js │ │ ├── AppStore.js │ │ ├── AppView.js │ │ ├── __flowtests__ │ │ │ └── App-flowtest.js │ │ └── root.js │ └── webpack.config.js ├── flux-jest-container │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ └── src │ │ └── __tests__ │ │ ├── AppContainer-test.js │ │ └── __snapshots__ │ │ └── AppContainer-test.js.snap ├── flux-jest │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ └── src │ │ └── __tests__ │ │ └── TodoStore-test.js ├── flux-logging │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── TodoDispatcher.js │ │ ├── TodoLoggerStore.js │ │ └── root.js │ └── webpack.config.js ├── flux-shell │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ └── root.js │ └── webpack.config.js ├── flux-todomvc │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── containers │ │ │ └── AppContainer.js │ │ ├── data │ │ │ ├── Counter.js │ │ │ ├── Todo.js │ │ │ ├── TodoActionTypes.js │ │ │ ├── TodoActions.js │ │ │ ├── TodoDispatcher.js │ │ │ ├── TodoDraftStore.js │ │ │ ├── TodoEditStore.js │ │ │ └── TodoStore.js │ │ ├── root.js │ │ └── views │ │ │ └── AppView.js │ ├── todomvc-common │ │ ├── .bower.json │ │ ├── base.css │ │ ├── bg.png │ │ ├── bower.json │ │ └── readme.md │ └── webpack.config.js └── todomvc-common │ ├── .bower.json │ ├── base.css │ ├── bg.png │ ├── bower.json │ └── readme.md ├── img └── flux-diagram-white-background.png ├── index.js ├── jest.config.js ├── package.json ├── scripts ├── babel │ └── default-options.js └── jest │ ├── environment.js │ └── preprocessor.js ├── src ├── .flowconfig ├── Dispatcher.js ├── FluxMixinLegacy.js ├── FluxStoreGroup.js ├── __tests__ │ ├── Dispatcher-test.js │ └── FluxStoreGroup-test.js ├── container │ ├── FluxContainer.js │ ├── FluxContainerSubscriptions.js │ └── __tests__ │ │ └── FluxContainer-test.js ├── stores │ ├── FluxReduceStore.js │ ├── FluxStore.js │ └── __tests__ │ │ ├── FluxReduceStore-test.js │ │ └── FluxStore-test.js └── utils │ └── abstractMethod.js ├── utils.js ├── website ├── .gitignore ├── README.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.js │ │ ├── styles.module.css │ │ └── support.js ├── static │ └── img │ │ ├── favicon.ico │ │ ├── flux-logo-color.svg │ │ ├── flux-logo-white.svg │ │ ├── oss_logo.png │ │ ├── overview │ │ ├── flux-simple-f8-diagram-1300w.png │ │ ├── flux-simple-f8-diagram-explained-1300w.png │ │ └── flux-simple-f8-diagram-with-client-action-1300w.png │ │ ├── undraw_building_blocks.svg │ │ ├── undraw_programming.svg │ │ └── undraw_react.svg └── yarn.lock └── yarn.lock /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x, 16.x, 18.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: yarn install 27 | - run: yarn test 28 | - run: yarn run build 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | *.log 4 | website/src/flux/docs/ 5 | website/core/metadata.js 6 | flow/include 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | Makefile 3 | Flux.js 4 | CONTRIBUTING.md 5 | .flowconfig 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true, 3 | "bracketSpacing": false, 4 | "printWidth": 80, 5 | "proseWrap": "never", 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 4.0.3 4 | 5 | - Upgraded `fbjs` to 3.0.1 which fixes [CVE-2021-27292](https://www.whitesourcesoftware.com/vulnerability-database/CVE-2021-27292) 6 | 7 | ### 4.0.2 8 | 9 | - Added support for `UNSAFE_componentWillReceiveProps` lifecycle method 10 | 11 | ### 4.0.1 12 | 13 | - Upgrade `fbemitter` dependency to 3.x 14 | 15 | ### 4.0.0 16 | 17 | - Upgrade `fbjs` dependency to ^3.x 18 | - Upgrade for Babel 7 compatibility (#495) (thanks to @koba04) 19 | - Added React 17 as a peer dependency 20 | 21 | ### 3.1.3 22 | 23 | - Added support for React 16 24 | 25 | ### 3.1.2 26 | 27 | - No meaningful changes. 28 | 29 | ### 3.1.1 30 | 31 | - No meaningful changes. 32 | 33 | ### 3.1.0 34 | 35 | - `Dispatcher`: Methods `register` and `unregister` can once again be called in 36 | the middle of a dispatch. 37 | 38 | ### 3.0.0 39 | 40 | - `FluxMapStore`: Removed. It added very little value over `FluxReduceStore`. 41 | - `FluxContainer`: Subscriptions are setup in constructor rather than 42 | `componentDidMount` 43 | - `FluxContainer`: Can create containers using stateless functional components 44 | - `FluxContainer`: Uses functional version of `setState` 45 | - `FluxMixin`: Subscriptions are setup in `componentWillMount` rather than 46 | `componentDidMount` 47 | - `Dispatcher`: Methods `register` and `unregister` can not be called in the 48 | middle of a dispatch 49 | - `React` added as peer dependency to `flux/utils` 50 | - Package `dist/FluxUtils.js` alongside `dist/Flux.js` 51 | 52 | _**Note**: This is marked as a breaking change due to the large number of small 53 | changes in `FluxContainer`. Depending on how coupled code is to the timing of 54 | `componentWillMount`, `componentDidMount`, or setting state synchronously it is 55 | possible that there may be some breakages. Generally it should not be an issue._ 56 | 57 | ### 2.1.1 58 | 59 | - Publish `dist/` on npm 60 | 61 | ### 2.1.0 62 | 63 | - Add flux-utils which include four main base classes: `Store`, `ReduceStore`, 64 | `MapStore`, `Container` 65 | - Add flux-utils example and documentation 66 | - Upgrade build script 67 | - Publish a minified version of `Flux` in `dist/` 68 | - Add flow types to `Dispatcher` 69 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Flux 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Code of Conduct 6 | The code of conduct is described in [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) 7 | 8 | ## Pull Requests 9 | We actively welcome your pull requests. 10 | 11 | 1. Fork the repo and create your branch from `master`. 12 | 2. If you've added code that should be tested, add tests 13 | 3. If you've changed APIs, update the documentation. 14 | 4. Ensure the test suite passes. 15 | 5. Make sure your code lints. 16 | 6. If you haven't already, complete the Contributor License Agreement (CLA). 17 | 18 | ## Contributor License Agreement (CLA) 19 | In order to accept your pull request, we need you to submit a CLA. You only need 20 | to do this once to work on any of Facebook's open source projects. 21 | 22 | Complete your CLA here: 23 | 24 | ## Bugs 25 | 26 | ### Where to Find Known Issues 27 | 28 | We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. 29 | 30 | ### Reporting New Issues 31 | 32 | The best way to get your bug fixed is to provide a reduced test case. jsFiddle, jsBin, and other sites provide a way to give live examples. Those are especially helpful though may not work for `JSX`-based code. 33 | 34 | ### Security Bugs 35 | 36 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. With that in mind, please do not file public issues; go through the process outlined on that page. 37 | 38 | ### Code Conventions 39 | 40 | * Use semicolons `;` 41 | * Commas last `,` 42 | * 2 spaces for indentation (no tabs) 43 | * Prefer `'` over `"` 44 | * `'use strict';` 45 | * 80 character line length 46 | * Write "attractive" code 47 | * Do not use the optional parameters of `setTimeout` and `setInterval` 48 | 49 | ### Documentation 50 | 51 | * Do not wrap lines at 80 characters 52 | 53 | ## License 54 | 55 | By contributing to Flux, you agree that your contributions will be licensed under its BSD license. 56 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | */ 8 | 9 | const babel = require('gulp-babel'); 10 | const del = require('del'); 11 | const flatten = require('gulp-flatten'); 12 | const gulp = require('gulp'); 13 | const gulpUtil = require('gulp-util'); 14 | const header = require('gulp-header'); 15 | const rename = require('gulp-rename'); 16 | const webpackStream = require('webpack-stream'); 17 | 18 | const babelDefaultOptions = require('./scripts/babel/default-options'); 19 | 20 | const DEVELOPMENT_HEADER = 21 | ['/**', ' * Flux v<%= version %>', ' */'].join('\n') + '\n'; 22 | const PRODUCTION_HEADER = 23 | [ 24 | '/**', 25 | ' * Flux v<%= version %>', 26 | ' *', 27 | ' * Copyright (c) Meta Platforms, Inc. and affiliates.', 28 | ' *', 29 | ' * This source code is licensed under the BSD-style license found in the', 30 | ' * LICENSE file in the root directory of this source tree. An additional grant', 31 | ' * of patent rights can be found in the PATENTS file in the same directory.', 32 | ' */', 33 | ].join('\n') + '\n'; 34 | 35 | const paths = { 36 | dist: './dist/', 37 | lib: 'lib', 38 | entry: './index.js', 39 | entryUtils: './utils.js', 40 | src: [ 41 | 'src/**/*.js', 42 | '!src/**/__tests__/**/*.js', 43 | '!src/**/__mocks__/**/*.js', 44 | ], 45 | }; 46 | 47 | const buildDist = function (opts) { 48 | const webpackOpts = { 49 | debug: opts.debug, 50 | module: { 51 | loaders: [{test: /\.js$/, loader: 'babel'}], 52 | }, 53 | output: { 54 | filename: opts.output, 55 | libraryTarget: 'umd', 56 | library: opts.library, 57 | }, 58 | plugins: [ 59 | new webpackStream.webpack.DefinePlugin({ 60 | 'process.env.NODE_ENV': JSON.stringify( 61 | opts.debug ? 'development' : 'production', 62 | ), 63 | }), 64 | ], 65 | }; 66 | if (!opts.debug) { 67 | webpackOpts.plugins.push( 68 | new webpackStream.webpack.optimize.UglifyJsPlugin({ 69 | compress: { 70 | hoist_vars: true, 71 | screw_ie8: true, 72 | warnings: false, 73 | }, 74 | }), 75 | ); 76 | } 77 | return webpackStream(webpackOpts, null, function (err, stats) { 78 | if (err) { 79 | throw new gulpUtil.PluginError('webpack', err); 80 | } 81 | if (stats.compilation.errors.length) { 82 | gulpUtil.log('webpack', '\n' + stats.toString({colors: true})); 83 | } 84 | }); 85 | }; 86 | 87 | gulp.task('clean', function () { 88 | return del([paths.lib, 'Flux.js']); 89 | }); 90 | 91 | gulp.task('lib', function () { 92 | return gulp 93 | .src(paths.src) 94 | .pipe(babel(babelDefaultOptions)) 95 | .pipe(flatten()) 96 | .pipe(gulp.dest(paths.lib)); 97 | }); 98 | 99 | gulp.task('flow', function () { 100 | return gulp 101 | .src(paths.src) 102 | .pipe(flatten()) 103 | .pipe(rename({extname: '.js.flow'})) 104 | .pipe(gulp.dest(paths.lib)); 105 | }); 106 | 107 | gulp.task( 108 | 'dist', 109 | gulp.series('lib', function () { 110 | const distOpts = { 111 | debug: true, 112 | output: 'Flux.js', 113 | library: 'Flux', 114 | }; 115 | return gulp 116 | .src(paths.entry) 117 | .pipe(buildDist(distOpts)) 118 | .pipe( 119 | header(DEVELOPMENT_HEADER, { 120 | version: process.env.npm_package_version, 121 | }), 122 | ) 123 | .pipe(gulp.dest(paths.dist)); 124 | }), 125 | ); 126 | 127 | gulp.task( 128 | 'dist:utils', 129 | gulp.series('lib', function () { 130 | const distOpts = { 131 | debug: true, 132 | output: 'FluxUtils.js', 133 | library: 'FluxUtils', 134 | }; 135 | return gulp 136 | .src(paths.entryUtils) 137 | .pipe(buildDist(distOpts)) 138 | .pipe( 139 | header(DEVELOPMENT_HEADER, { 140 | version: process.env.npm_package_version, 141 | }), 142 | ) 143 | .pipe(gulp.dest(paths.dist)); 144 | }), 145 | ); 146 | 147 | gulp.task( 148 | 'dist:min', 149 | gulp.series('lib', function () { 150 | const distOpts = { 151 | debug: false, 152 | output: 'Flux.min.js', 153 | library: 'Flux', 154 | }; 155 | return gulp 156 | .src(paths.entry) 157 | .pipe(buildDist(distOpts)) 158 | .pipe( 159 | header(PRODUCTION_HEADER, { 160 | version: process.env.npm_package_version, 161 | }), 162 | ) 163 | .pipe(gulp.dest(paths.dist)); 164 | }), 165 | ); 166 | 167 | gulp.task( 168 | 'dist:utils:min', 169 | gulp.series('lib', function () { 170 | const distOpts = { 171 | debug: false, 172 | output: 'FluxUtils.min.js', 173 | library: 'FluxUtils', 174 | }; 175 | return gulp 176 | .src(paths.entryUtils) 177 | .pipe(buildDist(distOpts)) 178 | .pipe( 179 | header(PRODUCTION_HEADER, { 180 | version: process.env.npm_package_version, 181 | }), 182 | ) 183 | .pipe(gulp.dest(paths.dist)); 184 | }), 185 | ); 186 | 187 | gulp.task('build', gulp.series('lib', 'flow', 'dist', 'dist:utils')); 188 | 189 | gulp.task( 190 | 'publish', 191 | gulp.series( 192 | 'clean', 193 | 'flow', 194 | gulp.parallel('dist', 'dist:min', 'dist:utils', 'dist:utils:min'), 195 | ), 196 | ); 197 | 198 | gulp.task('default', gulp.series('build')); 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Flux software 4 | 5 | Copyright (c) Facebook, Inc. and its affiliates. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ The Flux project has been archived and no further changes will be made. We recommend using more sophisticated alternatives like [Redux](http://redux.js.org/), [MobX](https://mobx.js.org/), [Recoil](https://recoiljs.org/), [Zustand](https://github.com/pmndrs/zustand), or [Jotai](https://github.com/pmndrs/jotai). 2 | 3 |

4 | logo 5 |

6 |

7 | Flux 8 |

9 |

10 | An application architecture for React utilizing a unidirectional data flow.
11 | 12 | 13 | Licence Badge 14 | 15 | 16 | Current npm package version. 17 | 18 |

19 | 20 |
21 | 22 | 23 | 24 | ## Getting Started 25 | 26 | Start by looking through the [guides and examples](./examples) on Github. For more resources and API docs check out [facebook.github.io/flux](https://facebookarchive.github.io/flux). 27 | 28 | ## How Flux works 29 | 30 | For more information on how Flux works check out the [Flux Concepts](./examples/flux-concepts) guide, or the [In Depth Overview](https://facebookarchive.github.io/flux/docs/in-depth-overview). 31 | 32 | ## Requirements 33 | 34 | Flux is more of a pattern than a framework, and does not have any hard dependencies. However, we often use [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) as a basis for `Stores` and [React](https://github.com/facebook/react) for our `Views`. The one piece of Flux not readily available elsewhere is the `Dispatcher`. This module, along with some other utilities, is available here to complete your Flux toolbox. 35 | 36 | ## Installing Flux 37 | 38 | Flux is available as a [npm module](https://www.npmjs.org/package/flux), so you can add it to your package.json file or run `npm install flux`. The dispatcher will be available as `Flux.Dispatcher` and can be required like this: 39 | 40 | ```javascript 41 | const Dispatcher = require('flux').Dispatcher; 42 | ``` 43 | 44 | Take a look at the [dispatcher API and some examples](https://facebookarchive.github.io/flux/docs/dispatcher). 45 | 46 | ## Flux Utils 47 | 48 | We have also provided some basic utility classes to help get you started with Flux. These base classes are a solid foundation for a simple Flux application, but they are **not** a feature-complete framework that will handle all use cases. There are many other great Flux frameworks out there if these utilities do not fulfill your needs. 49 | 50 | ```js 51 | import {ReduceStore} from 'flux/utils'; 52 | 53 | class CounterStore extends ReduceStore { 54 | getInitialState(): number { 55 | return 0; 56 | } 57 | 58 | reduce(state: number, action: Object): number { 59 | switch (action.type) { 60 | case 'increment': 61 | return state + 1; 62 | 63 | case 'square': 64 | return state * state; 65 | 66 | default: 67 | return state; 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | Check out the [examples](./examples) and [documentation](https://facebookarchive.github.io/flux/docs/flux-utils) for more information. 74 | 75 | ## Building Flux from a Cloned Repo 76 | 77 | Clone the repo and navigate into the resulting `flux` directory. Then run `npm install`. 78 | 79 | This will run [Gulp](https://gulpjs.com/)-based build tasks automatically and produce the file Flux.js, which you can then require as a module. 80 | 81 | You could then require the Dispatcher like so: 82 | 83 | ```javascript 84 | const Dispatcher = require('path/to/this/directory/Flux').Dispatcher; 85 | ``` 86 | 87 | The build process also produces de-sugared versions of the `Dispatcher` and `invariant` modules in a `lib` directory, and you can require those modules directly, copying them into whatever directory is most convenient for you. The `flux-todomvc` and `flux-chat` example applications both do this. 88 | 89 | ## License 90 | 91 | Flux is BSD-licensed. We also provide an additional patent grant. 92 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux", 3 | "description": "An application architecture based on a unidirectional data flow", 4 | "version": "2.1.1", 5 | "main": "dist/Flux.js", 6 | "license": "https://github.com/facebook/flux/blob/master/LICENSE", 7 | "homepage": "https://facebook.github.io/flux/", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/facebook/flux.git" 11 | }, 12 | "authors": [ 13 | "Facebook", 14 | "Jing Chen ", 15 | "Bill Fisher ", 16 | "Paul O'Shannessy " 17 | ], 18 | "keywords": [ 19 | "flux", 20 | "dispatcher", 21 | "react", 22 | "facebook" 23 | ], 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests", 30 | "docs", 31 | "examples", 32 | "src", 33 | "lib", 34 | "website", 35 | "CONTRIBUTING.md", 36 | "Gulpfile.js", 37 | "index.js", 38 | "package.json" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /dist/Flux.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flux v4.0.4 3 | * 4 | * Copyright (c) Meta Platforms, Inc. and affiliates. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | !function(i,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Flux=t():i.Flux=t()}(this,function(){return function(i){function t(e){if(n[e])return n[e].exports;var s=n[e]={exports:{},id:e,loaded:!1};return i[e].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=i,t.c=n,t.p="",t(0)}([function(i,t,n){i.exports.Dispatcher=n(1)},function(i,t,n){"use strict";function e(i,t,n){return t=s(t),t in i?Object.defineProperty(i,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):i[t]=n,i}function s(i){var t=r(i,"string");return"symbol"==typeof t?t:String(t)}function r(i,t){var n,e;if("object"!=typeof i||null===i)return i;if(n=i[Symbol.toPrimitive],void 0!==n){if(e=n.call(i,t||"default"),"object"!=typeof e)return e;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(i)}var o=n(2),a="ID_",c=function(){function i(){e(this,"_callbacks",void 0),e(this,"_isDispatching",void 0),e(this,"_isHandled",void 0),e(this,"_isPending",void 0),e(this,"_lastID",void 0),e(this,"_pendingPayload",void 0),this._callbacks={},this._isDispatching=!1,this._isHandled={},this._isPending={},this._lastID=1}var t=i.prototype;return t.register=function(i){var t=a+this._lastID++;return this._callbacks[t]=i,t},t.unregister=function(i){this._callbacks[i]?void 0:o(!1),delete this._callbacks[i]},t.waitFor=function(i){var t,n;for(this._isDispatching?void 0:o(!1),t=0;t2?n-2:0),r=2;r ids): void`** 현재 실행한 콜백이 진행되기 전에 특정 콜백을 지연하게 한다. 데이터 변동에 응답하는 콜백에만 사용해야 한다. 20 | 21 | - **`dispatch(object payload): void`** 등록된 모든 콜백에 데이터를 전달한다. 22 | 23 | - **`isDispatching(): boolean`** 이 Dispatcher가 지금 데이터를 전달하고 있는지 확인한다. 24 | 25 | ## 예시 26 | 27 | 가상의 비행 목적지 양식에서 국가를 선택했을 때 기본 도시를 선택하는 예를 보자: 28 | 29 | ```js 30 | var flightDispatcher = new Dispatcher(); 31 | 32 | // 어떤 국가를 선택했는지 계속 추적한다 33 | var CountryStore = {country: null}; 34 | 35 | // 어느 도시를 선택했는지 계속 추적한다 36 | var CityStore = {city: null}; 37 | 38 | // 선택된 도시의 기본 항공료를 계속 추적한다 39 | var FlightPriceStore = {price: null}; 40 | ``` 41 | 42 | 사용자가 선택한 도시를 변경하면 데이터를 전달한다: 43 | 44 | ```js 45 | flightDispatcher.dispatch({ 46 | actionType: 'city-update', 47 | selectedCity: 'paris', 48 | }); 49 | ``` 50 | 51 | 이 데이터 변동은 `CityStore`가 소화한다: 52 | 53 | ```js 54 | flightDispatcher.register(function (payload) { 55 | if (payload.actionType === 'city-update') { 56 | CityStore.city = payload.selectedCity; 57 | } 58 | }); 59 | ``` 60 | 61 | 사용자가 국가를 선택하면 데이터를 전달한다: 62 | 63 | ```js 64 | flightDispatcher.dispatch({ 65 | actionType: 'country-update', 66 | selectedCountry: 'australia', 67 | }); 68 | ``` 69 | 70 | 이 데이터는 두 store에 의해 소화된다: 71 | 72 | ```js 73 | CountryStore.dispatchToken = flightDispatcher.register(function (payload) { 74 | if (payload.actionType === 'country-update') { 75 | CountryStore.country = payload.selectedCountry; 76 | } 77 | }); 78 | ``` 79 | 80 | `CountryStore`가 등록한 콜백을 업데이트 할 때 반환되는 토큰을 참조값으로 저장했다. 이 토큰은 `waitFor()` 에서 사용할 수 있고 `CityStore`가 갱신하는 것보다 먼저 `CountryStore`를 갱신하도록 보장할 수 있다. 81 | 82 | ```js 83 | CityStore.dispatchToken = flightDispatcher.register(function (payload) { 84 | if (payload.actionType === 'country-update') { 85 | // `CountryStore.country`는 업데이트 되지 않는다 86 | flightDispatcher.waitFor([CountryStore.dispatchToken]); 87 | // `CountryStore.country`는 업데이트가 될 수 있음이 보장되었다 88 | 89 | // 새로운 국가의 기본 도시를 선택한다 90 | CityStore.city = getDefaultCityForCountry(CountryStore.country); 91 | } 92 | }); 93 | ``` 94 | 95 | `waitFor()`는 다음과 같이 묶을 수 있다: 96 | 97 | ```js 98 | FlightPriceStore.dispatchToken = flightDispatcher.register(function (payload) { 99 | switch (payload.actionType) { 100 | case 'country-update': 101 | case 'city-update': 102 | flightDispatcher.waitFor([CityStore.dispatchToken]); 103 | FlightPriceStore.price = getFlightPriceStore( 104 | CountryStore.country, 105 | CityStore.city, 106 | ); 107 | break; 108 | } 109 | }); 110 | ``` 111 | 112 | `country-update`는 콜백이 등록된 순서 즉 `CountryStore`, `CityStore`, `FlightPriceStore` 순서로 실행되도록 보장된다. 113 | -------------------------------------------------------------------------------- /docs/Dispatcher.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: dispatcher 3 | title: Dispatcher 4 | --- 5 | 6 | Dispatcher is used to broadcast payloads to registered callbacks. This is different from generic pub-sub systems in two ways: 7 | 8 | - Callbacks are not subscribed to particular events. Every payload is dispatched to every registered callback. 9 | - Callbacks can be deferred in whole or part until other callbacks have been executed. 10 | 11 | Check out [Dispatcher.js](https://github.com/facebookarchive/flux/blob/master/src/Dispatcher.js) for the source code. 12 | 13 | ## API 14 | 15 | - **`register(function callback): string`** Registers a callback to be invoked with every dispatched payload. Returns a token that can be used with `waitFor()`. 16 | 17 | - **`unregister(string id): void`** Removes a callback based on its token. 18 | 19 | - **`waitFor(array ids): void`** Waits for the callbacks specified to be invoked before continuing execution of the current callback. This method should only be used by a callback in response to a dispatched payload. 20 | 21 | - **`dispatch(object payload): void`** Dispatches a payload to all registered callbacks. 22 | 23 | - **`isDispatching(): boolean`** Is this Dispatcher currently dispatching. 24 | 25 | ## Example 26 | 27 | For example, consider this hypothetical flight destination form, which selects a default city when a country is selected: 28 | 29 | ```js 30 | var flightDispatcher = new Dispatcher(); 31 | 32 | // Keeps track of which country is selected 33 | var CountryStore = {country: null}; 34 | 35 | // Keeps track of which city is selected 36 | var CityStore = {city: null}; 37 | 38 | // Keeps track of the base flight price of the selected city 39 | var FlightPriceStore = {price: null}; 40 | ``` 41 | 42 | When a user changes the selected city, we dispatch the payload: 43 | 44 | ```js 45 | flightDispatcher.dispatch({ 46 | actionType: 'city-update', 47 | selectedCity: 'paris', 48 | }); 49 | ``` 50 | 51 | This payload is digested by `CityStore`: 52 | 53 | ```js 54 | flightDispatcher.register(function (payload) { 55 | if (payload.actionType === 'city-update') { 56 | CityStore.city = payload.selectedCity; 57 | } 58 | }); 59 | ``` 60 | 61 | When the user selects a country, we dispatch the payload: 62 | 63 | ```js 64 | flightDispatcher.dispatch({ 65 | actionType: 'country-update', 66 | selectedCountry: 'australia', 67 | }); 68 | ``` 69 | 70 | This payload is digested by both stores: 71 | 72 | ```js 73 | CountryStore.dispatchToken = flightDispatcher.register(function (payload) { 74 | if (payload.actionType === 'country-update') { 75 | CountryStore.country = payload.selectedCountry; 76 | } 77 | }); 78 | ``` 79 | 80 | When the callback to update `CountryStore` is registered, we save a reference to the returned token. Using this token with `waitFor()`, we can guarantee that `CountryStore` is updated before the callback that updates `CityStore` needs to query its data. 81 | 82 | ```js 83 | CityStore.dispatchToken = flightDispatcher.register(function (payload) { 84 | if (payload.actionType === 'country-update') { 85 | // `CountryStore.country` may not be updated. 86 | flightDispatcher.waitFor([CountryStore.dispatchToken]); 87 | // `CountryStore.country` is now guaranteed to be updated. 88 | 89 | // Select the default city for the new country 90 | CityStore.city = getDefaultCityForCountry(CountryStore.country); 91 | } 92 | }); 93 | ``` 94 | 95 | The usage of `waitFor()` can be chained, for example: 96 | 97 | ```js 98 | FlightPriceStore.dispatchToken = flightDispatcher.register(function (payload) { 99 | switch (payload.actionType) { 100 | case 'country-update': 101 | case 'city-update': 102 | flightDispatcher.waitFor([CityStore.dispatchToken]); 103 | FlightPriceStore.price = getFlightPriceStore( 104 | CountryStore.country, 105 | CityStore.city, 106 | ); 107 | break; 108 | } 109 | }); 110 | ``` 111 | 112 | The `country-update` payload will be guaranteed to invoke the stores' registered callbacks in order: `CountryStore`, `CityStore`, then `FlightPriceStore`. 113 | -------------------------------------------------------------------------------- /docs/Flux-Utils.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: flux-utils 3 | title: Flux Utils 4 | --- 5 | 6 | Flux Utils is a set of basic utility classes to help get you started with Flux. These base classes are a solid foundation for a simple Flux application, but they are **not** a feature-complete framework that will handle all use cases. There are many other great Flux frameworks out there if these utilities do not fulfill your needs. 7 | 8 | ## Usage 9 | 10 | There are three main classes exposed in Flux Utils: 11 | 12 | 1. `Store` 13 | 1. `ReduceStore` 14 | 1. `Container` 15 | 16 | These base classes can be imported from `flux/utils` like this: 17 | 18 | ```js 19 | import {ReduceStore} from 'flux/utils'; 20 | 21 | class CounterStore extends ReduceStore { 22 | getInitialState(): number { 23 | return 0; 24 | } 25 | 26 | reduce(state: number, action: Object): number { 27 | switch (action.type) { 28 | case 'increment': 29 | return state + 1; 30 | 31 | case 'square': 32 | return state * state; 33 | 34 | default: 35 | return state; 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | ## Best practices 42 | 43 | There are some best practices we try to follow when using these classes: 44 | 45 | ### Stores 46 | 47 | - Cache data 48 | - Expose public getters to access data (never have public setters) 49 | - Respond to specific actions from the dispatcher 50 | - Always emit a change when their data changes 51 | - Only emit changes during a dispatch 52 | 53 | ### Actions 54 | 55 | Describe a user's action, are not setters. (e.g. `select-page` not `set-page-id`) 56 | 57 | ### Containers 58 | 59 | - Are React components that control a view 60 | - Primary job is to gather information from stores and save it in their state 61 | - Have no `props` and no UI logic 62 | 63 | ### Views 64 | 65 | - Are React components that are controlled by a container 66 | - Have all of the UI and rendering logic 67 | - Receive all information and callbacks as props 68 | 69 | ## API 70 | 71 | ### `Store` 72 | 73 | #### `constructor(dispatcher: Dispatcher)` 74 | 75 | Constructs and registers an instance of this store with the given dispatcher. 76 | 77 | #### `addListener(callback: Function): {remove: Function}` 78 | 79 | Adds a listener to the store, when the store changes the given callback will be called. A token is returned that can be used to remove the listener. Calling the `remove()` function on the returned token will remove the listener. 80 | 81 | #### `getDispatcher(): Dispatcher` 82 | 83 | Returns the dispatcher this store is registered with. 84 | 85 | #### `getDispatchToken(): DispatchToken` 86 | 87 | Returns the dispatch token that the dispatcher recognizes this store by. Can be used to `waitFor()` this store. 88 | 89 | #### `hasChanged(): boolean` 90 | 91 | Ask if a store has changed during the current dispatch. Can only be invoked while dispatching. This can be used for constructing derived stores that depend on data from other stores. 92 | 93 | #### `__emitChange(): void` 94 | 95 | Emit an event notifying all listeners that this store has changed. This can only be invoked when dispatching. Changes are de-duplicated and resolved at the end of this store's `__onDispatch` function. 96 | 97 | #### `onDispatch(payload: Object): void` 98 | 99 | Subclasses must override this method. This is how the store receives actions from the dispatcher. All state mutation logic must be done during this method. 100 | 101 | --- 102 | 103 | ### `ReduceStore` 104 | 105 | This class extends the base `Store`. 106 | 107 | #### `getState(): T` 108 | 109 | Getter that exposes the entire state of this store. If your state is not immutable you should override this and not expose state directly. 110 | 111 | #### `getInitialState(): T` 112 | 113 | Constructs the initial state for this store. This is called once during construction of the store. 114 | 115 | #### `reduce(state: T, action: Object): T` 116 | 117 | Reduces the current state, and an action to the new state of this store. All subclasses must implement this method. This method should be pure and have no side-effects. 118 | 119 | #### `areEqual(one: T, two: T): boolean` 120 | 121 | Checks if two versions of state are the same. You do not need to override this if your state is immutable. 122 | 123 | #### Doesn't Need to Emit a Change 124 | 125 | Note that any store that extends `ReduceStore` does not need to manually emit changes in `reduce()` (you still can if you want to though). The state is compared before and after each dispatch and changes are emitted automatically. If you need to control this behavior (perhaps because your state is mutable) override `areEqual()`. 126 | 127 | --- 128 | 129 | ### `Container` 130 | 131 | #### `create(base: ReactClass, options: ?Object): ReactClass` 132 | 133 | Create is used to transform a react class into a container that updates its state when relevant stores change. The provided base class must have static methods `getStores()` and `calculateState()`. 134 | 135 | ```js 136 | import {Component} from 'react'; 137 | import {Container} from 'flux/utils'; 138 | 139 | class CounterContainer extends Component { 140 | static getStores() { 141 | return [CounterStore]; 142 | } 143 | 144 | static calculateState(prevState) { 145 | return { 146 | counter: CounterStore.getState(), 147 | }; 148 | } 149 | 150 | render() { 151 | return ; 152 | } 153 | } 154 | 155 | const container = Container.create(CounterContainer); 156 | ``` 157 | 158 | Additional options may be provided when creating your container in order to control certain behaviors. 159 | 160 | - **Containers are pure** - By default containers are pure, meaning they will not re-render when their props and state do not change (as determined by `shallowEquals()`). To disable this behavior pass options `{pure: false}` as the second argument to `create()`. 161 | 162 | - **Containers cannot access props** - By default containers are not able to access any props. This is both for performance reasons, and to ensure that containers are re-usable and props do not have to be threaded throughout a component tree. There are some valid situations in which you need to determine your state based on both props and a store's state. In those situations pass options `{withProps: true}` as the second argument to `create()`. This will expose the components props as the second argument to `calculateState()`. 163 | 164 | If you are unable to utilize react classes most of this functionality is also mirrored in a mixin. `import {Mixin} from 'flux/utils';` 165 | 166 | ## Using Flux with React Hooks 167 | 168 | React 16.8 introduced [Hooks](https://reactjs.org/docs/hooks-intro.html). Much of the functionality of Flux and Flux Utils can be reproduced using [useContext](https://reactjs.org/docs/hooks-reference.html#usecontext) & [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer). 169 | 170 | ## Existing Projects with `Store`/`ReduceStore` 171 | 172 | If you have existing projects that need to continue using Flux Util's Stores, you can use the [flux-hooks](https://github.com/Fieldscope/flux-hooks) package. Access the store using useFluxStore which provides an API similar to [Container](#container)'s calculateState. 173 | -------------------------------------------------------------------------------- /docs/Overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | layout: docs 5 | category: Quick Start 6 | next: in-depth-overview 7 | --- 8 | 9 | In order to get started check out the [overview and guides](https://github.com/facebookarchive/flux/tree/master/examples) maintained on GitHub, or continue on to the in-depth overview, which explores in more detail how the pieces of the Flux architecture work together. 10 | 11 | > Note: The in-depth overview was formerly the normal overview landing page but was replaced with the more terse and up-to-date guides on GitHub that are linked above. 12 | -------------------------------------------------------------------------------- /docs/Videos.ko-KR.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: videos-ko-KR 3 | title: 비디오 4 | --- 5 | 6 | ### Rethinking Web App Development at Facebook - Facebook F8 Conference 2014 7 | 8 |
9 | 10 |
11 | 12 | ### React and Flux: Building Applications with a Unidirectional Data Flow - Forward JS 2014 13 | 14 |
15 | 16 |
17 | 18 | Facebook 엔지니어 [Bill Fisher](http://twitter.com/fisherwebdev)와 [Jing Chen](http://twitter.com/jingc)이 Flux와 React에 대해서, 이 애플리케이션 아키텍처를 단일 데이터 흐름과 함께 어떻게 사용해 많은 코드를 깔끔하게 만들었는지에 대한 영상이다. 19 | 20 | ### React and Flux - HTML5DevConf 2014 21 | 22 |
23 | 24 |
25 | 26 | [Bill Fisher](http://twitter.com/fisherwebdev)가 MVC 패턴과 Flux가 어떻게 다른지에 대해 설명하고 웹 애플리케이션을 더 빠르고 관리하기 편한 코드로 깔끔하게 정리할 수 있었는가에 대한 영상이다. 27 | -------------------------------------------------------------------------------- /docs/Videos.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: videos 3 | title: Videos 4 | --- 5 | 6 | ### Rethinking Web App Development at Facebook - Facebook F8 Conference 2014 7 | 8 |
9 | 10 |
11 | 12 | ### React and Flux: Building Applications with a Unidirectional Data Flow - Forward JS 2014 13 | 14 |
15 | 16 |
17 | 18 | Facebook engineers [Bill Fisher](http://twitter.com/fisherwebdev) and [Jing Chen](http://twitter.com/jingc) talk about Flux and React, and how using an application architecture with a unidirectional data flow cleans up a lot of their code. 19 | 20 | ### React and Flux - HTML5DevConf 2014 21 | 22 |
23 | 24 |
25 | 26 | [Bill Fisher](http://twitter.com/fisherwebdev) discusses how Flux is different from MVC patterns, and how it can clean up web applications to make them faster and more maintainable with cleaner code. 27 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples that should help get you started with Flux. They are listed in the order you should complete them. 4 | 5 | ## [./flux-concepts](./flux-concepts) - Basic concepts (no code) 6 | 7 | These are the important high-level concepts and principles you should know about when writing applications that use Flux. 8 | 9 | ## [./flux-todomvc](./flux-todomvc) - Start coding here 10 | 11 | This example is where you should start. It walks you through creating the classic [TodoMVC](http://todomvc.com/) application using a simple Flux implementation. 12 | 13 | ## [./flux-jest](./flux-jest) - Unit Testing Stores 14 | 15 | Being able to unit test stores is critical. This example shows you how to write tests for the TodoMVC stores we created in an earlier example. 16 | 17 | ## [./flux-flow](./flux-flow) - Static typing 18 | 19 | This is a very simple example that highlights how to set up Flux and [Flow](https://flowtype.org/) in the same project. Flow is a static-type checking tool that will help catch errors in your code statically. It complements Flux well since it supports refining the action based on a type string. 20 | 21 | ## [./flux-logging](./flux-logging) - Add logging to Flux apps 22 | 23 | Taking advantage of the fact that a store gets every action makes it easy to add logging to a Flux application. Check out this quick example where we add a logger store to the TodoMVC app we created in an earlier example. 24 | 25 | ## [./flux-jest-container](./flux-jest-container) - Unit Testing Containers 26 | 27 | Testing the container logic that connects stores to views can be tricky. This example shows you how to create some utilities to help mock out store data in order to write these kinds of tests effectively. 28 | 29 | ## [./flux-async](./flux-async) - Flux with async requests 30 | 31 | This is an advanced example. It pulls a lot of the concepts from previous examples into a single application. This implements TodoMVC where the data is persisted and requested through a simple server. The server simulates delays and errors. In the example we will handle things like optimistic updates, loading states, and failing API requests. 32 | -------------------------------------------------------------------------------- /examples/flux-async/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "syntax-class-properties", 6 | "syntax-flow", 7 | "syntax-jsx", 8 | "syntax-object-rest-spread", 9 | "syntax-trailing-function-commas", 10 | "transform-class-properties", 11 | "transform-flow-strip-types", 12 | "transform-object-rest-spread", 13 | "transform-react-jsx", 14 | "transform-regenerator", 15 | "transform-runtime" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/flux-async/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | .*/node_modules/.* 4 | 5 | [libs] 6 | 7 | flow 8 | 9 | [options] 10 | 11 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 12 | experimental.const_params=true 13 | -------------------------------------------------------------------------------- /examples/flux-async/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | data.json 3 | lib 4 | -------------------------------------------------------------------------------- /examples/flux-async/README.md: -------------------------------------------------------------------------------- 1 | # flux-async 2 | 3 | This is an advanced example. It pulls a lot of the concepts from previous examples into a single application. This implements TodoMVC where the data is persisted and requested through a simple server. The server simulates delays and errors. In the example we will handle things like optimistic updates, loading states, and failing API requests. 4 | 5 | # Disclaimer 6 | 7 | This is just one way to handle asynchronous actions. It is not the only way, and may not be the best way. This is simply how we deal with asynchronous actions most commonly at Facebook. 8 | 9 | # Usage 10 | 11 | ```bash 12 | cd path/to/flux-async 13 | npm install 14 | 15 | # builds client code when it changes (npm run build to build just once) 16 | npm run watch 17 | 18 | # starts server, code is at ./server/app.js 19 | npm run start 20 | 21 | # this will run flow and make sure there are no type errors in the code 22 | flow 23 | 24 | # navigate to: http://localhost:3000/home/ to use the todo application 25 | ``` 26 | 27 | # Code Concepts 28 | 29 | ## DataManager 30 | 31 | In this example we introduce DataManagers. This is how to communicate with a server in Flux. A DataManager translates input arguments into an action. The resulting action should always be dispatched asynchronously. This allows time for an API request in before dispatching the action. 32 | 33 | DataManagers should only be called from stores. 34 | 35 | ## LoadObject 36 | 37 | `LoadObject`s are immutable objects that represent a piece of data that has to be requested from the server. It can have any combination of: `value`, `error`, and `operation`. We can represent many things, some examples: 38 | 39 | A piece of data we just requested: (`null`, `null`, `'LOADING'`) 40 | 41 | Data that has had an error: (`null`, `Invalid Request`, `'NONE'`) 42 | 43 | Data that we are retrying: (`null`, `'Invalid Request'`, `'LOADING'`) 44 | 45 | Normal piece of data: (`'Some data'`, `null`, `'NONE'`) 46 | 47 | Data that we are updating: (`'Some data'`, `null`, `'UPDATING'`) 48 | 49 | And many others states your data might be in. There are also several supporting objects to `LoadObject`. 50 | 51 | ### LoadObjectMap 52 | 53 | `LoadObjectMap` is an immutable map that has `LoadObject` values. When a key is requested that does not exist instead of returning null it will return an empty `LoadObject`, this helps prevent the need for null-checking when we can use `LoadObject`'s to represent the absence of a value. Additionally on construction you can provide a function that will be called with keys that are requested that do not yet have a value. This makes it easy to figure out when to load data from the server. Generally you dispatch an action with the missing keys so that a store will start loading them. 54 | 55 | ### LoadObjectState 56 | 57 | `LoadObjectState` provides similar functionality to `LoadObjectMap` but for when your state is a single `LoadObject`. 58 | 59 | ## FakeID 60 | 61 | ID's are always created on the server, so in order to optimistically update we need a service to generate some fake IDs. We just use a really simple utility that also has a mechanism for testing if IDs are fake. Without this it would be hard to provide optimistic updates and properly correlate responses with data that we are already rendering. 62 | 63 | ## ListStore 64 | 65 | List stores are a kind of store responsible for keeping track of a list of IDs. They only store IDs. They should never store lists of the objects themselves, that should always be in another store that keeps track of a map of those objects. This separates the concerns of simply loading the object data, and figuring out in which order it should render, dealing with sorting, filtering, etc. This makes it possible for the ordering to change without having to re-render views that use the unchanged data. 66 | 67 | ## Flow actions 68 | 69 | Instead of using action-creators we dispatch our actions manually. This only works because we have strongly typed every possible action in `TodoActions`. This means that Flow will have an error if we ever try to dispatch something that is invalid. This also allows us to refine actions in our reduce functions and make sure we are accessing valid properties based on what kind of action we are dealing with. 70 | -------------------------------------------------------------------------------- /examples/flux-async/flow/flux-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import type {Dispatcher} from 'flux'; 13 | import type React from 'react'; 14 | 15 | type DispatchToken = string; 16 | 17 | type ContainerOptions = { 18 | pure?: ?boolean, 19 | withProps?: ?boolean, 20 | withContext?: ?boolean, 21 | }; 22 | 23 | declare module 'flux/utils' { 24 | declare class Store { 25 | constructor(dispatcher: Dispatcher): void; 26 | addListener(callback: (eventType?: string) => void): {remove: () => void}; 27 | getDispatcher(): Dispatcher; 28 | getDispatchToken(): DispatchToken; 29 | hasChanged(): boolean; 30 | } 31 | 32 | declare class ReduceStore extends Store { 33 | getState(): TState; 34 | getInitialState(): TState; 35 | reduce(state: TState, action: TPayload): TState; 36 | areEqual(one: TState, two: TState): boolean; 37 | } 38 | 39 | // This isn't really a class, just a simple object. Not sure how to put that 40 | // in declare module. 41 | declare class Container { 42 | static create( 43 | base: React.Element, 44 | options?: ?ContainerOptions, 45 | ): React.Class; 46 | 47 | static createFunctional( 48 | viewFn: (props: State) => React.Element, 49 | getStores: (props?: ?Props, context?: any) => Array>, 50 | calculateState: ( 51 | prevState?: ?State, 52 | props?: ?Props, 53 | context?: any, 54 | ) => State, 55 | options?: ContainerOptions, 56 | ): React.Class; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/flux-async/flow/flux.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | type DispatchToken = string; 13 | 14 | declare module 'flux' { 15 | declare class Dispatcher { 16 | register(callback: (payload: TPayload) => void): DispatchToken; 17 | unregister(id: DispatchToken): void; 18 | waitFor(ids: Array): void; 19 | dispatch(payload: TPayload): void; 20 | isDispatching(): boolean; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/flux-async/flow/immutable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | // Not sure why this is necessary, but here we directly alias the 13 | // flow typedefs inside of immutable to the module "immutable". 14 | 15 | import type Immutable from 'immutable/dist/immutable.js.flow'; 16 | 17 | declare module 'immutable' { 18 | declare var exports: Immutable; 19 | } 20 | -------------------------------------------------------------------------------- /examples/flux-async/flow/misc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | // Don't really care about these modules. 13 | 14 | declare module 'xhr' { 15 | declare var exports: any; 16 | } 17 | 18 | declare module 'classnames' { 19 | declare var exports: any; 20 | } 21 | -------------------------------------------------------------------------------- /examples/flux-async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flux • TodoMVC 6 | 7 | 8 | 9 |
10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/flux-async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-todomvc", 3 | "version": "1.0.0", 4 | "description": "Basic shell application for the Flux architecture", 5 | "repository": "https://github.com/facebook/flux", 6 | "author": "Kyle Davis", 7 | "main": "bundle.js", 8 | "scripts": { 9 | "build": "webpack ./src/root.js ./bundle.js", 10 | "watch": "webpack ./src/root.js ./bundle.js --watch", 11 | "start": "node ./server/app.js", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "dependencies": { 15 | "classnames": "^2.2.3", 16 | "escape-html": "^1.0.3", 17 | "express": "^4.14.0", 18 | "flux": "../../.", 19 | "immutable": "^3.8.0", 20 | "react": "^15.0.2", 21 | "react-dom": "^15.0.1", 22 | "xhr": "^2.2.2" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.7.6", 26 | "babel-loader": "^6.2.4", 27 | "babel-plugin-syntax-async-functions": "^6.5.0", 28 | "babel-plugin-syntax-class-properties": "^6.13.0", 29 | "babel-plugin-syntax-flow": "^6.5.0", 30 | "babel-plugin-syntax-jsx": "^6.5.0", 31 | "babel-plugin-syntax-object-rest-spread": "^6.5.0", 32 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0", 33 | "babel-plugin-transform-class-properties": "^6.11.5", 34 | "babel-plugin-transform-flow-strip-types": "^6.5.0", 35 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 36 | "babel-plugin-transform-react-jsx": "^6.7.5", 37 | "babel-plugin-transform-regenerator": "^6.5.2", 38 | "babel-plugin-transform-runtime": "^6.5.2", 39 | "babel-preset-es2015": "^6.5.0", 40 | "webpack": "^1.13.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/flux-async/src/TodoActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type Todo from './records/Todo'; 15 | 16 | export type Action = 17 | // UI Actions for updating the draft. 18 | | { 19 | type: 'draft/set', 20 | value: string, 21 | } 22 | 23 | // Dealing with todo ids. 24 | | { 25 | type: 'ids/start-load', 26 | } 27 | | { 28 | type: 'ids/loaded', 29 | ids: Array, 30 | } 31 | | { 32 | type: 'ids/load-error', 33 | error: Error, 34 | } 35 | 36 | // Creating todos. 37 | | { 38 | type: 'todo/start-create', 39 | value: string, 40 | fakeID: string, 41 | } 42 | | { 43 | type: 'todo/created', 44 | todo: Todo, 45 | fakeID: string, 46 | } 47 | | { 48 | type: 'todo/create-error', 49 | error: Error, 50 | fakeID: string, 51 | } 52 | 53 | // Deleting todos. 54 | | { 55 | type: 'todos/start-delete', 56 | ids: Array, 57 | } 58 | | { 59 | type: 'todos/deleted', 60 | ids: Array, 61 | } 62 | | { 63 | type: 'todos/delete-error', 64 | error: Error, 65 | ids: Array, 66 | } 67 | 68 | // Reading todos. 69 | | { 70 | type: 'todos/start-load', 71 | ids: Array, 72 | } 73 | | { 74 | type: 'todos/loaded', 75 | todos: Array, 76 | } 77 | | { 78 | type: 'todos/load-error', 79 | ids: Array, 80 | error: Error, 81 | } 82 | 83 | // Updating todos. 84 | | { 85 | type: 'todos/start-update', 86 | ids: Array, 87 | texts: Array, 88 | completes: Array, 89 | } 90 | | { 91 | type: 'todos/updated', 92 | todos: Array, 93 | } 94 | | { 95 | type: 'todos/update-error', 96 | originalTodos: Array, 97 | error: Error, 98 | }; 99 | -------------------------------------------------------------------------------- /examples/flux-async/src/TodoDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type {Action} from './TodoActions'; 15 | 16 | import {Dispatcher} from 'flux'; 17 | 18 | const dispatcher: Dispatcher = new Dispatcher(); 19 | 20 | export default dispatcher; 21 | -------------------------------------------------------------------------------- /examples/flux-async/src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import AppView from '../views/AppView'; 15 | import {Container} from 'flux/utils'; 16 | import FakeID from '../utils/FakeID'; 17 | import Todo from '../records/Todo'; 18 | import TodoDispatcher from '../TodoDispatcher'; 19 | import TodoDraftStore from '../stores/TodoDraftStore'; 20 | import TodoListStore from '../stores/TodoListStore'; 21 | import TodoStore from '../stores/TodoStore'; 22 | 23 | function getStores() { 24 | return [TodoDraftStore, TodoListStore, TodoStore]; 25 | } 26 | 27 | function getState() { 28 | const todos = TodoStore.getState(); 29 | const ids = TodoListStore.getState(); 30 | 31 | // Figure out which ids are being deleted. 32 | const deletedIDs = new Set(); 33 | todos.forEach((lo, id) => { 34 | if (lo.isDeleting()) { 35 | deletedIDs.add(id); 36 | } 37 | }); 38 | 39 | return { 40 | draft: TodoDraftStore.getState(), 41 | 42 | // Then optimistically remove todos that are being deleted. 43 | ids: ids.map((list) => list.filter((id) => !deletedIDs.has(id))), 44 | todos: todos.filter((_, id) => !deletedIDs.has(id)), 45 | 46 | onDelete, 47 | onDraftCreate, 48 | onDraftSet, 49 | onRetry, 50 | onUpdateTodos, 51 | }; 52 | } 53 | 54 | function onDelete(ids: Array) { 55 | TodoDispatcher.dispatch({ 56 | type: 'todos/start-delete', 57 | ids, 58 | }); 59 | } 60 | 61 | function onDraftCreate(value: string) { 62 | if (value && value.trim()) { 63 | TodoDispatcher.dispatch({ 64 | type: 'todo/start-create', 65 | fakeID: FakeID.next(), 66 | value, 67 | }); 68 | } 69 | } 70 | 71 | function onDraftSet(value: string) { 72 | TodoDispatcher.dispatch({ 73 | type: 'draft/set', 74 | value, 75 | }); 76 | } 77 | 78 | function onRetry(todo: Todo) { 79 | if (FakeID.isFake(todo.id)) { 80 | // If it's a fakeID we had an error creating it, try again. 81 | TodoDispatcher.dispatch({ 82 | type: 'todo/start-create', 83 | value: todo.text, 84 | fakeID: todo.id, 85 | }); 86 | } else { 87 | // If it's a real ID we had an error loading it, try again. 88 | TodoDispatcher.dispatch({ 89 | type: 'todos/start-load', 90 | ids: [todo.id], 91 | }); 92 | } 93 | } 94 | 95 | function onUpdateTodos(todos: Array) { 96 | TodoDispatcher.dispatch({ 97 | type: 'todos/start-update', 98 | ids: todos.map((todo) => todo.id), 99 | texts: todos.map((todo) => todo.text), 100 | completes: todos.map((todo) => todo.complete), 101 | }); 102 | } 103 | 104 | export default Container.createFunctional(AppView, getStores, getState); 105 | -------------------------------------------------------------------------------- /examples/flux-async/src/data_managers/TodoAPI.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import xhr from 'xhr'; 13 | 14 | // The port is hard coded in the server too. If you change it make sure to 15 | // update it there as well. 16 | 17 | const PORT = 3000; 18 | const PREFIX = 'http://localhost:' + PORT; 19 | 20 | type TodoObject = { 21 | id: string, 22 | text: string, 23 | complete: boolean, 24 | }; 25 | 26 | // Using some flow trickery we can strongly type our requests! We don't verify 27 | // this at runtime though, so it's not actually sound. But we're all good if 28 | // we trust the API implementation :) 29 | declare class TodoAPI { 30 | static get(uri: '/ids'): Promise>; 31 | 32 | static get(uri: '/todo', data: {id: string}): Promise; 33 | 34 | static get( 35 | uri: '/todos', 36 | data: {ids: Array}, 37 | ): Promise>; 38 | 39 | static post(uri: '/todo/create', data: {text: string}): Promise; 40 | 41 | static post( 42 | uri: '/todos/create', 43 | data: {texts: Array}, 44 | ): Promise>; 45 | 46 | static post( 47 | uri: '/todo/update', 48 | data: {id: string, text: string, complete: boolean}, 49 | ): Promise; 50 | 51 | static post( 52 | uri: '/todos/update', 53 | data: {ids: Array, texts: Array, completes: Array}, 54 | ): Promise>; 55 | 56 | static post(uri: '/todo/delete', data: {id: string}): Promise; 57 | 58 | static post(uri: '/todos/delete', data: {ids: Array}): Promise; 59 | } 60 | 61 | // $FlowExpectedError: Intentional rebinding of variable. 62 | const TodoAPI = { 63 | get(uri, data) { 64 | return promiseXHR('get', uri, data); 65 | }, 66 | 67 | post(uri, data) { 68 | return promiseXHR('post', uri, data); 69 | }, 70 | }; 71 | 72 | /** 73 | * This is a simple wrapper around XHR that let's us use promises. Not very 74 | * advanced but works with our server's API. 75 | */ 76 | function promiseXHR(method: 'get' | 'post', uri, data) { 77 | const query = []; 78 | if (data) { 79 | Object.keys(data).forEach((key) => { 80 | query.push(key + '=' + JSON.stringify(data[key])); 81 | }); 82 | } 83 | const suffix = query.length > 0 ? '?' + query.join('&') : ''; 84 | return new Promise((resolve, reject) => { 85 | xhr[method](PREFIX + uri + suffix, (err, res, body) => { 86 | if (err) { 87 | reject(err); 88 | return; 89 | } 90 | if (res.statusCode !== 200) { 91 | reject(new Error('[status: ' + res.statusCode + '] ' + res.body)); 92 | return; 93 | } 94 | 95 | // It's fine if the body is empty. 96 | if (body == null) { 97 | resolve(undefined); 98 | } 99 | 100 | // Not okay if the body isn't a string though. 101 | if (typeof body !== 'string') { 102 | reject(new Error('Responses from server must be JSON strings.')); 103 | } 104 | 105 | try { 106 | resolve(JSON.parse(body)); 107 | } catch (e) { 108 | reject(new Error('Responses from server must be JSON strings.')); 109 | } 110 | }); 111 | }); 112 | } 113 | 114 | export default TodoAPI; 115 | -------------------------------------------------------------------------------- /examples/flux-async/src/data_managers/TodoDataManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import Todo from '../records/Todo'; 15 | import TodoAPI from './TodoAPI'; 16 | import TodoDispatcher from '../TodoDispatcher'; 17 | 18 | const TodoDataManager = { 19 | create(text: string, fakeID: string) { 20 | TodoAPI.post('/todo/create', {text}) 21 | .then((rawTodo) => { 22 | TodoDispatcher.dispatch({ 23 | type: 'todo/created', 24 | todo: new Todo(rawTodo), 25 | fakeID, 26 | }); 27 | }) 28 | .catch((error) => { 29 | TodoDispatcher.dispatch({ 30 | type: 'todo/create-error', 31 | error, 32 | fakeID, 33 | }); 34 | }); 35 | }, 36 | 37 | deleteTodos(ids: Array) { 38 | TodoAPI.post('/todos/delete', {ids}) 39 | .then(() => { 40 | TodoDispatcher.dispatch({ 41 | type: 'todos/deleted', 42 | ids, 43 | }); 44 | }) 45 | .catch((error) => { 46 | TodoDispatcher.dispatch({ 47 | type: 'todos/delete-error', 48 | error, 49 | ids, 50 | }); 51 | }); 52 | }, 53 | 54 | updateTodos( 55 | ids: Array, 56 | texts: Array, 57 | completes: Array, 58 | originalTodos: Array, 59 | ) { 60 | TodoAPI.post('/todos/update', {ids, texts, completes}) 61 | .then((rawTodos) => { 62 | TodoDispatcher.dispatch({ 63 | type: 'todos/updated', 64 | todos: rawTodos.map((rawTodo) => new Todo(rawTodo)), 65 | }); 66 | }) 67 | .catch((error) => { 68 | TodoDispatcher.dispatch({ 69 | type: 'todos/update-error', 70 | originalTodos, 71 | error, 72 | }); 73 | }); 74 | }, 75 | 76 | loadIDs() { 77 | TodoAPI.get('/ids') 78 | .then((ids) => { 79 | TodoDispatcher.dispatch({ 80 | type: 'ids/loaded', 81 | ids, 82 | }); 83 | }) 84 | .catch((error) => { 85 | TodoDispatcher.dispatch({ 86 | type: 'ids/load-error', 87 | error, 88 | }); 89 | }); 90 | }, 91 | 92 | loadTodos(ids: Array) { 93 | TodoAPI.get('/todos', {ids}) 94 | .then((rawTodos) => { 95 | TodoDispatcher.dispatch({ 96 | type: 'todos/loaded', 97 | todos: rawTodos.map((rawTodo) => new Todo(rawTodo)), 98 | }); 99 | }) 100 | .catch((error) => { 101 | TodoDispatcher.dispatch({ 102 | type: 'todos/load-error', 103 | ids, 104 | error, 105 | }); 106 | }); 107 | }, 108 | }; 109 | 110 | export default TodoDataManager; 111 | -------------------------------------------------------------------------------- /examples/flux-async/src/load_object/LoadObjectMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import Immutable from 'immutable'; 13 | import LoadObject from './LoadObject'; 14 | 15 | /** 16 | * LoadObjectMap is a structure that mimics Immutable.Map where the values are 17 | * LoadObjects. It has a few features that make working with LoadObjects easier. 18 | * 19 | * - `get()` always returns a value, if the value is not found in the 20 | * underlying map an empty load object is returned, and... 21 | * - all keys that are requested in the same frame that do not exist in the 22 | * underlying map will be passed to the `loadAll()` function given to the 23 | * constructor of the map. This allows them to then be "loaded". 24 | * 25 | * There is some important timing issues to be aware of regarding `loadAll()`. 26 | * 27 | * - `loadAll()` is called in the frame AFTER calls to `get()` that has a 28 | * cache miss. 29 | * - `loadAll()` must SYNCHRONOUSLY change all of the given keys to a state 30 | * in the cache such that they will no longer be considered a cache miss. 31 | * Generally this means setting them to loading. Dispatching an action is 32 | * synchronous and generally what should be done in `loadAll()`. 33 | */ 34 | class LoadObjectMap { 35 | _data: Immutable.Map>; 36 | _loadAll: (key: Set) => void; 37 | _shouldLoad: (lo: LoadObject) => boolean; 38 | 39 | // Mutable state on this map so we don't accidently load something many times. 40 | _preventLoadsForThisFrame: Set; 41 | _clearPreventLoadsForThisFrame: mixed; 42 | 43 | constructor( 44 | loadAll: (keys: Set) => void, 45 | shouldLoad?: (lo: LoadObject) => boolean, 46 | ) { 47 | this._data = Immutable.Map(); 48 | this._loadAll = loadAll; 49 | this._shouldLoad = shouldLoad || ((lo) => lo.isEmpty()); 50 | this._preventLoadsForThisFrame = new Set(); 51 | this._clearPreventLoadsForThisFrame = null; 52 | } 53 | 54 | // Some trickery so that we always return a load object, and call the provided 55 | // load function when appropriate. 56 | get(key: K): LoadObject { 57 | const lo = this._data.has(key) ? this._data.get(key) : LoadObject.empty(); 58 | if (!this._preventLoadsForThisFrame.has(key) && this._shouldLoad(lo)) { 59 | // This must be done asynchronously to avoid nested dispatches. 60 | this._preventLoadsForThisFrame.add(key); 61 | if (!this._clearPreventLoadsForThisFrame) { 62 | this._clearPreventLoadsForThisFrame = setTimeout(() => { 63 | this._loadAll(this._preventLoadsForThisFrame); 64 | this._preventLoadsForThisFrame = new Set(); 65 | this._clearPreventLoadsForThisFrame = null; 66 | }, 0); 67 | } 68 | } 69 | return lo; 70 | } 71 | 72 | getKeys(): Array { 73 | return Array.from(this._data.keys()); 74 | } 75 | 76 | getValues(): Array> { 77 | return Array.from(this._data.values()); 78 | } 79 | 80 | every(fn: (lo: LoadObject, key: K) => boolean): boolean { 81 | return this._data.every(fn); 82 | } 83 | 84 | some(fn: (lo: LoadObject, key: K) => boolean): boolean { 85 | return this._data.some(fn); 86 | } 87 | 88 | forEach(fn: (lo: LoadObject, key: K) => any): void { 89 | this._data.forEach(fn); 90 | } 91 | 92 | get size(): number { 93 | return this._data.size; 94 | } 95 | 96 | ////////// A selection of mutation functions found on Immutable.Map ////////// 97 | 98 | delete(key: K): LoadObjectMap { 99 | return this._mutate(() => this._data.delete(key)); 100 | } 101 | 102 | set(key: K, lo: LoadObject): LoadObjectMap { 103 | return this._mutate(() => this._data.set(key, lo)); 104 | } 105 | 106 | merge(map: Iterable<[K, LoadObject]>): LoadObjectMap { 107 | return this._mutate(() => this._data.merge(map)); 108 | } 109 | 110 | filter(fn: (lo: LoadObject, key: K) => boolean): LoadObjectMap { 111 | return this._mutate(() => this._data.filter(fn)); 112 | } 113 | 114 | update( 115 | key: K, 116 | fn: (lo: LoadObject) => LoadObject, 117 | ): LoadObjectMap { 118 | return this._mutate(() => this._data.update(key, fn)); 119 | } 120 | 121 | _mutate(fn: () => Immutable.Map>): LoadObjectMap { 122 | const nextData = fn(); 123 | if (nextData === this._data) { 124 | return this; 125 | } 126 | const nextThis = new LoadObjectMap(this._loadAll, this._shouldLoad); 127 | nextThis._data = nextData; 128 | return nextThis; 129 | } 130 | } 131 | 132 | export default LoadObjectMap; 133 | -------------------------------------------------------------------------------- /examples/flux-async/src/load_object/LoadObjectState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import LoadObject from './LoadObject'; 15 | 16 | /** 17 | * This helps work with state that can be represented by a single load object. 18 | * Similar to LoadObjectMap. 19 | */ 20 | class LoadObjectState { 21 | _data: LoadObject; 22 | _load: () => void; 23 | _shouldLoad: (lo: LoadObject) => boolean; 24 | 25 | _preventLoadsForThisFrame: boolean; 26 | _clearPreventLoadsForThisFrame: mixed; 27 | 28 | constructor(load: () => void, shouldLoad?: (lo: LoadObject) => boolean) { 29 | this._data = LoadObject.empty(); 30 | this._load = load; 31 | this._shouldLoad = shouldLoad || ((lo) => lo.isEmpty()); 32 | this._preventLoadsForThisFrame = false; 33 | this._clearPreventLoadsForThisFrame = null; 34 | } 35 | 36 | getLoadObject(): LoadObject { 37 | if (!this._preventLoadsForThisFrame && this._shouldLoad(this._data)) { 38 | this._preventLoadsForThisFrame = true; 39 | this._clearPreventLoadsForThisFrame = setTimeout(() => { 40 | this._load(); 41 | this._preventLoadsForThisFrame = false; 42 | this._clearPreventLoadsForThisFrame = null; 43 | }, 0); 44 | } 45 | return this._data; 46 | } 47 | 48 | setLoadObject(lo: LoadObject): LoadObjectState { 49 | if (lo === this._data) { 50 | return this; 51 | } 52 | const next = new LoadObjectState(this._load, this._shouldLoad); 53 | next._data = lo; 54 | return next; 55 | } 56 | 57 | map(fn: (value: V) => V): LoadObjectState { 58 | const lo = this.getLoadObject().map(fn); 59 | if (lo === this._data) { 60 | return this; 61 | } 62 | const next = new LoadObjectState(this._load, this._shouldLoad); 63 | next._data = lo; 64 | return next; 65 | } 66 | } 67 | 68 | export default LoadObjectState; 69 | -------------------------------------------------------------------------------- /examples/flux-async/src/records/Todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import Immutable from 'immutable'; 15 | 16 | declare class Todo { 17 | id: string; 18 | complete: boolean; 19 | text: string; 20 | 21 | constructor(data: { 22 | id: string, 23 | complete: boolean, 24 | text: string, 25 | }): void; 26 | 27 | set(key: 'id', value: string): Todo; 28 | set(key: 'complete', value: boolean): Todo; 29 | set(key: 'text', value: string): Todo; 30 | } 31 | 32 | // $FlowExpectedError: Intentional rebinding for flow. 33 | const Todo = Immutable.Record({ 34 | id: '', 35 | complete: false, 36 | text: '', 37 | }); 38 | 39 | export default Todo; 40 | -------------------------------------------------------------------------------- /examples/flux-async/src/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import AppContainer from './containers/AppContainer'; 15 | import React from 'react'; 16 | import ReactDOM from 'react-dom'; 17 | 18 | import TodoLoggerStore from './stores/TodoLoggerStore'; 19 | 20 | ReactDOM.render(, document.getElementById('todoapp')); 21 | -------------------------------------------------------------------------------- /examples/flux-async/src/stores/TodoDraftStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type {Action} from '../TodoActions'; 15 | 16 | import {ReduceStore} from 'flux/utils'; 17 | import TodoDataManager from '../data_managers/TodoDataManager'; 18 | import TodoDispatcher from '../TodoDispatcher'; 19 | 20 | type State = string; 21 | 22 | class TodoDraftStore extends ReduceStore { 23 | constructor() { 24 | super(TodoDispatcher); 25 | } 26 | 27 | getInitialState(): State { 28 | return ''; 29 | } 30 | 31 | reduce(state: State, action: Action): State { 32 | switch (action.type) { 33 | case 'todo/start-create': 34 | return ''; 35 | 36 | case 'draft/set': 37 | return action.value; 38 | 39 | default: 40 | return state; 41 | } 42 | } 43 | } 44 | 45 | export default new TodoDraftStore(); 46 | -------------------------------------------------------------------------------- /examples/flux-async/src/stores/TodoListStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type {Action} from '../TodoActions'; 15 | 16 | import FakeID from '../utils/FakeID'; 17 | import Immutable from 'immutable'; 18 | import LoadObject from '../load_object/LoadObject'; 19 | import LoadObjectState from '../load_object/LoadObjectState'; 20 | import {ReduceStore} from 'flux/utils'; 21 | import TodoDataManager from '../data_managers/TodoDataManager'; 22 | import TodoDispatcher from '../TodoDispatcher'; 23 | 24 | type State = LoadObjectState>; 25 | 26 | class TodoListStore extends ReduceStore { 27 | constructor() { 28 | super(TodoDispatcher); 29 | } 30 | 31 | getInitialState(): State { 32 | return new LoadObjectState(() => 33 | TodoDispatcher.dispatch({ 34 | type: 'ids/start-load', 35 | }), 36 | ); 37 | } 38 | 39 | reduce(state: State, action: Action): State { 40 | switch (action.type) { 41 | ///// Loading ///// 42 | 43 | case 'ids/start-load': 44 | TodoDataManager.loadIDs(); 45 | return state.setLoadObject(LoadObject.loading()); 46 | 47 | case 'ids/loaded': 48 | return state.setLoadObject( 49 | LoadObject.withValue(Immutable.List(action.ids)), 50 | ); 51 | 52 | case 'ids/load-error': 53 | return state.setLoadObject(LoadObject.withError(action.error)); 54 | 55 | ///// Creating ///// 56 | 57 | case 'todo/start-create': 58 | return state.map((list) => 59 | list.contains(action.fakeID) ? list : list.push(action.fakeID), 60 | ); 61 | 62 | case 'todo/created': 63 | // This replaces the fake ID we added optimistically with the real id. 64 | return state.map((list) => 65 | list.map((id) => (id === action.fakeID ? action.todo.id : id)), 66 | ); 67 | 68 | case 'todo/create-error': 69 | // We don't need to remove the id on an error. It will be updated to 70 | // have an error and the user can explicitly remove it. 71 | return state; 72 | 73 | ///// Deleting ///// 74 | 75 | case 'todos/start-delete': 76 | // Optimistically remove any fake ids. 77 | const fakeIDs = action.ids.filter((id) => FakeID.isFake(id)); 78 | const fakeIDSet = new Set(fakeIDs); 79 | return state.map((list) => list.filter((id) => !fakeIDSet.has(id))); 80 | 81 | case 'todos/deleted': 82 | const idSet = new Set(action.ids); 83 | return state.map((list) => list.filter((id) => !idSet.has(id))); 84 | 85 | case 'todo/delete-error': 86 | // No need to remove any ids when the delete fails, user will have to 87 | // retry in order to delete the items. 88 | return state; 89 | 90 | default: 91 | return state; 92 | } 93 | } 94 | } 95 | 96 | export default new TodoListStore(); 97 | -------------------------------------------------------------------------------- /examples/flux-async/src/stores/TodoLoggerStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type {Action} from '../TodoActions'; 15 | 16 | import {ReduceStore} from 'flux/utils'; 17 | import TodoDispatcher from '../TodoDispatcher'; 18 | 19 | const DEBUG = false; 20 | 21 | function log(action: Action): void { 22 | if (DEBUG) { 23 | console.info(action); 24 | } 25 | } 26 | 27 | class TodoLoggerStore extends ReduceStore { 28 | constructor() { 29 | super(TodoDispatcher); 30 | } 31 | 32 | getInitialState(): null { 33 | return null; 34 | } 35 | 36 | reduce(state: null, action: Action): null { 37 | log(action); 38 | return state; 39 | } 40 | } 41 | 42 | export default new TodoLoggerStore(); 43 | -------------------------------------------------------------------------------- /examples/flux-async/src/stores/TodoStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type {Action} from '../TodoActions'; 15 | 16 | import FakeID from '../utils/FakeID'; 17 | import Immutable from 'immutable'; 18 | import LoadObject from '../load_object/LoadObject'; 19 | import LoadObjectMap from '../load_object/LoadObjectMap'; 20 | import {ReduceStore} from 'flux/utils'; 21 | import TodoDataManager from '../data_managers/TodoDataManager'; 22 | import TodoDispatcher from '../TodoDispatcher'; 23 | import Todo from '../records/Todo'; 24 | 25 | type State = LoadObjectMap; 26 | 27 | class TodoStore extends ReduceStore { 28 | constructor() { 29 | super(TodoDispatcher); 30 | } 31 | 32 | getInitialState(): State { 33 | return new LoadObjectMap((keys) => 34 | TodoDispatcher.dispatch({ 35 | type: 'todos/start-load', 36 | ids: Array.from(keys), 37 | }), 38 | ); 39 | } 40 | 41 | reduce(state: State, action: Action): State { 42 | switch (action.type) { 43 | ///// Creating ///// 44 | 45 | case 'todo/start-create': 46 | TodoDataManager.create(action.value, action.fakeID); 47 | // Optimistically create the todo with the fakeID. 48 | return state.set( 49 | action.fakeID, 50 | LoadObject.creating().setValue( 51 | new Todo({ 52 | id: action.fakeID, 53 | text: action.value, 54 | complete: false, 55 | }), 56 | ), 57 | ); 58 | 59 | case 'todo/created': 60 | // Replace the optimistic todo with the real data. 61 | return state 62 | .delete(action.fakeID) 63 | .set(action.todo.id, LoadObject.withValue(action.todo)); 64 | 65 | case 'todo/create-error': 66 | // Clear the operation and save the error when there is one. 67 | return state.update(action.fakeID, (lo) => 68 | lo.setError(action.error).done(), 69 | ); 70 | 71 | ///// Loading ///// 72 | 73 | case 'todos/start-load': 74 | TodoDataManager.loadTodos(action.ids); 75 | return state.merge(action.ids.map((id) => [id, LoadObject.loading()])); 76 | 77 | case 'todos/loaded': 78 | return state.merge( 79 | action.todos.map((todo) => [todo.id, LoadObject.withValue(todo)]), 80 | ); 81 | 82 | case 'todos/load-error': 83 | return state.merge( 84 | action.ids.map((id) => [id, LoadObject.withError(action.error)]), 85 | ); 86 | 87 | ///// Updating ///// 88 | 89 | case 'todos/start-update': { 90 | let nextState = state; 91 | // We need to save the original todos so we know what to revert to 92 | // in case of a failure. 93 | const originalTodos = []; 94 | action.ids.forEach((id, i) => { 95 | const todoLo = state.get(id); 96 | if (!todoLo.hasValue()) { 97 | return; 98 | } 99 | originalTodos.push(todoLo.getValueEnforcing()); 100 | nextState = nextState.update(id, (lo) => 101 | lo.updating().map((todo) => { 102 | return todo 103 | .set('text', action.texts[i]) 104 | .set('complete', action.completes[i]); 105 | }), 106 | ); 107 | }); 108 | TodoDataManager.updateTodos( 109 | action.ids, 110 | action.texts, 111 | action.completes, 112 | originalTodos, 113 | ); 114 | return nextState; 115 | } 116 | 117 | case 'todos/updated': 118 | return state.merge( 119 | action.todos.map((todo) => [todo.id, LoadObject.withValue(todo)]), 120 | ); 121 | 122 | case 'todos/update-error': 123 | return state.merge( 124 | action.originalTodos.map((todo) => [ 125 | todo.id, 126 | LoadObject.withValue(todo).setError(action.error), 127 | ]), 128 | ); 129 | 130 | ///// Deleting ///// 131 | 132 | case 'todos/start-delete': { 133 | let nextState = state; 134 | const realIDs = []; 135 | action.ids.forEach((id) => { 136 | if (FakeID.isFake(id)) { 137 | nextState = nextState.delete(id); 138 | } else { 139 | realIDs.push(id); 140 | nextState = nextState.update(id, (lo) => lo.deleting()); 141 | } 142 | }); 143 | TodoDataManager.deleteTodos(realIDs); 144 | return nextState; 145 | } 146 | 147 | case 'todos/deleted': 148 | const idSet = new Set(action.ids); 149 | return state.filter((_, id) => !idSet.has(id)); 150 | 151 | case 'todos/delete-error': { 152 | let nextState = state; 153 | action.ids.forEach((id) => { 154 | nextState = nextState.update(id, (lo) => 155 | lo.setError(action.error).done(), 156 | ); 157 | }); 158 | return nextState; 159 | } 160 | 161 | default: 162 | return state; 163 | } 164 | } 165 | } 166 | 167 | export default new TodoStore(); 168 | -------------------------------------------------------------------------------- /examples/flux-async/src/utils/FakeID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | // We include some randomness so that it's not easy to create these id's through 15 | // some other module. This should be the only place for testing fake ids. 16 | const RANDOM_INT = Math.round(Math.random() * 10000); 17 | const PREFIX = 'FAKE_' + RANDOM_INT + '_'; 18 | 19 | let count = 0; 20 | 21 | /** 22 | * This generated fake ids we can use to optimistically update the UI. 23 | */ 24 | const FakeID = { 25 | next(): string { 26 | return PREFIX + ++count; 27 | }, 28 | 29 | isFake(id: string): boolean { 30 | return id.startsWith(PREFIX); 31 | }, 32 | }; 33 | 34 | export default FakeID; 35 | -------------------------------------------------------------------------------- /examples/flux-async/todomvc-common/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "0.1.9", 4 | "homepage": "https://github.com/tastejs/todomvc-common", 5 | "_release": "0.1.9", 6 | "_resolution": { 7 | "type": "version", 8 | "tag": "v0.1.9", 9 | "commit": "7dd61b0ebf56c020e719a69444442cc7ae7242ff" 10 | }, 11 | "_source": "git://github.com/tastejs/todomvc-common.git", 12 | "_target": "~0.1.4", 13 | "_originalSource": "todomvc-common" 14 | } 15 | -------------------------------------------------------------------------------- /examples/flux-async/todomvc-common/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/flux/4ee8c50865c357e005cb40ea03cdd403533aad26/examples/flux-async/todomvc-common/bg.png -------------------------------------------------------------------------------- /examples/flux-async/todomvc-common/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "0.1.9" 4 | } 5 | -------------------------------------------------------------------------------- /examples/flux-async/todomvc-common/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-common 2 | 3 | > Bower component for some common utilities we use in every app 4 | 5 | ## License 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /examples/flux-async/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | module: { 10 | loaders: [{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/flux-concepts/README.md: -------------------------------------------------------------------------------- 1 | # flux-concepts 2 | 3 | These are the important high-level concepts and principles you should know about when writing applications that use Flux. 4 | 5 | ## Overview 6 | 7 | Flux is a pattern for managing data flow in your application. The most important concept is that data flows in one direction. As we go through this guide we'll talk about the different pieces of a Flux application and show how they form unidirectional cycles that data can flow through. 8 | 9 | ## Flux Parts 10 | 11 | - Dispatcher 12 | - Store 13 | - Action 14 | - View 15 | 16 | ## Dispatcher 17 | 18 | The dispatcher receives actions and dispatches them to stores that have registered with the dispatcher. **Every store will receive every action.** There should be only one singleton dispatcher in each application. 19 | 20 | Example: 21 | 22 | 1. User types in title for a todo and hits enter. 23 | 2. The view captures this event and **dispatches** an "add-todo" action containing the title of the todo. 24 | 3. **Every store** will then receive this action. 25 | 26 | ## Store 27 | 28 | A store is what holds the data of an application. Stores will register with the application's dispatcher so that they can receive actions. **The data in a store must only be mutated by responding to an action.** There should not be any public setters on a store, only getters. Stores decide what actions they want to respond to. **Every time a store's data changes it must emit a "change" event.** There should be many stores in each application. 29 | 30 | Examples: 31 | 32 | 1. Store receives an "add-todo" action. 33 | 2. It decides it is relevant and adds the todo to the list of things that need to be done today. 34 | 3. The store updates its data and then emits a "change" event. 35 | 36 | ## Actions 37 | 38 | Actions define the internal API of your application. They capture the ways in which anything might interact with your application. They are simple objects that have a "type" field and some data. 39 | 40 | Actions should be semantic and descriptive of the action taking place. They should not describe implementation details of that action. Use "delete-user" rather than breaking it up into "delete-user-id", "clear-user-data", "refresh-credentials" (or however the process works). Remember that all stores will receive the action and can know they need to clear the data or refresh credentials by handling the same "delete-user" action. 41 | 42 | Examples: 43 | 44 | 1. When a user clicks "delete" on a completed todo a single "delete-todo" action is dispatched: 45 | 46 | ``` 47 | { 48 | type: 'delete-todo', 49 | todoID: '1234', 50 | } 51 | ``` 52 | 53 | ## Views 54 | 55 | Data from stores is displayed in views. Views can use whatever framework you want (In most examples here we will use React). **When a view uses data from a store it must also subscribe to change events from that store.** Then when the store emits a change the view can get the new data and re-render. If a component ever uses a store and does not subscribe to it then there is likely a subtle bug waiting to be found. Actions are typically dispatched from views as the user interacts with parts of the application's interface. 56 | 57 | Example: 58 | 59 | 1. The main view subscribes to the TodoStore. 60 | 2. It accesses a list of the Todos and renders them in a readable format for the user to interact with. 61 | 3. When a user types in the title of a new Todo and hits enter the view tells the Dispatcher to dispatch an action. 62 | 4. All stores receive the dispatched action. 63 | 5. The TodoStore handles the action and adds another Todo to its internal data structure, then emits a "change" event. 64 | 6. The main view is listening for the "change" event. It gets the event, gets new data from the TodoStore, and then re-renders the list of Todos in the user interface. 65 | 66 | ## Flow of data 67 | 68 | We can piece the parts of Flux above into a diagram describing how data flows through the system. 69 | 70 | 1. Views send actions to the dispatcher. 71 | 2. The dispatcher sends actions to every store. 72 | 3. Stores send data to the views. 73 | 74 | - _(Different phrasing: Views get data from the stores.)_ 75 | 76 | ![Data flow within Flux application](./flux-simple-f8-diagram-with-client-action-1300w.png) 77 | 78 | _(There is also another node in the diagram accounting for actions that do not originate from views, which is common)_ 79 | 80 | ## Next steps 81 | 82 | There are also plenty of great summaries of the Flux architecture online, feel free to search around for more resources if you need to. 83 | 84 | Otherwise you can start coding with the [flux-todomvc](../flux-todomvc) example, or head back to check out the full list of [example topics](..). 85 | -------------------------------------------------------------------------------- /examples/flux-concepts/flux-simple-f8-diagram-with-client-action-1300w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/flux/4ee8c50865c357e005cb40ea03cdd403533aad26/examples/flux-concepts/flux-simple-f8-diagram-with-client-action-1300w.png -------------------------------------------------------------------------------- /examples/flux-flow/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "syntax-flow", 6 | "syntax-jsx", 7 | "syntax-object-rest-spread", 8 | "syntax-trailing-function-commas", 9 | "transform-flow-strip-types", 10 | "transform-object-rest-spread", 11 | "transform-react-jsx", 12 | "transform-regenerator", 13 | "transform-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/flux-flow/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | .*/node_modules/.* 4 | 5 | [libs] 6 | 7 | flow 8 | 9 | [options] 10 | 11 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 12 | experimental.const_params=true 13 | -------------------------------------------------------------------------------- /examples/flux-flow/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | lib 3 | -------------------------------------------------------------------------------- /examples/flux-flow/README.md: -------------------------------------------------------------------------------- 1 | # flux-flow 2 | 3 | This is a very simple example that highlights how to set up Flux and [Flow](https://flowtype.org/) in the same project. Flow is a static-type checking tool that will help catch errors in your code statically. It complements Flux well since it supports refining the action based on a type string. 4 | 5 | # Usage 6 | 7 | ```bash 8 | cd path/to/flux-flow 9 | flow 10 | ``` 11 | 12 | _Note: Since flow is a static analysis tool you don't actually need to build anything for this example._ 13 | 14 | # Differences from flux-shell 15 | 16 | The primary differences from the `flux-shell` project that are necessary to use Flow are: 17 | 18 | 1. Copy the `Flux` library definitions from [`./flow`](./flow) 19 | 2. Create a [`.flowconfig`](./.flowconfig) file with the correct options 20 | 21 | After that you can navigate to your project and run `flow` in the terminal. 22 | 23 | # Kinds of errors caught by flow 24 | 25 | - Check out: [`__flowtests__/App-flowtest.js`](./src/__flowtests__/App-flowtest.js) 26 | - Or remove the `suppress_comment` line from [`.flowconfig`](./.flowconfig) 27 | 28 | # Const params 29 | 30 | You may notice that `.flowconfig` has `experimental.const_params=true` in it. This is generally necessary to make refinements on the payload stronger. If for other reasons it's not possible to use this option in your project you may need to do this to work around Flow refinement issues: 31 | 32 | ``` 33 | function reduce(state: State, _action: Action): State { 34 | const action = _action; 35 | ... 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /examples/flux-flow/flow/flux-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import type {Dispatcher} from 'flux'; 13 | import type React from 'react'; 14 | 15 | type DispatchToken = string; 16 | 17 | type ContainerOptions = { 18 | pure?: ?boolean, 19 | withProps?: ?boolean, 20 | withContext?: ?boolean, 21 | }; 22 | 23 | declare module 'flux/utils' { 24 | declare class Store { 25 | constructor(dispatcher: Dispatcher): void; 26 | addListener(callback: (eventType?: string) => void): {remove: () => void}; 27 | getDispatcher(): Dispatcher; 28 | getDispatchToken(): DispatchToken; 29 | hasChanged(): boolean; 30 | } 31 | 32 | declare class ReduceStore extends Store { 33 | getState(): TState; 34 | getInitialState(): TState; 35 | reduce(state: TState, action: TPayload): TState; 36 | areEqual(one: TState, two: TState): boolean; 37 | } 38 | 39 | // This isn't really a class, just a simple object. Not sure how to put that 40 | // in declare module. 41 | declare class Container { 42 | static create( 43 | base: React.Element, 44 | options?: ?ContainerOptions, 45 | ): React.Class; 46 | 47 | static createFunctional( 48 | viewFn: (props: State) => React.Element, 49 | getStores: (props?: ?Props, context?: any) => Array>, 50 | calculateState: ( 51 | prevState?: ?State, 52 | props?: ?Props, 53 | context?: any, 54 | ) => State, 55 | options?: ContainerOptions, 56 | ): React.Class; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/flux-flow/flow/flux.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | type DispatchToken = string; 13 | 14 | declare module 'flux' { 15 | declare class Dispatcher { 16 | register(callback: (payload: TPayload) => void): DispatchToken; 17 | unregister(id: DispatchToken): void; 18 | waitFor(ids: Array): void; 19 | dispatch(payload: TPayload): void; 20 | isDispatching(): boolean; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/flux-flow/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flux • Shell 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/flux-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-flow", 3 | "version": "1.0.0", 4 | "description": "Basic shell application for the Flux architecture", 5 | "repository": "https://github.com/facebook/flux", 6 | "author": "Kyle Davis", 7 | "main": "bundle.js", 8 | "scripts": { 9 | "build": "webpack ./src/root.js ./bundle.js", 10 | "watch": "webpack ./src/root.js ./bundle.js --watch", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "dependencies": { 14 | "classnames": "^2.2.3", 15 | "flux": "../../.", 16 | "immutable": "^3.8.0", 17 | "react": "^15.0.2", 18 | "react-dom": "^15.0.1" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.7.6", 22 | "babel-loader": "^6.2.4", 23 | "babel-plugin-syntax-async-functions": "^6.5.0", 24 | "babel-plugin-syntax-flow": "^6.5.0", 25 | "babel-plugin-syntax-jsx": "^6.5.0", 26 | "babel-plugin-syntax-object-rest-spread": "^6.5.0", 27 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0", 28 | "babel-plugin-transform-flow-strip-types": "^6.5.0", 29 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 30 | "babel-plugin-transform-react-jsx": "^6.7.5", 31 | "babel-plugin-transform-regenerator": "^6.5.2", 32 | "babel-plugin-transform-runtime": "^6.5.2", 33 | "babel-preset-es2015": "^6.5.0", 34 | "webpack": "^1.13.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/flux-flow/src/AppActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | export type Action = 15 | | { 16 | type: 'foo', 17 | foo: string, 18 | } 19 | | { 20 | type: 'bar', 21 | bar: string, 22 | }; 23 | -------------------------------------------------------------------------------- /examples/flux-flow/src/AppContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import AppDispatcher from './AppDispatcher'; 15 | import AppStore from './AppStore'; 16 | import AppView from './AppView'; 17 | import {Container} from 'flux/utils'; 18 | 19 | function getStores() { 20 | return [AppStore]; 21 | } 22 | 23 | function getState() { 24 | return { 25 | value: AppStore.getState(), 26 | 27 | // $FlowExpectedError: Cannot dispatch an incorrectly formed action. 28 | onFooChange: () => 29 | AppDispatcher.dispatch({ 30 | type: 'foo', 31 | bar: 'Hello Bar!', 32 | }), 33 | }; 34 | } 35 | 36 | export default Container.createFunctional(AppView, getStores, getState); 37 | -------------------------------------------------------------------------------- /examples/flux-flow/src/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type {Action} from './AppActions'; 15 | 16 | import {Dispatcher} from 'flux'; 17 | 18 | const dispatcher: Dispatcher = new Dispatcher(); 19 | 20 | export default dispatcher; 21 | -------------------------------------------------------------------------------- /examples/flux-flow/src/AppStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type {Action} from './AppActions'; 15 | 16 | import {ReduceStore} from 'flux/utils'; 17 | import AppDispatcher from './AppDispatcher'; 18 | 19 | class AppStore extends ReduceStore { 20 | constructor() { 21 | super(AppDispatcher); 22 | } 23 | 24 | getInitialState(): string { 25 | return 'Hello World!'; 26 | } 27 | 28 | reduce(state: string, action: Action): string { 29 | switch (action.type) { 30 | case 'foo': 31 | return action.foo; 32 | 33 | // This is an error that should be caught by flow. 34 | case 'bar': 35 | // $FlowExpectedError: action type 'bar' does not have a 'foo' property. 36 | return action.foo; 37 | 38 | // This case is okay though, bar actions have a bar property. 39 | case 'bar': 40 | return action.bar; 41 | 42 | default: 43 | return state; 44 | } 45 | } 46 | } 47 | 48 | export default new AppStore(); 49 | -------------------------------------------------------------------------------- /examples/flux-flow/src/AppView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import React from 'react'; 15 | 16 | export type Props = {value: string}; 17 | 18 | function AppView(props: Props): React.Element<*> { 19 | return
{props.value}
; 20 | } 21 | 22 | export default AppView; 23 | -------------------------------------------------------------------------------- /examples/flux-flow/src/__flowtests__/App-flowtest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | // This file will violate certain parts of flow definitions and expect errors 15 | // using the annotation "FlowExpectedError". This works because if the error 16 | // suppressing comment no longer suppresses an error that becomes a flow error. 17 | // This means regressions where we expect an error and there is not an error 18 | // will be caught. 19 | 20 | import AppDispatcher from '../AppDispatcher'; 21 | import AppStore from '../AppStore'; 22 | import React from 'react'; 23 | import {Container} from 'flux/utils'; 24 | 25 | /** 26 | * Tests a simple dispatch with an invalid payload. 27 | */ 28 | function t1() { 29 | // $FlowExpectedError: Cannot dispatch an incorrectly formed action. 30 | AppDispatcher.dispatch({ 31 | type: 'foo', 32 | bar: 'Hello Bar!', 33 | }); 34 | } 35 | 36 | /** 37 | * Tests creating a container with state that doesn't match props of the view. 38 | */ 39 | function t2(): React.Element<*> { 40 | function MyView(props: {value: string}) { 41 | return
{props.value}
; 42 | } 43 | 44 | const MyContainer = Container.createFunctional( 45 | MyView, 46 | () => [AppStore], 47 | // $FlowExpectedError: Incorrect shape for state. 48 | () => ({notValue: AppStore.getState()}), 49 | ); 50 | 51 | return ; 52 | } 53 | -------------------------------------------------------------------------------- /examples/flux-flow/src/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import AppContainer from './AppContainer'; 15 | import React from 'react'; 16 | import ReactDOM from 'react-dom'; 17 | 18 | ReactDOM.render(, document.getElementById('root')); 19 | -------------------------------------------------------------------------------- /examples/flux-flow/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | module: { 10 | loaders: [{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/flux-jest-container/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "syntax-flow", 6 | "syntax-jsx", 7 | "syntax-object-rest-spread", 8 | "syntax-trailing-function-commas", 9 | "transform-flow-strip-types", 10 | "transform-object-rest-spread", 11 | "transform-react-jsx", 12 | "transform-regenerator", 13 | "transform-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/flux-jest-container/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | lib 3 | -------------------------------------------------------------------------------- /examples/flux-jest-container/README.md: -------------------------------------------------------------------------------- 1 | # flux-jest-container 2 | 3 | Testing the container logic that connects stores to views can be tricky. This example shows you how to create some utilities to help mock out store data in order to write these kinds of tests effectively. 4 | 5 | # Usage 6 | 7 | ```bash 8 | cd path/to/flux-jest 9 | npm install 10 | npm run test 11 | ``` 12 | 13 | # Differences from flux-shell 14 | 15 | The primary differences from the `flux-shell` project that are necessary to use Jest are: 16 | 17 | 1. Run `npm install --save-dev jest babel-jest react-test-renderer` 18 | 2. Add `"jest"` property to [`package.json`](./package.json) 19 | 3. Update `"test"` key in the `"scripts"` section of [`package.json`](./package.json) 20 | 21 | Setting it up this way will reuse the [`.babelrc`](./babelrc) file we already have set up so we can keep using the same syntax in our test files! 22 | 23 | # Check out the tests 24 | 25 | Check out the test file and make sure to read through the comments in `beforeEach()` to get a sense of how and why we set up the test file the way we did. 26 | 27 | In this test we make use of `toMatchSnapshot()` which will serialize the state of a react component to a snapshot file. Then if any changes are made that cause the snapshot to change the test will fail unless we explicitly decide to update the snapshot. This helps prevent any unexpected UI regressions. Read more about snapshot testing in the blog post here: [React Tree Snapshot Testing](https://facebook.github.io/jest/blog/2016/07/27/jest-14.html). 28 | 29 | Important files for this example: 30 | 31 | - [`AppContainer-test.js`](./src/__tests__/AppContainer-test.js) 32 | - [`AppContainer-test.js.snap`](./src/__tests__/__snapshots__/AppContainer-test.js.snap) 33 | - [`AppContainer.js`](../flux-todomvc/src/containers/AppContainer.js) 34 | -------------------------------------------------------------------------------- /examples/flux-jest-container/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-jest", 3 | "version": "1.0.0", 4 | "description": "Basic shell application for the Flux architecture", 5 | "repository": "https://github.com/facebook/flux", 6 | "author": "Kyle Davis", 7 | "main": "no_main.js", 8 | "scripts": { 9 | "test": "jest" 10 | }, 11 | "dependencies": { 12 | "classnames": "^2.2.3", 13 | "flux": "../../.", 14 | "immutable": "^3.8.0", 15 | "react": "^15.3.1", 16 | "react-dom": "^15.3.1" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.7.6", 20 | "babel-jest": "^15.0.0", 21 | "babel-plugin-syntax-async-functions": "^6.5.0", 22 | "babel-plugin-syntax-flow": "^6.5.0", 23 | "babel-plugin-syntax-jsx": "^6.5.0", 24 | "babel-plugin-syntax-object-rest-spread": "^6.5.0", 25 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0", 26 | "babel-plugin-transform-flow-strip-types": "^6.5.0", 27 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 28 | "babel-plugin-transform-react-jsx": "^6.7.5", 29 | "babel-plugin-transform-regenerator": "^6.5.2", 30 | "babel-plugin-transform-runtime": "^6.5.2", 31 | "babel-preset-es2015": "^6.5.0", 32 | "jest": "^15.1.1", 33 | "react-test-renderer": "^15.3.1" 34 | }, 35 | "jest": { 36 | "modulePathIgnorePatterns": [ 37 | "/node_modules/" 38 | ], 39 | "preprocessorIgnorePatterns": [ 40 | "/node_modules/" 41 | ], 42 | "rootDir": "", 43 | "testPathDirs": [ 44 | "/src" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/flux-jest-container/src/__tests__/AppContainer-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import AppContainer from '../../../flux-todomvc/src/containers/AppContainer'; 13 | import Counter from '../../../flux-todomvc/src/data/Counter'; 14 | import Immutable from 'immutable'; 15 | import React from 'react'; 16 | import Todo from '../../../flux-todomvc/src/data/Todo'; 17 | import TodoDraftStore from '../../../flux-todomvc/src/data/TodoDraftStore'; 18 | import TodoEditStore from '../../../flux-todomvc/src/data/TodoEditStore'; 19 | import TodoStore from '../../../flux-todomvc/src/data/TodoStore'; 20 | 21 | import renderer from 'react-test-renderer'; 22 | 23 | describe('AppContainer', function () { 24 | // Set up functions to help mock the data in each store that is used by 25 | // our container. If there are child containers you may also need to mock that 26 | // data as well. We do not need to mock all of the callbacks because we are 27 | // just testing how it renders at a particular point in time. If you also 28 | // wanted to test how the callbacks behave you would need to make these helper 29 | // functions actually inject their data into the stores rather than 30 | // overridding each store's getState() function. As your application grows 31 | // you should move these into test utils that can be reused across tests for 32 | // many containers. This should prevent the need for any code to be in the 33 | // beforeEach() function of your container tests. 34 | beforeEach(function () { 35 | let editStore = ''; 36 | this.setEditID = (id) => (editStore = id); 37 | 38 | let draftStore = ''; 39 | this.setDraftText = (text) => (draftStore = text); 40 | 41 | let todoStore = Immutable.OrderedMap(); 42 | this.setTodos = (todos) => { 43 | todos.forEach((todo) => { 44 | const id = Counter.increment(); 45 | todoStore = todoStore.set( 46 | id, 47 | new Todo({id, text: todo.text, complete: !!todo.complete}), 48 | ); 49 | }); 50 | }; 51 | 52 | // Because of how TodoStore is set up it's not easy to get access to ids of 53 | // todos. This will get the id of a particular todo based on the index it 54 | // was added to state in. 55 | this.id = (index) => { 56 | if (todoStore.size <= index) { 57 | throw new Error( 58 | 'Requested id for an index that is larger than the size of the ' + 59 | 'current state.', 60 | ); 61 | } 62 | return Array.from(todoStore.keys())[index]; 63 | }; 64 | 65 | // Override all the get state's to read from our fake data. 66 | TodoDraftStore.getState = () => draftStore; 67 | TodoEditStore.getState = () => editStore; 68 | TodoStore.getState = () => todoStore; 69 | 70 | // Simple helper so tests read easier. 71 | this.render = () => renderer.create().toJSON(); 72 | }); 73 | 74 | ///// Begin tests ///// 75 | 76 | it('renders some todos', function () { 77 | this.setTodos([ 78 | {text: 'Hello', complete: false}, 79 | {text: 'World!', complete: false}, 80 | // Uncomment this to see what it looks like when a snapshot doesn't match. 81 | // {text: 'Some changes', complete: false}, 82 | ]); 83 | 84 | expect(this.render()).toMatchSnapshot(); 85 | }); 86 | 87 | it('renders with no todos', function () { 88 | expect(this.render()).toMatchSnapshot(); 89 | }); 90 | 91 | it('renders todos that are complete', function () { 92 | this.setTodos([ 93 | // Try changing complete to "true" for test0 to see how snapshot changes. 94 | {text: 'test0', complete: false}, 95 | {text: 'test1', complete: true}, 96 | {text: 'test2', complete: true}, 97 | {text: 'test3', complete: false}, 98 | ]); 99 | 100 | expect(this.render()).toMatchSnapshot(); 101 | }); 102 | 103 | it('can edit task that is not complete', function () { 104 | this.setTodos([ 105 | {text: 'test0', complete: false}, 106 | {text: 'test1', complete: true}, 107 | {text: 'test2', complete: true}, 108 | {text: 'test3', complete: false}, 109 | ]); 110 | 111 | this.setEditID(this.id(0)); 112 | 113 | expect(this.render()).toMatchSnapshot(); 114 | }); 115 | 116 | it('can edit task that is complete', function () { 117 | this.setTodos([ 118 | {text: 'test0', complete: false}, 119 | {text: 'test1', complete: true}, 120 | {text: 'test2', complete: true}, 121 | {text: 'test3', complete: false}, 122 | ]); 123 | 124 | this.setEditID(this.id(1)); 125 | 126 | expect(this.render()).toMatchSnapshot(); 127 | }); 128 | 129 | it('renders draft with todos', function () { 130 | this.setTodos([{text: 'test0', complete: false}]); 131 | 132 | this.setDraftText('test1'); 133 | 134 | expect(this.render()).toMatchSnapshot(); 135 | }); 136 | 137 | it('renders draft with no todos', function () { 138 | this.setDraftText('test0'); 139 | 140 | expect(this.render()).toMatchSnapshot(); 141 | }); 142 | 143 | it('renders draft with todos while editing', function () { 144 | this.setTodos([ 145 | {text: 'test0', complete: false}, 146 | {text: 'test1', complete: false}, 147 | ]); 148 | 149 | this.setEditID(this.id(1)); 150 | 151 | this.setDraftText('test1 edit'); 152 | 153 | expect(this.render()).toMatchSnapshot(); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /examples/flux-jest/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "syntax-flow", 6 | "syntax-jsx", 7 | "syntax-object-rest-spread", 8 | "syntax-trailing-function-commas", 9 | "transform-flow-strip-types", 10 | "transform-object-rest-spread", 11 | "transform-react-jsx", 12 | "transform-regenerator", 13 | "transform-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/flux-jest/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | lib 3 | -------------------------------------------------------------------------------- /examples/flux-jest/README.md: -------------------------------------------------------------------------------- 1 | # flux-jest 2 | 3 | Being able to unit test stores is critical. This example shows you how to write tests for the TodoMVC stores we created in an earlier example. 4 | 5 | # Usage 6 | 7 | ```bash 8 | cd path/to/flux-jest 9 | npm install 10 | npm run test 11 | ``` 12 | 13 | # Differences from flux-shell 14 | 15 | The primary differences from the `flux-shell` project that are necessary to use Jest are: 16 | 17 | 1. Run `npm install --save-dev jest babel-jest` 18 | 2. Add `"jest"` property to [`package.json`](./package.json) 19 | 3. Update `"test"` key in the `"scripts"` section of [`package.json`](./package.json) 20 | 21 | Setting it up this way will reuse the [`.babelrc`](./babelrc) file we already have set up so we can keep using the same syntax in our test files! 22 | 23 | # Check out the tests 24 | 25 | Check out the test file and make sure to read through the comments in `beforeEach()` to get a sense of how and why we set up the test file the way we did. 26 | 27 | - [`TodoStore-test.js`](./src/__tests__/TodoStore-test.js) 28 | - [`TodoStore.js`](../flux-todomvc/src/data/TodoStore.js) 29 | -------------------------------------------------------------------------------- /examples/flux-jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-jest", 3 | "version": "1.0.0", 4 | "description": "Basic shell application for the Flux architecture", 5 | "repository": "https://github.com/facebook/flux", 6 | "author": "Kyle Davis", 7 | "main": "no_main.js", 8 | "scripts": { 9 | "test": "jest" 10 | }, 11 | "dependencies": { 12 | "classnames": "^2.2.3", 13 | "flux": "../../.", 14 | "immutable": "^3.8.0", 15 | "react": "^15.0.2", 16 | "react-dom": "^15.0.1" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.7.6", 20 | "babel-jest": "^15.0.0", 21 | "babel-plugin-syntax-async-functions": "^6.5.0", 22 | "babel-plugin-syntax-flow": "^6.5.0", 23 | "babel-plugin-syntax-jsx": "^6.5.0", 24 | "babel-plugin-syntax-object-rest-spread": "^6.5.0", 25 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0", 26 | "babel-plugin-transform-flow-strip-types": "^6.5.0", 27 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 28 | "babel-plugin-transform-react-jsx": "^6.7.5", 29 | "babel-plugin-transform-regenerator": "^6.5.2", 30 | "babel-plugin-transform-runtime": "^6.5.2", 31 | "babel-preset-es2015": "^6.5.0", 32 | "jest": "^15.1.1" 33 | }, 34 | "jest": { 35 | "modulePathIgnorePatterns": [ 36 | "/node_modules/" 37 | ], 38 | "preprocessorIgnorePatterns": [ 39 | "/node_modules/" 40 | ], 41 | "rootDir": "", 42 | "testPathDirs": [ 43 | "/src" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/flux-logging/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "syntax-flow", 6 | "syntax-jsx", 7 | "syntax-object-rest-spread", 8 | "syntax-trailing-function-commas", 9 | "transform-flow-strip-types", 10 | "transform-object-rest-spread", 11 | "transform-react-jsx", 12 | "transform-regenerator", 13 | "transform-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/flux-logging/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | lib 3 | -------------------------------------------------------------------------------- /examples/flux-logging/README.md: -------------------------------------------------------------------------------- 1 | # flux-logging 2 | 3 | Taking advantage of the fact that a store gets every action makes it easy to add logging to a Flux application. Check out this quick example where we add a logger store to the TodoMVC app we created in an earlier example. 4 | 5 | # Usage 6 | 7 | ```bash 8 | cd path/to/flux-logging 9 | npm install 10 | npm run watch 11 | # open path/to/flux-logging/index.html in your browser 12 | ``` 13 | 14 | # See the logging 15 | 16 | Open the console in your browser and start adding todos. There should be logging printed to the console for each action that is fired. 17 | 18 | To see how this is accomplished in code look at the logger store we created: 19 | 20 | - [`TodoLoggerStore.js`](./src/TodoLoggerStore.js) 21 | 22 | When creating a logger store you will also need to make sure it is explicitly initialized at the root of your application. This is because no container will directly use the logger store. 23 | 24 | - See: [`root.js`](./src/root.js) 25 | -------------------------------------------------------------------------------- /examples/flux-logging/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flux • TodoMVC 6 | 7 | 8 | 9 |
10 |
11 |

Double-click to edit a todo

12 |

Part of TodoMVC

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/flux-logging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-logging", 3 | "version": "1.0.0", 4 | "description": "Basic shell application for the Flux architecture", 5 | "repository": "https://github.com/facebook/flux", 6 | "author": "Kyle Davis", 7 | "main": "bundle.js", 8 | "scripts": { 9 | "build": "webpack ./src/root.js ./bundle.js", 10 | "watch": "webpack ./src/root.js ./bundle.js --watch", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "dependencies": { 14 | "classnames": "^2.2.3", 15 | "flux": "../../.", 16 | "immutable": "^3.8.0", 17 | "react": "^15.0.2", 18 | "react-dom": "^15.0.1" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.7.6", 22 | "babel-loader": "^6.2.4", 23 | "babel-plugin-syntax-async-functions": "^6.5.0", 24 | "babel-plugin-syntax-flow": "^6.5.0", 25 | "babel-plugin-syntax-jsx": "^6.5.0", 26 | "babel-plugin-syntax-object-rest-spread": "^6.5.0", 27 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0", 28 | "babel-plugin-transform-flow-strip-types": "^6.5.0", 29 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 30 | "babel-plugin-transform-react-jsx": "^6.7.5", 31 | "babel-plugin-transform-regenerator": "^6.5.2", 32 | "babel-plugin-transform-runtime": "^6.5.2", 33 | "babel-preset-es2015": "^6.5.0", 34 | "webpack": "^1.13.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/flux-logging/src/TodoDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import TodoStore from '../../flux-todomvc/src/data/TodoStore'; 13 | 14 | // This is just a hack due to how we are requiring things from another project. 15 | // We need to make sure we use the correct dispatcher in our logger store and 16 | // this is the easiest way to ensure that. You should ignore this. 17 | const dispatcher = TodoStore.__dispatcher; 18 | 19 | export default dispatcher; 20 | -------------------------------------------------------------------------------- /examples/flux-logging/src/TodoLoggerStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import {ReduceStore} from 'flux/utils'; 13 | import TodoActionTypes from '../../flux-todomvc/src/data/TodoActionTypes'; 14 | import TodoDispatcher from './TodoDispatcher'; 15 | 16 | // This would be the util for logging data to wherver you want to keep it. 17 | // For now we will just log it to the console. 18 | const TodoLogger = { 19 | log(...args) { 20 | console.log(...args); 21 | }, 22 | }; 23 | 24 | class TodoLoggerStore extends ReduceStore { 25 | constructor() { 26 | super(TodoDispatcher); 27 | } 28 | 29 | getInitialState() { 30 | // We don't actually have any state in this store. Eventually you may want 31 | // to keep state here in order to track "sequences of actions", but we won't 32 | // cover that in this example. 33 | return null; 34 | } 35 | 36 | reduce(state, action) { 37 | // We will always log the action type. Could extend this to log the entire 38 | // action, but depending on your actions and applications that could be a 39 | // large amount of data. 40 | TodoLogger.log('general_action_logging', action.type); 41 | 42 | // Here we can log additional information for specific action types we 43 | // care about. Can handle any kind of action here from any store. 44 | switch (action.type) { 45 | case TodoActionTypes.DELETE_TODO: 46 | TodoLogger.log('direct_deletion', action.id); 47 | break; 48 | } 49 | 50 | // Always return the old state. 51 | return state; 52 | } 53 | } 54 | 55 | export default new TodoLoggerStore(); 56 | -------------------------------------------------------------------------------- /examples/flux-logging/src/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import AppContainer from '../../flux-todomvc/src/containers/AppContainer'; 13 | import {Container} from 'flux/utils'; 14 | import React from 'react'; 15 | import ReactDOM from 'react-dom'; 16 | 17 | // Import the logger store explicitly so that it registers with the dispatcher. 18 | import TodoLoggerStore from './TodoLoggerStore'; 19 | 20 | ReactDOM.render(, document.getElementById('todoapp')); 21 | -------------------------------------------------------------------------------- /examples/flux-logging/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | module: { 10 | loaders: [{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/flux-shell/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "syntax-flow", 6 | "syntax-jsx", 7 | "syntax-object-rest-spread", 8 | "syntax-trailing-function-commas", 9 | "transform-flow-strip-types", 10 | "transform-object-rest-spread", 11 | "transform-react-jsx", 12 | "transform-regenerator", 13 | "transform-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/flux-shell/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | lib 3 | -------------------------------------------------------------------------------- /examples/flux-shell/README.md: -------------------------------------------------------------------------------- 1 | # flux-shell 2 | 3 | This is a very simple hello world app that you can use as a starting point for your application. 4 | 5 | # Usage 6 | 7 | ```bash 8 | cd path/to/flux-shell 9 | npm install 10 | npm run build 11 | # open path/to/flux-shell/index.html in your browser 12 | ``` 13 | 14 | # Watch 15 | 16 | Instead of manually building after each change it's possible to automatically recompile the javascript bundle when files change: 17 | 18 | ```bash 19 | npm run watch 20 | # make javascript changes, then refresh path/to/flux-shell/index.html 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/flux-shell/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flux • Shell 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/flux-shell/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-shell", 3 | "version": "1.0.0", 4 | "description": "Basic shell application for the Flux architecture", 5 | "repository": "https://github.com/facebook/flux", 6 | "author": "Kyle Davis", 7 | "license": "BSD-3-Clause", 8 | "main": "bundle.js", 9 | "scripts": { 10 | "build": "webpack ./src/root.js ./bundle.js", 11 | "watch": "webpack ./src/root.js ./bundle.js --watch", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "dependencies": { 15 | "classnames": "^2.2.3", 16 | "flux": "../../.", 17 | "immutable": "^3.8.0", 18 | "react": "^15.0.2", 19 | "react-dom": "^15.0.1" 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^6.7.6", 23 | "babel-loader": "^6.2.4", 24 | "babel-plugin-syntax-async-functions": "^6.5.0", 25 | "babel-plugin-syntax-flow": "^6.5.0", 26 | "babel-plugin-syntax-jsx": "^6.5.0", 27 | "babel-plugin-syntax-object-rest-spread": "^6.5.0", 28 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0", 29 | "babel-plugin-transform-flow-strip-types": "^6.5.0", 30 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 31 | "babel-plugin-transform-react-jsx": "^6.7.5", 32 | "babel-plugin-transform-regenerator": "^6.5.2", 33 | "babel-plugin-transform-runtime": "^6.5.2", 34 | "babel-preset-es2015": "^6.5.0", 35 | "webpack": "^1.13.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/flux-shell/src/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import React from 'react'; 13 | import ReactDOM from 'react-dom'; 14 | 15 | ReactDOM.render(
Hello World!
, document.getElementById('root')); 16 | -------------------------------------------------------------------------------- /examples/flux-shell/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | module: { 10 | loaders: [{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/flux-todomvc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "syntax-flow", 6 | "syntax-jsx", 7 | "syntax-object-rest-spread", 8 | "syntax-trailing-function-commas", 9 | "transform-flow-strip-types", 10 | "transform-object-rest-spread", 11 | "transform-react-jsx", 12 | "transform-regenerator", 13 | "transform-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/flux-todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | lib 3 | -------------------------------------------------------------------------------- /examples/flux-todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flux • TodoMVC 6 | 7 | 8 | 9 |
10 |
11 |

Double-click to edit a todo

12 |

Part of TodoMVC

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/flux-todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-todomvc", 3 | "version": "1.0.0", 4 | "description": "Basic shell application for the Flux architecture", 5 | "repository": "https://github.com/facebook/flux", 6 | "author": "Kyle Davis", 7 | "main": "bundle.js", 8 | "scripts": { 9 | "build": "webpack ./src/root.js ./bundle.js", 10 | "watch": "webpack ./src/root.js ./bundle.js --watch", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "dependencies": { 14 | "classnames": "^2.2.3", 15 | "flux": "../../.", 16 | "immutable": "^3.8.0", 17 | "react": "^15.0.2", 18 | "react-dom": "^15.0.1" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.7.6", 22 | "babel-loader": "^6.2.4", 23 | "babel-plugin-syntax-async-functions": "^6.5.0", 24 | "babel-plugin-syntax-flow": "^6.5.0", 25 | "babel-plugin-syntax-jsx": "^6.5.0", 26 | "babel-plugin-syntax-object-rest-spread": "^6.5.0", 27 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0", 28 | "babel-plugin-transform-flow-strip-types": "^6.5.0", 29 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 30 | "babel-plugin-transform-react-jsx": "^6.7.5", 31 | "babel-plugin-transform-regenerator": "^6.5.2", 32 | "babel-plugin-transform-runtime": "^6.5.2", 33 | "babel-preset-es2015": "^6.5.0", 34 | "webpack": "^1.13.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import AppView from '../views/AppView'; 13 | import {Container} from 'flux/utils'; 14 | import TodoActions from '../data/TodoActions'; 15 | import TodoDraftStore from '../data/TodoDraftStore'; 16 | import TodoEditStore from '../data/TodoEditStore'; 17 | import TodoStore from '../data/TodoStore'; 18 | 19 | function getStores() { 20 | return [TodoEditStore, TodoDraftStore, TodoStore]; 21 | } 22 | 23 | function getState() { 24 | return { 25 | draft: TodoDraftStore.getState(), 26 | editing: TodoEditStore.getState(), 27 | todos: TodoStore.getState(), 28 | 29 | onAdd: TodoActions.addTodo, 30 | onDeleteCompletedTodos: TodoActions.deleteCompletedTodos, 31 | onDeleteTodo: TodoActions.deleteTodo, 32 | onEditTodo: TodoActions.editTodo, 33 | onStartEditingTodo: TodoActions.startEditingTodo, 34 | onStopEditingTodo: TodoActions.stopEditingTodo, 35 | onToggleAllTodos: TodoActions.toggleAllTodos, 36 | onToggleTodo: TodoActions.toggleTodo, 37 | onUpdateDraft: TodoActions.updateDraft, 38 | }; 39 | } 40 | 41 | export default Container.createFunctional(AppView, getStores, getState); 42 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/Counter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | let _counter = 1; 13 | 14 | /** 15 | * This is a simple counter for providing unique ids. 16 | */ 17 | const Counter = { 18 | increment() { 19 | return 'id-' + String(_counter++); 20 | }, 21 | }; 22 | 23 | export default Counter; 24 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/Todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import Immutable from 'immutable'; 13 | 14 | const Todo = Immutable.Record({ 15 | id: '', 16 | complete: false, 17 | text: '', 18 | }); 19 | 20 | export default Todo; 21 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/TodoActionTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | const ActionTypes = { 13 | ADD_TODO: 'ADD_TODO', 14 | DELETE_COMPLETED_TODOS: 'DELETE_COMPLETED_TODOS', 15 | DELETE_TODO: 'DELETE_TODO', 16 | EDIT_TODO: 'EDIT_TODO', 17 | START_EDITING_TODO: 'START_EDITING_TODO', 18 | STOP_EDITING_TODO: 'STOP_EDITING_TODO', 19 | TOGGLE_ALL_TODOS: 'TOGGLE_ALL_TODOS', 20 | TOGGLE_TODO: 'TOGGLE_TODO', 21 | UPDATE_DRAFT: 'UPDATE_DRAFT', 22 | }; 23 | 24 | export default ActionTypes; 25 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/TodoActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import TodoActionTypes from './TodoActionTypes'; 13 | import TodoDispatcher from './TodoDispatcher'; 14 | 15 | const Actions = { 16 | addTodo(text) { 17 | TodoDispatcher.dispatch({ 18 | type: TodoActionTypes.ADD_TODO, 19 | text, 20 | }); 21 | }, 22 | 23 | deleteCompletedTodos() { 24 | TodoDispatcher.dispatch({ 25 | type: TodoActionTypes.DELETE_COMPLETED_TODOS, 26 | }); 27 | }, 28 | 29 | deleteTodo(id) { 30 | TodoDispatcher.dispatch({ 31 | type: TodoActionTypes.DELETE_TODO, 32 | id, 33 | }); 34 | }, 35 | 36 | editTodo(id, text) { 37 | TodoDispatcher.dispatch({ 38 | type: TodoActionTypes.EDIT_TODO, 39 | id, 40 | text, 41 | }); 42 | }, 43 | 44 | startEditingTodo(id) { 45 | TodoDispatcher.dispatch({ 46 | type: TodoActionTypes.START_EDITING_TODO, 47 | id, 48 | }); 49 | }, 50 | 51 | stopEditingTodo() { 52 | TodoDispatcher.dispatch({ 53 | type: TodoActionTypes.STOP_EDITING_TODO, 54 | }); 55 | }, 56 | 57 | toggleAllTodos() { 58 | TodoDispatcher.dispatch({ 59 | type: TodoActionTypes.TOGGLE_ALL_TODOS, 60 | }); 61 | }, 62 | 63 | toggleTodo(id) { 64 | TodoDispatcher.dispatch({ 65 | type: TodoActionTypes.TOGGLE_TODO, 66 | id, 67 | }); 68 | }, 69 | 70 | updateDraft(text) { 71 | TodoDispatcher.dispatch({ 72 | type: TodoActionTypes.UPDATE_DRAFT, 73 | text, 74 | }); 75 | }, 76 | }; 77 | 78 | export default Actions; 79 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/TodoDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Blah blah 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import {Dispatcher} from 'flux'; 15 | 16 | export default new Dispatcher(); 17 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/TodoDraftStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import {ReduceStore} from 'flux/utils'; 13 | import TodoActionTypes from './TodoActionTypes'; 14 | import TodoDispatcher from './TodoDispatcher'; 15 | 16 | class TodoDraftStore extends ReduceStore { 17 | constructor() { 18 | super(TodoDispatcher); 19 | } 20 | 21 | getInitialState() { 22 | return ''; 23 | } 24 | 25 | reduce(state, action) { 26 | switch (action.type) { 27 | case TodoActionTypes.ADD_TODO: 28 | return ''; 29 | 30 | case TodoActionTypes.UPDATE_DRAFT: 31 | return action.text; 32 | 33 | default: 34 | return state; 35 | } 36 | } 37 | } 38 | 39 | export default new TodoDraftStore(); 40 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/TodoEditStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import {ReduceStore} from 'flux/utils'; 13 | import TodoActionTypes from './TodoActionTypes'; 14 | import TodoDispatcher from './TodoDispatcher'; 15 | 16 | class TodoEditStore extends ReduceStore { 17 | constructor() { 18 | super(TodoDispatcher); 19 | } 20 | 21 | getInitialState() { 22 | return ''; 23 | } 24 | 25 | reduce(state, action) { 26 | switch (action.type) { 27 | case TodoActionTypes.START_EDITING_TODO: 28 | return action.id; 29 | 30 | case TodoActionTypes.STOP_EDITING_TODO: 31 | return ''; 32 | 33 | default: 34 | return state; 35 | } 36 | } 37 | } 38 | 39 | export default new TodoEditStore(); 40 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/data/TodoStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import Counter from './Counter'; 13 | import Immutable from 'immutable'; 14 | import {ReduceStore} from 'flux/utils'; 15 | import Todo from './Todo'; 16 | import TodoActionTypes from './TodoActionTypes'; 17 | import TodoDispatcher from './TodoDispatcher'; 18 | 19 | class TodoStore extends ReduceStore { 20 | constructor() { 21 | super(TodoDispatcher); 22 | } 23 | 24 | getInitialState() { 25 | return Immutable.OrderedMap(); 26 | } 27 | 28 | reduce(state, action) { 29 | switch (action.type) { 30 | case TodoActionTypes.ADD_TODO: 31 | // Don't add todos with no text. 32 | if (!action.text) { 33 | return state; 34 | } 35 | const id = Counter.increment(); 36 | return state.set( 37 | id, 38 | new Todo({ 39 | id, 40 | text: action.text, 41 | complete: false, 42 | }), 43 | ); 44 | 45 | case TodoActionTypes.DELETE_COMPLETED_TODOS: 46 | return state.filter((todo) => !todo.complete); 47 | 48 | case TodoActionTypes.DELETE_TODO: 49 | return state.delete(action.id); 50 | 51 | case TodoActionTypes.EDIT_TODO: 52 | return state.setIn([action.id, 'text'], action.text); 53 | 54 | case TodoActionTypes.TOGGLE_ALL_TODOS: 55 | const areAllComplete = state.every((todo) => todo.complete); 56 | return state.map((todo) => todo.set('complete', !areAllComplete)); 57 | 58 | case TodoActionTypes.TOGGLE_TODO: 59 | return state.update(action.id, (todo) => 60 | todo.set('complete', !todo.complete), 61 | ); 62 | 63 | default: 64 | return state; 65 | } 66 | } 67 | } 68 | 69 | export default new TodoStore(); 70 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import AppContainer from './containers/AppContainer'; 13 | import React from 'react'; 14 | import ReactDOM from 'react-dom'; 15 | 16 | ReactDOM.render(, document.getElementById('todoapp')); 17 | -------------------------------------------------------------------------------- /examples/flux-todomvc/src/views/AppView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import React from 'react'; 13 | 14 | import classnames from 'classnames'; 15 | 16 | function AppView(props) { 17 | return ( 18 |
19 |
20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | function Header(props) { 27 | return ( 28 | 32 | ); 33 | } 34 | 35 | function Main(props) { 36 | if (props.todos.size === 0) { 37 | return null; 38 | } 39 | 40 | // If this were expensive we could move it to the container. 41 | const areAllComplete = props.todos.every((todo) => todo.complete); 42 | 43 | return ( 44 |
45 | 51 | 52 |
    53 | {[...props.todos.values()].reverse().map((todo) => ( 54 | 64 | ))} 65 |
66 |
67 | ); 68 | } 69 | 70 | function Footer(props) { 71 | if (props.todos.size === 0) { 72 | return null; 73 | } 74 | 75 | const remaining = props.todos.filter((todo) => !todo.complete).size; 76 | const completed = props.todos.size - remaining; 77 | const phrase = remaining === 1 ? ' item left' : ' items left'; 78 | 79 | let clearCompletedButton = null; 80 | if (completed > 0) { 81 | clearCompletedButton = ( 82 | 85 | ); 86 | } 87 | 88 | return ( 89 |
90 | 91 | {remaining} 92 | {phrase} 93 | 94 | {clearCompletedButton} 95 |
96 | ); 97 | } 98 | 99 | const ENTER_KEY_CODE = 13; 100 | function NewTodo(props) { 101 | const addTodo = () => props.onAdd(props.draft); 102 | const onBlur = () => addTodo(); 103 | const onChange = (event) => props.onUpdateDraft(event.target.value); 104 | const onKeyDown = (event) => { 105 | if (event.keyCode === ENTER_KEY_CODE) { 106 | addTodo(); 107 | } 108 | }; 109 | return ( 110 | 119 | ); 120 | } 121 | 122 | function TodoItem(props) { 123 | const {editing, todo} = props; 124 | const isEditing = editing === todo.id; 125 | const onDeleteTodo = () => props.onDeleteTodo(todo.id); 126 | const onStartEditingTodo = () => props.onStartEditingTodo(todo.id); 127 | const onToggleTodo = () => props.onToggleTodo(todo.id); 128 | 129 | // Construct the input for editing a task if necessary. 130 | let input = null; 131 | if (isEditing) { 132 | const onChange = (event) => props.onEditTodo(todo.id, event.target.value); 133 | const onStopEditingTodo = props.onStopEditingTodo; 134 | const onKeyDown = (event) => { 135 | if (event.keyCode === ENTER_KEY_CODE) { 136 | onStopEditingTodo(); 137 | } 138 | }; 139 | input = ( 140 | 148 | ); 149 | } 150 | 151 | return ( 152 |
  • 157 |
    158 | 164 | 165 |
    167 | {input} 168 |
  • 169 | ); 170 | } 171 | 172 | export default AppView; 173 | -------------------------------------------------------------------------------- /examples/flux-todomvc/todomvc-common/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "0.1.9", 4 | "homepage": "https://github.com/tastejs/todomvc-common", 5 | "_release": "0.1.9", 6 | "_resolution": { 7 | "type": "version", 8 | "tag": "v0.1.9", 9 | "commit": "7dd61b0ebf56c020e719a69444442cc7ae7242ff" 10 | }, 11 | "_source": "git://github.com/tastejs/todomvc-common.git", 12 | "_target": "~0.1.4", 13 | "_originalSource": "todomvc-common" 14 | } 15 | -------------------------------------------------------------------------------- /examples/flux-todomvc/todomvc-common/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/flux/4ee8c50865c357e005cb40ea03cdd403533aad26/examples/flux-todomvc/todomvc-common/bg.png -------------------------------------------------------------------------------- /examples/flux-todomvc/todomvc-common/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "0.1.9" 4 | } 5 | -------------------------------------------------------------------------------- /examples/flux-todomvc/todomvc-common/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-common 2 | 3 | > Bower component for some common utilities we use in every app 4 | 5 | ## License 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /examples/flux-todomvc/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | module: { 10 | loaders: [{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/todomvc-common/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "0.1.9", 4 | "homepage": "https://github.com/tastejs/todomvc-common", 5 | "_release": "0.1.9", 6 | "_resolution": { 7 | "type": "version", 8 | "tag": "v0.1.9", 9 | "commit": "7dd61b0ebf56c020e719a69444442cc7ae7242ff" 10 | }, 11 | "_source": "git://github.com/tastejs/todomvc-common.git", 12 | "_target": "~0.1.4", 13 | "_originalSource": "todomvc-common" 14 | } 15 | -------------------------------------------------------------------------------- /examples/todomvc-common/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/flux/4ee8c50865c357e005cb40ea03cdd403533aad26/examples/todomvc-common/bg.png -------------------------------------------------------------------------------- /examples/todomvc-common/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "0.1.9" 4 | } 5 | -------------------------------------------------------------------------------- /examples/todomvc-common/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-common 2 | 3 | > Bower component for some common utilities we use in every app 4 | 5 | ## License 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /img/flux-diagram-white-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/flux/4ee8c50865c357e005cb40ea03cdd403533aad26/img/flux-diagram-white-background.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | module.exports.Dispatcher = require('./lib/Dispatcher'); 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | modulePathIgnorePatterns: ['/lib/', '/node_modules/'], 10 | transformIgnorePatterns: ['/node_modules/'], 11 | rootDir: './', 12 | transform: { 13 | '\\.js$': './scripts/jest/preprocessor.js', 14 | }, 15 | setupFiles: ['./scripts/jest/environment.js'], 16 | roots: ['/src'], 17 | modulePaths: [ 18 | '/src', 19 | '/src/container', 20 | '/src/stores', 21 | ], 22 | testEnvironment: 'jsdom', 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux", 3 | "version": "4.0.4", 4 | "description": "An application architecture based on a unidirectional data flow", 5 | "keywords": [ 6 | "flux", 7 | "react", 8 | "facebook", 9 | "dispatcher" 10 | ], 11 | "homepage": "https://facebookarchive.github.io/flux/", 12 | "bugs": "https://github.com/facebookarchive/flux/issues", 13 | "files": [ 14 | "LICENSE", 15 | "PATENTS", 16 | "README.md", 17 | "index.js", 18 | "lib", 19 | "utils.js", 20 | "dist" 21 | ], 22 | "main": "index.js", 23 | "scripts": { 24 | "build": "gulp build", 25 | "prepublish": "gulp publish", 26 | "test": "NODE_ENV=test jest" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/facebook/flux" 31 | }, 32 | "author": "Facebook", 33 | "contributors": [ 34 | "Jing Chen ", 35 | "Bill Fisher ", 36 | "Paul O'Shannessy ", 37 | "Kyle Davis ", 38 | "Yangshun Tay " 39 | ], 40 | "license": "BSD-3-Clause", 41 | "devDependencies": { 42 | "@babel/core": "^7.12.10", 43 | "babel-loader": "^8.1.0", 44 | "babel-preset-fbjs": "^3.3.0", 45 | "del": "^2.2.0", 46 | "gulp": "^4.0.2", 47 | "gulp-babel": "^8.0.0", 48 | "gulp-flatten": "^0.2.0", 49 | "gulp-header": "1.8.2", 50 | "gulp-rename": "^1.2.2", 51 | "gulp-util": "^3.0.6", 52 | "immutable": "^3.7.4", 53 | "jest": "^28.1.0", 54 | "jest-environment-jsdom": "^28.1.0", 55 | "object-assign": "^4.0.1", 56 | "react": "^17.0.1", 57 | "react-addons-test-utils": "^15.6.2", 58 | "react-dom": "^17.0.1", 59 | "vinyl-source-stream": "^1.0.0", 60 | "webpack": "^1.11.0", 61 | "webpack-stream": "^3.1.0" 62 | }, 63 | "dependencies": { 64 | "fbemitter": "^3.0.0", 65 | "fbjs": "^3.0.1" 66 | }, 67 | "peerDependencies": { 68 | "react": "^15.0.2 || ^16.0.0 || ^17.0.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /scripts/babel/default-options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = { 12 | presets: [ 13 | require('babel-preset-fbjs/configure')({ 14 | rewriteModules: { 15 | map: { 16 | react: 'react', 17 | fbemitter: 'fbemitter', 18 | immutable: 'immutable', 19 | invariant: 'fbjs/lib/invariant', 20 | shallowEqual: 'fbjs/lib/shallowEqual', 21 | }, 22 | }, 23 | stripDEV: true, 24 | }), 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /scripts/jest/environment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | */ 8 | 9 | global.__DEV__ = true; 10 | -------------------------------------------------------------------------------- /scripts/jest/preprocessor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const assign = require('object-assign'); 9 | const babel = require('@babel/core'); 10 | const babelOpts = require('../babel/default-options'); 11 | 12 | module.exports = { 13 | process: function (src, path) { 14 | return { 15 | code: babel.transform(src, assign({filename: path}, babelOpts)).code, 16 | }; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/__tests__/.* 3 | 4 | [include] 5 | ../node_modules/fbemitter 6 | ../node_modules/fbjs/lib 7 | ../node_modules/immutable 8 | ../node_modules/promise 9 | ../node_modules/react 10 | 11 | [libs] 12 | ../node_modules/fbjs/flow/lib 13 | 14 | [options] 15 | module.system=haste 16 | munge_underscores=true 17 | 18 | suppress_type=$FlowIssue 19 | suppress_type=$FlowFixMe 20 | suppress_type=$FixMe 21 | suppress_type=$FlowExpectedError 22 | 23 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-2]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\) 24 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-2]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\)?:? #[0-9]+ 25 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 26 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 27 | 28 | [version] 29 | 0.35.0 30 | -------------------------------------------------------------------------------- /src/FluxMixinLegacy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | * 8 | * @providesModule FluxMixinLegacy 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type FluxStore from 'FluxStore'; 15 | 16 | var FluxStoreGroup = require('FluxStoreGroup'); 17 | 18 | var invariant = require('invariant'); 19 | 20 | type Options = { 21 | withProps?: boolean, 22 | }; 23 | 24 | /** 25 | * `FluxContainer` should be preferred over this mixin, but it requires using 26 | * react with classes. So this mixin is provided where it is not yet possible 27 | * to convert a container to be a class. 28 | * 29 | * This mixin should be used for React components that have state based purely 30 | * on stores. `this.props` will not be available inside of `calculateState()`. 31 | * 32 | * This mixin will only `setState` not replace it, so you should always return 33 | * every key in your state unless you know what you are doing. Consider this: 34 | * 35 | * var Foo = React.createClass({ 36 | * mixins: [ 37 | * FluxMixinLegacy([FooStore]) 38 | * ], 39 | * 40 | * statics: { 41 | * calculateState(prevState) { 42 | * if (!prevState) { 43 | * return { 44 | * foo: FooStore.getFoo(), 45 | * }; 46 | * } 47 | * 48 | * return { 49 | * bar: FooStore.getBar(), 50 | * }; 51 | * } 52 | * }, 53 | * }); 54 | * 55 | * On the second calculateState when prevState is not null, the state will be 56 | * updated to contain the previous foo AND the bar that was just returned. Only 57 | * returning bar will not delete foo. 58 | * 59 | */ 60 | function FluxMixinLegacy( 61 | stores: Array, 62 | options: Options = {withProps: false}, 63 | ): any { 64 | stores = stores.filter((store) => !!store); 65 | 66 | return { 67 | getInitialState(): Object { 68 | enforceInterface(this); 69 | return options.withProps 70 | ? this.constructor.calculateState(null, this.props) 71 | : this.constructor.calculateState(null, undefined); 72 | }, 73 | 74 | componentWillMount(): void { 75 | // This tracks when any store has changed and we may need to update. 76 | var changed = false; 77 | var setChanged = () => { 78 | changed = true; 79 | }; 80 | 81 | // This adds subscriptions to stores. When a store changes all we do is 82 | // set changed to true. 83 | this._fluxMixinSubscriptions = stores.map((store) => 84 | store.addListener(setChanged), 85 | ); 86 | 87 | // This callback is called after the dispatch of the relevant stores. If 88 | // any have reported a change we update the state, then reset changed. 89 | var callback = () => { 90 | if (changed) { 91 | this.setState((prevState) => 92 | options.withProps 93 | ? this.constructor.calculateState(prevState, this.props) 94 | : this.constructor.calculateState(prevState, undefined), 95 | ); 96 | } 97 | changed = false; 98 | }; 99 | this._fluxMixinStoreGroup = new FluxStoreGroup(stores, callback); 100 | }, 101 | 102 | componentWillUnmount(): void { 103 | this._fluxMixinStoreGroup.release(); 104 | for (var subscription of this._fluxMixinSubscriptions) { 105 | subscription.remove(); 106 | } 107 | this._fluxMixinSubscriptions = []; 108 | }, 109 | }; 110 | } 111 | 112 | function enforceInterface(o: any): void { 113 | invariant( 114 | o.constructor.calculateState, 115 | 'Components that use FluxMixinLegacy must implement ' + 116 | '`calculateState()` on the statics object', 117 | ); 118 | } 119 | 120 | module.exports = FluxMixinLegacy; 121 | -------------------------------------------------------------------------------- /src/FluxStoreGroup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule FluxStoreGroup 10 | * @flow 11 | */ 12 | 13 | 'use strict'; 14 | 15 | import type Dispatcher from 'Dispatcher'; 16 | import type FluxStore from 'FluxStore'; 17 | 18 | var invariant = require('invariant'); 19 | 20 | type DispatchToken = string; 21 | 22 | /** 23 | * FluxStoreGroup allows you to execute a callback on every dispatch after 24 | * waiting for each of the given stores. 25 | */ 26 | class FluxStoreGroup { 27 | _dispatcher: Dispatcher; 28 | _dispatchToken: DispatchToken; 29 | 30 | constructor(stores: Array, callback: Function): void { 31 | this._dispatcher = _getUniformDispatcher(stores); 32 | 33 | // Precompute store tokens. 34 | var storeTokens = stores.map((store) => store.getDispatchToken()); 35 | 36 | // Register with the dispatcher. 37 | this._dispatchToken = this._dispatcher.register((payload) => { 38 | this._dispatcher.waitFor(storeTokens); 39 | callback(); 40 | }); 41 | } 42 | 43 | release(): void { 44 | this._dispatcher.unregister(this._dispatchToken); 45 | } 46 | } 47 | 48 | function _getUniformDispatcher(stores: Array): Dispatcher { 49 | invariant( 50 | stores && stores.length, 51 | 'Must provide at least one store to FluxStoreGroup', 52 | ); 53 | var dispatcher = stores[0].getDispatcher(); 54 | if (__DEV__) { 55 | for (var store of stores) { 56 | invariant( 57 | store.getDispatcher() === dispatcher, 58 | 'All stores in a FluxStoreGroup must use the same dispatcher', 59 | ); 60 | } 61 | } 62 | return dispatcher; 63 | } 64 | 65 | module.exports = FluxStoreGroup; 66 | -------------------------------------------------------------------------------- /src/__tests__/Dispatcher-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import Dispatcher from 'Dispatcher'; 11 | 12 | describe('Dispatcher', () => { 13 | let dispatcher; 14 | let callbackA; 15 | let callbackB; 16 | 17 | beforeEach(() => { 18 | dispatcher = new Dispatcher(); 19 | callbackA = jest.fn(); 20 | callbackB = jest.fn(); 21 | }); 22 | 23 | it('should execute all subscriber callbacks', () => { 24 | dispatcher.register(callbackA); 25 | dispatcher.register(callbackB); 26 | 27 | const payload = {}; 28 | dispatcher.dispatch(payload); 29 | 30 | expect(callbackA.mock.calls.length).toBe(1); 31 | expect(callbackA.mock.calls[0][0]).toBe(payload); 32 | 33 | expect(callbackB.mock.calls.length).toBe(1); 34 | expect(callbackB.mock.calls[0][0]).toBe(payload); 35 | 36 | dispatcher.dispatch(payload); 37 | 38 | expect(callbackA.mock.calls.length).toBe(2); 39 | expect(callbackA.mock.calls[1][0]).toBe(payload); 40 | 41 | expect(callbackB.mock.calls.length).toBe(2); 42 | expect(callbackB.mock.calls[1][0]).toBe(payload); 43 | }); 44 | 45 | it('should wait for callbacks registered earlier', () => { 46 | const tokenA = dispatcher.register(callbackA); 47 | 48 | dispatcher.register((payload) => { 49 | dispatcher.waitFor([tokenA]); 50 | expect(callbackA.mock.calls.length).toBe(1); 51 | expect(callbackA.mock.calls[0][0]).toBe(payload); 52 | callbackB(payload); 53 | }); 54 | 55 | const payload = {}; 56 | dispatcher.dispatch(payload); 57 | 58 | expect(callbackA.mock.calls.length).toBe(1); 59 | expect(callbackA.mock.calls[0][0]).toBe(payload); 60 | 61 | expect(callbackB.mock.calls.length).toBe(1); 62 | expect(callbackB.mock.calls[0][0]).toBe(payload); 63 | }); 64 | 65 | it('should wait for callbacks registered later', () => { 66 | dispatcher.register((payload) => { 67 | dispatcher.waitFor([tokenB]); 68 | expect(callbackB.mock.calls.length).toBe(1); 69 | expect(callbackB.mock.calls[0][0]).toBe(payload); 70 | callbackA(payload); 71 | }); 72 | 73 | const tokenB = dispatcher.register(callbackB); 74 | 75 | const payload = {}; 76 | dispatcher.dispatch(payload); 77 | 78 | expect(callbackA.mock.calls.length).toBe(1); 79 | expect(callbackA.mock.calls[0][0]).toBe(payload); 80 | 81 | expect(callbackB.mock.calls.length).toBe(1); 82 | expect(callbackB.mock.calls[0][0]).toBe(payload); 83 | }); 84 | 85 | it('should throw if dispatch() while dispatching', () => { 86 | dispatcher.register((payload) => { 87 | dispatcher.dispatch(payload); 88 | callbackA(); 89 | }); 90 | 91 | const payload = {}; 92 | expect(() => dispatcher.dispatch(payload)).toThrow(); 93 | expect(callbackA.mock.calls.length).toBe(0); 94 | }); 95 | 96 | it('should throw if waitFor() while not dispatching', () => { 97 | const tokenA = dispatcher.register(callbackA); 98 | 99 | expect(() => dispatcher.waitFor([tokenA])).toThrow(); 100 | expect(callbackA.mock.calls.length).toBe(0); 101 | }); 102 | 103 | it('should throw if waitFor() with invalid token', () => { 104 | const invalidToken = 1337; 105 | 106 | dispatcher.register(() => { 107 | dispatcher.waitFor([invalidToken]); 108 | }); 109 | 110 | const payload = {}; 111 | expect(() => dispatcher.dispatch(payload)).toThrow(); 112 | }); 113 | 114 | it('should throw on self-circular dependencies', () => { 115 | const tokenA = dispatcher.register((payload) => { 116 | dispatcher.waitFor([tokenA]); 117 | callbackA(payload); 118 | }); 119 | 120 | const payload = {}; 121 | expect(() => dispatcher.dispatch(payload)).toThrow(); 122 | expect(callbackA.mock.calls.length).toBe(0); 123 | }); 124 | 125 | it('should throw on multi-circular dependencies', () => { 126 | const tokenA = dispatcher.register((payload) => { 127 | dispatcher.waitFor([tokenB]); 128 | callbackA(payload); 129 | }); 130 | 131 | const tokenB = dispatcher.register((payload) => { 132 | dispatcher.waitFor([tokenA]); 133 | callbackB(payload); 134 | }); 135 | 136 | expect(() => dispatcher.dispatch({})).toThrow(); 137 | expect(callbackA.mock.calls.length).toBe(0); 138 | expect(callbackB.mock.calls.length).toBe(0); 139 | }); 140 | 141 | it('should remain in a consistent state after a failed dispatch', () => { 142 | dispatcher.register(callbackA); 143 | dispatcher.register((payload) => { 144 | if (payload.shouldThrow) { 145 | throw new Error(); 146 | } 147 | callbackB(); 148 | }); 149 | 150 | expect(() => dispatcher.dispatch({shouldThrow: true})).toThrow(); 151 | 152 | // Cannot make assumptions about a failed dispatch. 153 | const callbackACount = callbackA.mock.calls.length; 154 | 155 | dispatcher.dispatch({shouldThrow: false}); 156 | 157 | expect(callbackA.mock.calls.length).toBe(callbackACount + 1); 158 | expect(callbackB.mock.calls.length).toBe(1); 159 | }); 160 | 161 | it('should properly unregister callbacks', () => { 162 | dispatcher.register(callbackA); 163 | 164 | const tokenB = dispatcher.register(callbackB); 165 | 166 | const payload = {}; 167 | dispatcher.dispatch(payload); 168 | 169 | expect(callbackA.mock.calls.length).toBe(1); 170 | expect(callbackA.mock.calls[0][0]).toBe(payload); 171 | 172 | expect(callbackB.mock.calls.length).toBe(1); 173 | expect(callbackB.mock.calls[0][0]).toBe(payload); 174 | 175 | dispatcher.unregister(tokenB); 176 | 177 | dispatcher.dispatch(payload); 178 | 179 | expect(callbackA.mock.calls.length).toBe(2); 180 | expect(callbackA.mock.calls[1][0]).toBe(payload); 181 | 182 | expect(callbackB.mock.calls.length).toBe(1); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /src/__tests__/FluxStoreGroup-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @emails oncall+uiecommd 10 | * @typechecks 11 | */ 12 | 13 | import Dispatcher from 'Dispatcher'; 14 | import FluxStore from 'FluxStore'; 15 | import FluxStoreGroup from 'FluxStoreGroup'; 16 | 17 | class SimpleStore extends FluxStore { 18 | __onDispatch(payload) { 19 | this.__emitChange(); 20 | } 21 | } 22 | 23 | describe('FluxStoreGroup', () => { 24 | let dispatcher; 25 | let storeA; 26 | let storeB; 27 | 28 | beforeEach(() => { 29 | dispatcher = new Dispatcher(); 30 | storeA = new SimpleStore(dispatcher); 31 | storeB = new SimpleStore(dispatcher); 32 | }); 33 | 34 | it('should register a callback with the dispatcher', () => { 35 | const callback = jest.fn(); 36 | new FluxStoreGroup([storeA, storeB], callback); 37 | 38 | dispatcher.dispatch({type: 'foo'}); 39 | expect(callback).toBeCalled(); 40 | }); 41 | 42 | it('should wait for the store dependencies', () => { 43 | const callback = jest.fn().mockImplementation(() => { 44 | expect(storeA.hasChanged()).toBe(true); 45 | expect(storeB.hasChanged()).toBe(true); 46 | }); 47 | new FluxStoreGroup([storeA, storeB], callback); 48 | 49 | dispatcher.dispatch({type: 'foo'}); 50 | expect(callback).toBeCalled(); 51 | }); 52 | 53 | it('should not run the callback after being released', () => { 54 | const callback = jest.fn(); 55 | const group = new FluxStoreGroup([storeA, storeB], callback); 56 | group.release(); 57 | 58 | dispatcher.dispatch({type: 'foo'}); 59 | expect(callback).not.toBeCalled(); 60 | }); 61 | 62 | it('should have at least one store', () => { 63 | const callback = jest.fn(); 64 | expect(() => new FluxStoreGroup([], callback)).toThrow(); 65 | }); 66 | 67 | it('should make sure dispatchers are uniform in DEV', () => { 68 | const callback = jest.fn(); 69 | const storeC = new SimpleStore(new Dispatcher()); 70 | expect(() => new FluxStoreGroup([storeA, storeC], callback)).toThrow(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/container/FluxContainerSubscriptions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | * 8 | * @providesModule FluxContainerSubscriptions 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type FluxStore from 'FluxStore'; 15 | 16 | const FluxStoreGroup = require('FluxStoreGroup'); 17 | 18 | function shallowArrayEqual(a: Array, b: Array): boolean { 19 | if (a === b) { 20 | return true; 21 | } 22 | if (a.length !== b.length) { 23 | return false; 24 | } 25 | for (let i = 0; i < a.length; i++) { 26 | if (a[i] !== b[i]) { 27 | return false; 28 | } 29 | } 30 | return true; 31 | } 32 | 33 | class FluxContainerSubscriptions { 34 | _callbacks: Array<() => void>; 35 | _storeGroup: ?FluxStoreGroup; 36 | _stores: ?Array; 37 | _tokens: ?Array<{remove: () => void}>; 38 | 39 | constructor() { 40 | this._callbacks = []; 41 | } 42 | 43 | setStores(stores: Array): void { 44 | if (this._stores && shallowArrayEqual(this._stores, stores)) { 45 | return; 46 | } 47 | this._stores = stores; 48 | this._resetTokens(); 49 | this._resetStoreGroup(); 50 | 51 | let changed = false; 52 | let changedStores = []; 53 | 54 | if (__DEV__) { 55 | // Keep track of the stores that changed for debugging purposes only 56 | this._tokens = stores.map((store) => 57 | store.addListener(() => { 58 | changed = true; 59 | changedStores.push(store); 60 | }), 61 | ); 62 | } else { 63 | const setChanged = () => { 64 | changed = true; 65 | }; 66 | this._tokens = stores.map((store) => store.addListener(setChanged)); 67 | } 68 | 69 | const callCallbacks = () => { 70 | if (changed) { 71 | this._callbacks.forEach((fn) => fn()); 72 | changed = false; 73 | if (__DEV__) { 74 | // Uncomment this to print the stores that changed. 75 | // console.log(changedStores); 76 | changedStores = []; 77 | } 78 | } 79 | }; 80 | this._storeGroup = new FluxStoreGroup(stores, callCallbacks); 81 | } 82 | 83 | addListener(fn: () => void): void { 84 | this._callbacks.push(fn); 85 | } 86 | 87 | reset(): void { 88 | this._resetTokens(); 89 | this._resetStoreGroup(); 90 | this._resetCallbacks(); 91 | this._resetStores(); 92 | } 93 | 94 | _resetTokens() { 95 | if (this._tokens) { 96 | this._tokens.forEach((token) => token.remove()); 97 | this._tokens = null; 98 | } 99 | } 100 | 101 | _resetStoreGroup(): void { 102 | if (this._storeGroup) { 103 | this._storeGroup.release(); 104 | this._storeGroup = null; 105 | } 106 | } 107 | 108 | _resetStores(): void { 109 | this._stores = null; 110 | } 111 | 112 | _resetCallbacks(): void { 113 | this._callbacks = []; 114 | } 115 | } 116 | 117 | module.exports = FluxContainerSubscriptions; 118 | -------------------------------------------------------------------------------- /src/stores/FluxReduceStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | * 8 | * @providesModule FluxReduceStore 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type Dispatcher from 'Dispatcher'; 15 | 16 | const FluxStore = require('FluxStore'); 17 | 18 | const abstractMethod = require('abstractMethod'); 19 | const invariant = require('invariant'); 20 | 21 | /** 22 | * This is the basic building block of a Flux application. All of your stores 23 | * should extend this class. 24 | * 25 | * class CounterStore extends FluxReduceStore { 26 | * getInitialState(): number { 27 | * return 1; 28 | * } 29 | * 30 | * reduce(state: number, action: Object): number { 31 | * switch(action.type) { 32 | * case: 'add': 33 | * return state + action.value; 34 | * case: 'double': 35 | * return state * 2; 36 | * default: 37 | * return state; 38 | * } 39 | * } 40 | * } 41 | */ 42 | class FluxReduceStore extends FluxStore { 43 | _state: TState; 44 | 45 | constructor(dispatcher: Dispatcher) { 46 | super(dispatcher); 47 | this._state = this.getInitialState(); 48 | } 49 | 50 | /** 51 | * Getter that exposes the entire state of this store. If your state is not 52 | * immutable you should override this and not expose _state directly. 53 | */ 54 | getState(): TState { 55 | return this._state; 56 | } 57 | 58 | /** 59 | * Constructs the initial state for this store. This is called once during 60 | * construction of the store. 61 | */ 62 | getInitialState(): TState { 63 | return abstractMethod('FluxReduceStore', 'getInitialState'); 64 | } 65 | 66 | /** 67 | * Used to reduce a stream of actions coming from the dispatcher into a 68 | * single state object. 69 | */ 70 | reduce(state: TState, action: Object): TState { 71 | return abstractMethod('FluxReduceStore', 'reduce'); 72 | } 73 | 74 | /** 75 | * Checks if two versions of state are the same. You do not need to override 76 | * this if your state is immutable. 77 | */ 78 | areEqual(one: TState, two: TState): boolean { 79 | return one === two; 80 | } 81 | 82 | __invokeOnDispatch(action: Object): void { 83 | this.__changed = false; 84 | 85 | // Reduce the stream of incoming actions to state, update when necessary. 86 | const startingState = this._state; 87 | const endingState = this.reduce(startingState, action); 88 | 89 | // This means your ending state should never be undefined. 90 | invariant( 91 | endingState !== undefined, 92 | '%s returned undefined from reduce(...), did you forget to return ' + 93 | 'state in the default case? (use null if this was intentional)', 94 | this.constructor.name, 95 | ); 96 | 97 | if (!this.areEqual(startingState, endingState)) { 98 | this._state = endingState; 99 | 100 | // `__emitChange()` sets `this.__changed` to true and then the actual 101 | // change will be fired from the emitter at the end of the dispatch, this 102 | // is required in order to support methods like `hasChanged()` 103 | this.__emitChange(); 104 | } 105 | 106 | if (this.__changed) { 107 | this.__emitter.emit(this.__changeEvent); 108 | } 109 | } 110 | } 111 | 112 | module.exports = FluxReduceStore; 113 | -------------------------------------------------------------------------------- /src/stores/FluxStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | * 8 | * @providesModule FluxStore 9 | * @flow 10 | */ 11 | 12 | 'use strict'; 13 | 14 | import type Dispatcher from 'Dispatcher'; 15 | 16 | const {EventEmitter} = require('fbemitter'); 17 | 18 | const invariant = require('invariant'); 19 | 20 | /** 21 | * This class represents the most basic functionality for a FluxStore. Do not 22 | * extend this store directly; instead extend FluxReduceStore when creating a 23 | * new store. 24 | */ 25 | class FluxStore { 26 | // private 27 | _dispatchToken: string; 28 | 29 | // protected, available to subclasses 30 | __changed: boolean; 31 | __changeEvent: string; 32 | __className: any; 33 | __dispatcher: Dispatcher; 34 | __emitter: EventEmitter; 35 | 36 | constructor(dispatcher: Dispatcher): void { 37 | this.__className = this.constructor.name; 38 | 39 | this.__changed = false; 40 | this.__changeEvent = 'change'; 41 | this.__dispatcher = dispatcher; 42 | this.__emitter = new EventEmitter(); 43 | this._dispatchToken = dispatcher.register((payload) => { 44 | this.__invokeOnDispatch(payload); 45 | }); 46 | } 47 | 48 | addListener(callback: (eventType?: string) => void): {remove: () => void} { 49 | return this.__emitter.addListener(this.__changeEvent, callback); 50 | } 51 | 52 | getDispatcher(): Dispatcher { 53 | return this.__dispatcher; 54 | } 55 | 56 | /** 57 | * This exposes a unique string to identify each store's registered callback. 58 | * This is used with the dispatcher's waitFor method to declaratively depend 59 | * on other stores updating themselves first. 60 | */ 61 | getDispatchToken(): string { 62 | return this._dispatchToken; 63 | } 64 | 65 | /** 66 | * Returns whether the store has changed during the most recent dispatch. 67 | */ 68 | hasChanged(): boolean { 69 | invariant( 70 | this.__dispatcher.isDispatching(), 71 | '%s.hasChanged(): Must be invoked while dispatching.', 72 | this.__className, 73 | ); 74 | return this.__changed; 75 | } 76 | 77 | __emitChange(): void { 78 | invariant( 79 | this.__dispatcher.isDispatching(), 80 | '%s.__emitChange(): Must be invoked while dispatching.', 81 | this.__className, 82 | ); 83 | this.__changed = true; 84 | } 85 | 86 | /** 87 | * This method encapsulates all logic for invoking __onDispatch. It should 88 | * be used for things like catching changes and emitting them after the 89 | * subclass has handled a payload. 90 | */ 91 | __invokeOnDispatch(payload: Object): void { 92 | this.__changed = false; 93 | this.__onDispatch(payload); 94 | if (this.__changed) { 95 | this.__emitter.emit(this.__changeEvent); 96 | } 97 | } 98 | 99 | /** 100 | * The callback that will be registered with the dispatcher during 101 | * instantiation. Subclasses must override this method. This callback is the 102 | * only way the store receives new data. 103 | */ 104 | __onDispatch(payload: Object): void { 105 | invariant( 106 | false, 107 | '%s has not overridden FluxStore.__onDispatch(), which is required', 108 | this.__className, 109 | ); 110 | } 111 | } 112 | 113 | module.exports = FluxStore; 114 | -------------------------------------------------------------------------------- /src/stores/__tests__/FluxReduceStore-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @emails oncall+uiecommd 10 | */ 11 | 12 | import Dispatcher from 'Dispatcher'; 13 | import FluxReduceStore from 'FluxReduceStore'; 14 | import Immutable from 'immutable'; 15 | 16 | class FooStore extends FluxReduceStore { 17 | getInitialState() { 18 | return Immutable.Map(); 19 | } 20 | 21 | reduce(state, action) { 22 | switch (action.type) { 23 | case 'foo': 24 | return state.set('foo', action.foo); 25 | case 'bar': 26 | return state.set('bar', action.bar); 27 | case 'foobar': 28 | return state.set('foo', action.foo).set('bar', action.bar); 29 | case 'boom': 30 | return state.clear(); 31 | default: 32 | return state; 33 | } 34 | } 35 | } 36 | 37 | describe('FluxReduceStore', () => { 38 | var dispatch; 39 | var onChange; 40 | var store; 41 | 42 | beforeEach(() => { 43 | var dispatcher = new Dispatcher(); 44 | store = new FooStore(dispatcher); 45 | dispatch = dispatcher.dispatch.bind(dispatcher); 46 | store.__emitter.emit = jest.fn(); 47 | onChange = store.__emitter.emit; 48 | }); 49 | 50 | it('should respond to actions', () => { 51 | expect(store.getState().get('foo')).toBe(undefined); 52 | expect(store.getState().has('foo')).toBe(false); 53 | 54 | dispatch({ 55 | type: 'foo', 56 | foo: 100, 57 | }); 58 | 59 | expect(store.getState().get('foo')).toBe(100); 60 | expect(store.getState().has('foo')).toBe(true); 61 | }); 62 | 63 | it('should only emit one change for multiple cache changes', () => { 64 | dispatch({ 65 | type: 'foo', 66 | foo: 100, 67 | }); 68 | 69 | expect(onChange.mock.calls.length).toBe(1); 70 | expect(store.getState().get('foo')).toBe(100); 71 | 72 | dispatch({ 73 | type: 'foobar', 74 | foo: 200, 75 | bar: 400, 76 | }); 77 | 78 | expect(onChange.mock.calls.length).toBe(2); 79 | expect(store.getState().get('foo')).toBe(200); 80 | expect(store.getState().get('bar')).toBe(400); 81 | }); 82 | 83 | it('should not emit for empty changes', () => { 84 | dispatch({ 85 | type: 'foo', 86 | foo: 100, 87 | }); 88 | 89 | expect(onChange.mock.calls.length).toBe(1); 90 | expect(store.getState().get('foo')).toBe(100); 91 | 92 | dispatch({ 93 | type: 'foo', 94 | foo: 100, 95 | }); 96 | 97 | expect(onChange.mock.calls.length).toBe(1); 98 | expect(store.getState().get('foo')).toBe(100); 99 | }); 100 | 101 | it('should clear the cache', () => { 102 | dispatch({ 103 | type: 'foo', 104 | foo: 100, 105 | }); 106 | 107 | expect(onChange.mock.calls.length).toBe(1); 108 | expect(store.getState().get('foo')).toBe(100); 109 | 110 | dispatch({type: 'boom'}); 111 | 112 | expect(onChange.mock.calls.length).toBe(2); 113 | expect(store.getState().get('foo')).toBe(undefined); 114 | expect(store.getState().has('foo')).toBe(false); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/stores/__tests__/FluxStore-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @emails oncall+uiecommd 10 | * @typechecks 11 | */ 12 | 13 | import FluxStore from 'FluxStore'; 14 | import Dispatcher from 'Dispatcher'; 15 | import {EventEmitter} from 'fbemitter'; 16 | 17 | jest.mock('fbemitter'); 18 | 19 | class TestFluxStore extends FluxStore { 20 | __onDispatch(action) { 21 | switch (action.type) { 22 | case 'store-will-change-state': 23 | this.__emitChange(); 24 | break; 25 | default: 26 | // no op 27 | } 28 | } 29 | } 30 | 31 | class IncompleteFluxStore extends FluxStore {} 32 | 33 | class IllegalFluxStore extends FluxStore { 34 | __onDispatch() {} 35 | illegalEmit() { 36 | this.__emitChange(); 37 | } 38 | } 39 | 40 | describe('FluxStore', () => { 41 | var dispatcher; 42 | var fluxStore; 43 | var mockEmit; 44 | var registeredCallback; 45 | 46 | beforeEach(() => { 47 | dispatcher = new Dispatcher(); 48 | dispatcher.register = jest 49 | .fn() 50 | .mockReturnValueOnce('ID_1') 51 | .mockReturnValueOnce('ID_2'); 52 | EventEmitter.mockClear(); 53 | fluxStore = new TestFluxStore(dispatcher); 54 | mockEmit = EventEmitter.mock.instances[0].emit; 55 | registeredCallback = dispatcher.register.mock.calls[0][0]; 56 | }); 57 | 58 | it('registers a callback with the dispatcher', () => { 59 | expect(dispatcher.register.mock.calls.length).toBe(1); 60 | }); 61 | 62 | it('requires that subclasses override the __onDispatch() method', () => { 63 | new IncompleteFluxStore(dispatcher); 64 | var incompleteStoreCallback = dispatcher.register.mock.calls[1][0]; 65 | expect(() => incompleteStoreCallback({type: 'action-type'})).toThrow(); 66 | expect(() => registeredCallback({type: 'action-type'})).not.toThrow(); 67 | }); 68 | 69 | it('throws when __emitChange() is invoked outside of a dispatch', () => { 70 | var illegalFluxStore = new IllegalFluxStore(dispatcher); 71 | expect(() => illegalFluxStore.illegalEmit()).toThrow(); 72 | }); 73 | 74 | it('throws when hasChanged() is invoked outside of a dispatch', () => { 75 | expect(() => fluxStore.hasChanged()).toThrow(); 76 | }); 77 | 78 | it('emits an event on state change', () => { 79 | dispatcher.isDispatching = jest.fn().mockReturnValue(true); 80 | registeredCallback({type: 'store-will-change-state'}); 81 | expect(mockEmit.mock.calls.length).toBe(1); 82 | expect(mockEmit.mock.calls[0][0]).toBe('change'); 83 | }); 84 | 85 | it('exposes whether the state has changed during current dispatch', () => { 86 | dispatcher.isDispatching = jest.fn(); 87 | dispatcher.isDispatching.mockReturnValue(true); 88 | registeredCallback({type: 'store-will-change-state'}); 89 | expect(fluxStore.hasChanged()).toBe(true); 90 | registeredCallback({type: 'store-will-ignore'}); 91 | expect(fluxStore.hasChanged()).toBe(false); 92 | }); 93 | 94 | it('exposes the dispatch token in a getter', () => { 95 | expect(fluxStore.getDispatchToken()).toBeTruthy(); 96 | }); 97 | 98 | it('wraps EventEmitter.addListener() with an addListener() method', () => { 99 | var mockAddListener = EventEmitter.mock.instances[0].addListener; 100 | mockAddListener.mockImplementation(() => { 101 | return {}; 102 | }); 103 | fluxStore.addListener(() => {}); 104 | expect(mockAddListener.mock.calls.length).toBe(1); 105 | expect(typeof mockAddListener.mock.calls[0][0]).toBe('string'); 106 | expect(typeof mockAddListener.mock.calls[0][1]).toBe('function'); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/utils/abstractMethod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule abstractMethod 10 | * @flow 11 | */ 12 | 13 | 'use strict'; 14 | 15 | var invariant = require('invariant'); 16 | 17 | function abstractMethod(className: string, methodName: string): T { 18 | invariant( 19 | false, 20 | 'Subclasses of %s must override %s() with their own implementation.', 21 | className, 22 | methodName, 23 | ); 24 | } 25 | 26 | module.exports = abstractMethod; 27 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | module.exports.Container = require('./lib/FluxContainer'); 11 | module.exports.Mixin = require('./lib/FluxMixinLegacy'); 12 | module.exports.ReduceStore = require('./lib/FluxReduceStore'); 13 | module.exports.Store = require('./lib/FluxStore'); 14 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /build 6 | 7 | # generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using Docusaurus 2, a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=1 yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | title: 'Flux', 10 | tagline: 'Application architecture for building user interfaces', 11 | url: 'https://facebookarchive.github.io', 12 | baseUrl: '/flux/', 13 | favicon: 'img/favicon.ico', 14 | organizationName: 'facebookarchive', 15 | projectName: 'flux', 16 | themeConfig: { 17 | announcementBar: { 18 | id: 'support_ukraine', 19 | content: 20 | 'The Flux project has been archived and no further changes will be made.', 21 | backgroundColor: '#20232a', 22 | textColor: '#fff', 23 | isCloseable: false, 24 | }, 25 | algolia: { 26 | appId: 'YDWP2C57PH', 27 | apiKey: '05bee0fdd678621b57cf7f8881751543', 28 | indexName: 'flux', 29 | }, 30 | navbar: { 31 | title: 'Flux', 32 | logo: { 33 | alt: 'Flux Logo', 34 | src: 'img/flux-logo-color.svg', 35 | }, 36 | items: [ 37 | {to: 'docs/overview', label: 'Docs', position: 'left'}, 38 | {to: 'support', label: 'Support', position: 'left'}, 39 | { 40 | href: 'https://github.com/facebookarchive/flux', 41 | label: 'GitHub', 42 | position: 'right', 43 | }, 44 | ], 45 | }, 46 | footer: { 47 | style: 'dark', 48 | logo: { 49 | alt: 'Facebook Open Source Logo', 50 | src: 'https://docusaurus.io/img/meta_opensource_logo_negative.svg', 51 | }, 52 | links: [ 53 | { 54 | title: 'Docs', 55 | items: [ 56 | { 57 | label: 'Introduction', 58 | to: 'docs/overview', 59 | }, 60 | ], 61 | }, 62 | { 63 | title: 'Community', 64 | items: [ 65 | { 66 | label: 'Stack Overflow', 67 | href: 'https://stackoverflow.com/questions/tagged/flux', 68 | }, 69 | ], 70 | }, 71 | { 72 | title: 'Social', 73 | items: [ 74 | { 75 | label: 'GitHub', 76 | href: 'https://github.com/facebookarchive/flux', 77 | }, 78 | ], 79 | }, 80 | { 81 | title: 'Legal', 82 | // Please do not remove the privacy and terms, it's a legal requirement. 83 | items: [ 84 | { 85 | label: 'Privacy', 86 | href: 'https://opensource.facebook.com/legal/privacy/', 87 | target: '_blank', 88 | rel: 'noreferrer noopener', 89 | }, 90 | { 91 | label: 'Terms', 92 | href: 'https://opensource.facebook.com/legal/terms/', 93 | target: '_blank', 94 | rel: 'noreferrer noopener', 95 | }, 96 | ], 97 | }, 98 | ], 99 | copyright: `Copyright © ${new Date().getFullYear()} Meta Platforms, Inc.`, 100 | }, 101 | }, 102 | presets: [ 103 | [ 104 | '@docusaurus/preset-classic', 105 | { 106 | docs: { 107 | path: '../docs', 108 | sidebarPath: require.resolve('./sidebars.js'), 109 | editUrl: 'https://github.com/facebookarchive/flux/edit/master/docs/', 110 | showLastUpdateAuthor: true, 111 | showLastUpdateTime: true, 112 | }, 113 | theme: { 114 | customCss: require.resolve('./src/css/custom.css'), 115 | }, 116 | }, 117 | ], 118 | ], 119 | }; 120 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-website", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy" 11 | }, 12 | "dependencies": { 13 | "@docusaurus/core": "^2.3.1", 14 | "@docusaurus/preset-classic": "^2.3.1", 15 | "classnames": "^2.2.6", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | module.exports = { 9 | docs: { 10 | 'Quick Start': ['overview', 'in-depth-overview'], 11 | Reference: ['dispatcher', 'flux-utils'], 12 | Resources: ['videos', 'related-libraries'], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /** 9 | * Any CSS included here will be global. The classic template 10 | * bundles Infima by default. Infima is a CSS framework designed to 11 | * work well for content-centric websites. 12 | */ 13 | 14 | /* You can override the default Infima variables here. */ 15 | :root { 16 | --ifm-color-primary: #318435; 17 | --ifm-color-primary-dark: rgb(44, 119, 48); 18 | --ifm-color-primary-darker: rgb(42, 112, 45); 19 | --ifm-color-primary-darkest: rgb(34, 92, 37); 20 | --ifm-color-primary-light: rgb(80, 150, 83); 21 | --ifm-color-primary-lighter: rgb(111, 169, 114); 22 | --ifm-color-primary-lightest: rgb(152, 194, 154); 23 | 24 | --ifm-footer-padding-vertical: 4rem; 25 | } 26 | 27 | .navbar .navbar__logo { 28 | height: auto; 29 | width: 50px; 30 | } 31 | 32 | .video-container { 33 | height: 0; 34 | margin: 0; 35 | margin-bottom: 30px; 36 | overflow: hidden; 37 | padding-bottom: 56.25%; 38 | padding-top: 30px; 39 | position: relative; 40 | } 41 | 42 | .video-container iframe { 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | width: 100%; 47 | height: 100%; 48 | } 49 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react'; 9 | import classnames from 'classnames'; 10 | import Layout from '@theme/Layout'; 11 | import Link from '@docusaurus/Link'; 12 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 13 | import useBaseUrl from '@docusaurus/useBaseUrl'; 14 | import styles from './styles.module.css'; 15 | 16 | const features = [ 17 | { 18 | title: <>What is Flux?, 19 | imageUrl: 'img/undraw_building_blocks.svg', 20 | description: ( 21 | <> 22 | Flux is the application architecture that Facebook uses for building 23 | client-side web applications. 24 | 25 | ), 26 | }, 27 | { 28 | title: <>What does it do?, 29 | imageUrl: 'img/undraw_react.svg', 30 | description: ( 31 | <> 32 | It complements React's composable view components by utilizing a 33 | unidirectional data flow. 34 | 35 | ), 36 | }, 37 | { 38 | title: <>How do I use it?, 39 | imageUrl: 'img/undraw_programming.svg', 40 | description: ( 41 | <> 42 | It's more of a pattern rather than a formal framework, and you can start 43 | using Flux immediately without a lot of new code. 44 | 45 | ), 46 | }, 47 | ]; 48 | 49 | function VideoContainer() { 50 | return ( 51 |
    52 |
    53 |
    54 |

    Brief Introduction into Flux

    55 |
    56 |