├── .gitignore ├── README.md ├── book.json ├── contributes.md ├── docs ├── Glossary.md ├── README.md ├── Troubleshooting.md ├── advanced │ ├── AsyncActions.md │ ├── AsyncFlow.md │ ├── ExampleRedditAPI.md │ ├── Middleware.md │ ├── NextSteps.md │ ├── README.md │ └── UsageWithReactRouter.md ├── api │ ├── README.md │ ├── Store.md │ ├── applyMiddleware.md │ ├── bindActionCreators.md │ ├── combineReducers.md │ ├── compose.md │ └── createStore.md ├── basics │ ├── Actions.md │ ├── DataFlow.md │ ├── ExampleTodoList.md │ ├── README.md │ ├── Reducers.md │ ├── Store.md │ └── UsageWithReact.md ├── introduction │ ├── Ecosystem.md │ ├── Examples.md │ ├── Motivation.md │ ├── PriorArt.md │ ├── README.md │ └── ThreePrinciples.md └── recipes │ ├── ComputingDerivedData.md │ ├── MigratingToRedux.md │ ├── README.md │ ├── ReducingBoilerplate.md │ └── WritingTests.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | _book/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Redux 中文文档](http://github.com/camsong/redux-in-chinese) 2 | 3 | 在线 Gitbook 地址:http://camsong.github.io/redux-in-chinese/ 4 | 5 | 英文原版:http://rackt.github.io/redux/ 6 | 7 | **翻译正在进行中,[加入我们](#加入翻译)** 8 | 9 | Redux 是 JavaScript 状态容器,提供可预测化的状态管理。 10 | 11 | 可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供 12 | 超爽的开发体验,比如有一个[时间旅行调试器可以编辑后实时预览](https://github.com/gaearon/redux-devtools)。 13 | 14 | Redux 除了和 [React](https://facebook.github.io/react/) 一起用外,还支持其它界面库。 15 | 体小精悍(只有2kB)且没有任何依赖。 16 | 17 | ### 评价 18 | 19 | >[“Love what you’re doing with Redux”](https://twitter.com/jingc/status/616608251463909376) 20 | >Jing Chen,Flux 作者 21 | 22 | >[“I asked for comments on Redux in FB's internal JS discussion group, and it was universally praised. Really awesome work.”](https://twitter.com/fisherwebdev/status/616286955693682688) 23 | >Bill Fisher,Flux 作者 24 | 25 | >[“It's cool that you are inventing a better Flux by not doing Flux at all.”](https://twitter.com/andrestaltz/status/616271392930201604) 26 | >André Staltz,Cycle 作者 27 | 28 | ### 开发经历 29 | 30 | Redux 的开发最早开始于我在准备 React Europe 演讲[热加载与时间旅行](https://www.youtube.com/watch?v=xsSnOQynTHs)的时候,当初的目标是创建一个状态管理库,来提供最简化 API,但同时做到行为的完全可预测,因此才得以实现日志打印,热加载,时间旅行,同构应用,录制和重放,而不需要任何开发参与。 31 | 32 | ### 启示 33 | 34 | Redux 由 [Flux](http://facebook.github.io/flux/) 演变而来,但受 [Elm](http://elm-lang.org/guide/architecture) 的启发,避开了 Flux 的复杂性。不管你有没有使用过它们,只需几分钟就能上手 Redux。 35 | 36 | ### 安装 37 | 38 | 安装稳定版: 39 | 40 | ``` 41 | npm install --save redux 42 | ``` 43 | 44 | 多数情况下,你还需要使用 [React 绑定库](http://github.com/gaearon/react-redux)和[开发者工具](http://github.com/gaearon/redux-devtools)。 45 | 46 | ``` 47 | npm install --save react-redux 48 | npm install --save-dev redux-devtools 49 | ``` 50 | 51 | ### 要点 52 | 53 | 应用中所有的 state 都以一个对象树的形式储存在一个单一的 *store* 中。 54 | 惟一改变 state 的办法是触发 *action*,一个描述发生什么的对象。 55 | 为了描述 action 如何改变 state 树,你需要编写 *reducers*。 56 | 57 | 就是这样! 58 | 59 | ```js 60 | import { createStore } from 'redux'; 61 | 62 | /** 63 | * 这是一个 reducer,形式为 (state, action) => state 的纯函数。 64 | * 描述了 action 如何把 state 转变成下一个 state。 65 | * 66 | * state 的形式取决于你,可以是基本类型、数组、对象、 67 | * 甚至是 Immutable.js 生成的数据结构。惟一的要点是 68 | * 当 state 变化时需要返回全新的对象,而不是修改传入的参数。 69 | * 70 | * 下面例子使用 `switch` 语句和字符串来做判断,但你可以写帮助类(helper) 71 | * 根据不同的约定(如方法映射)来判断,只要适用你的项目即可。 72 | */ 73 | function counter(state = 0, action) { 74 | switch (action.type) { 75 | case 'INCREMENT': 76 | return state + 1; 77 | case 'DECREMENT': 78 | return state - 1; 79 | default: 80 | return state; 81 | } 82 | } 83 | 84 | // 创建 Redux store 来存放应用的状态。 85 | // API 是 { subscribe, dispatch, getState }。 86 | let store = createStore(counter); 87 | 88 | // 可以手动订阅更新,也可以事件绑定到视图层。 89 | store.subscribe(() => 90 | console.log(store.getState()) 91 | ); 92 | 93 | // 改变内部 state 惟一方法是 dispatch 一个 action。 94 | // action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行 95 | store.dispatch({ type: 'INCREMENT' }); 96 | // 1 97 | store.dispatch({ type: 'INCREMENT' }); 98 | // 2 99 | store.dispatch({ type: 'DECREMENT' }); 100 | // 1 101 | ``` 102 | 你应该把要做的修改变成一个普通对象,这个对象被叫做 *action*,而不是直接修改 state。然后编写专门的函数来决定每个 action 如何改变应用的 state,这个函数被叫做 *reducer*。 103 | 104 | 如果你以前使用 Flux,那么你只需要注意一个重要的区别。Redux 没有 Dispatcher 且不支持多个 store。相反,只有一个单一的 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。 105 | 106 | 用这个架构开发计数器有点杀鸡用牛刀,但它的美在于做复杂应用和庞大系统时优秀的扩展能力。由于它可以用 action 追溯应用的每一次修改,因此才有强大的开发工具。如录制用户会话并回放所有 action 来重现它。 107 | 108 | ### 文档 109 | 110 | * [介绍](http://camsong.github.io/redux-in-chinese//docs/introduction/index.html) 111 | * [基础](http://camsong.github.io/redux-in-chinese//docs/basics/index.html) 112 | * [高级](http://camsong.github.io/redux-in-chinese//docs/advanced/index.html) 113 | * [技巧](http://camsong.github.io/redux-in-chinese//docs/recipes/index.html) 114 | * [排错](http://camsong.github.io/redux-in-chinese//docs/Troubleshooting.html) 115 | * [词汇表](http://camsong.github.io/redux-in-chinese//docs/Glossary.html) 116 | * [API 文档](http://camsong.github.io/redux-in-chinese//docs/api/index.html) 117 | 118 | ### 示例 119 | 120 | * [Counter](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#counter) ([source](https://github.com/rackt/redux/tree/master/examples/counter)) 121 | * [TodoMVC](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#todomvc) ([source](https://github.com/rackt/redux/tree/master/examples/todomvc)) 122 | * [Async](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#async) ([source](https://github.com/rackt/redux/tree/master/examples/async)) 123 | * [Real World](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#real-world) ([source](https://github.com/rackt/redux/tree/master/examples/real-world)) 124 | 125 | 如果你是 NPM 新手,创建和运行一个新的项目有难度,或者不知道上面的代码应该放到哪里使用,请下载 [simplest-redux-example](https://github.com/jackielii/simplest-redux-example) 这个示例,它是一个集成了 React、Browserify 和 Redux 的最简化的示例项目。 126 | 127 | ### 交流 128 | 129 | 打开 Slack,加入 [Reactiflux](http://reactiflux.com/) 中的 **#redux** 频道。 130 | 131 | ### 感谢 132 | 133 | * [Elm 架构](https://github.com/evancz/elm-architecture-tutorial) 介绍了使用 reducers 来操作 state 数据; 134 | * [Turning the database inside-out](http://blog.confluent.io/2015/03/04/turning-the-database-inside-out-with-apache-samza/) 大开脑洞; 135 | * [ClojureScript 里使用 Figwheel](http://www.youtube.com/watch?v=j-kj2qwJa_E) for convincing me that re-evaluation should “just work”; 136 | * [Webpack](https://github.com/webpack/docs/wiki/hot-module-replacement-with-webpack) 热模块替换; 137 | * [Flummox](https://github.com/acdlite/flummox) 教我在 Flux 里去掉样板文件和单例对象; 138 | * [disto](https://github.com/threepointone/disto) 演示使用热加载 Stores 的可行性; 139 | * [NuclearJS](https://github.com/optimizely/nuclear-js) 证明这样的架构性能可以很好; 140 | * [Om](https://github.com/omcljs/om) 普及 state 惟一原子化的思想。 141 | * [Cycle](https://github.com/staltz/cycle) 介绍了 function 是如何在很多场景都是最好的工具; 142 | * [React](https://github.com/facebook/react) 实践启迪。 143 | 144 | 145 | ## 加入翻译 146 | 147 | 非常感谢你的关注,如果你也想为 Redux 推广贡献一份力量,欢迎加入我们一起翻译。目前翻译工作还没有完成,校对工作更是才刚刚开始。你可以按照以下的步骤进行认领提交。在认领之前,你需要先到 fork 一份代码,建议使用 Markdown 类编辑器开启横向双列模式, 148 | 149 | ### 认领制度 150 | 151 | 你应该在翻译之前进行翻译认领来保证没有人和你重复工作。很简单,只要在 contributes.md 中找到或者加入你想翻译的文章的资料, 152 | 153 | 加入: 154 | 155 | ``` 156 | work_in_progress: true 157 | contributor: +your_github_acount+ 158 | ``` 159 | 160 | 之后,发个 pull request 过来就代表你成功的占到了一个坑,你可以开始慢慢的翻译自己的文章了。 161 | 162 | 你可以在 contributor 一栏中加入自己的 github 用户名,以便其他人可以联系到你。另外如果你占坑太久你的占位可能会被取消。尽量在半个月内完成你的工作。 163 | 164 | ### 翻译约定 165 | 166 | * 专有名词保持大写:HTML, HAML, SASS, REST... 等等。 167 | * 英文和数字与中文之间要留空格。中文标点符号和中文之间不需要留空格。 168 | * 使用中文的标点符号。句号是 `。` 不是 `.`,破折号是 `——` 不是 `-`。 169 | * 英文有斜体、中文没有,可用强调取代 `**强调**`。 170 | * 译文和原文行数应保持一致,以便于后期同步更新。 171 | * 译完自己读一次,看看像不像中文。 172 | 173 | ### 约定翻译的名词 174 | 175 | 为了免除误解,这些词第一次出现时可以用 `()` 来显示原文。 176 | 177 | 英文 | 中文 178 | ------------ | ------------- 179 | plain object | 普通对象 180 | manage | 管理 181 | compose | 组合 182 | <<<<<<< HEAD 183 | action creator | action 生成器 184 | dispatch | 发起 185 | note | 注意 186 | hold | 维持 187 | state shape | state 结构 188 | handle | 处理 189 | boilerplate | 样板代码 190 | normalized | 规格化 191 | function | 函数/方法? 192 | composition | 合成 193 | helper utility | 辅助工具 194 | ======= 195 | action creator | action 创建函数 196 | dispatch | 发起 197 | note | 注意 198 | hold | 维持 199 | state tree | state 树? 200 | state shape | state 结构 201 | handle | 处理 202 | boilerplate | 样板代码 203 | normalized | 范式化 204 | function | 函数? 205 | composition | 合成 206 | helper utility | 辅助工具 207 | this won’t work | 这样做行不通 208 | this will work | 这样做行得通 209 | tips | 小贴士 210 | >>>>>>> upstream/master 211 | 212 | ### 保留不译的名词 213 | 214 | 前端开发常用的专有名词,在不造成读者理解困难的情况下,尽量保持原汁原味。 215 | 216 | 英文 | 说明 217 | ----- | ------ 218 | action | 动作 219 | reducer | - 220 | store | - 221 | middleware | 中间件 222 | dispatcher | 分发器 223 | state | 状态 224 | props | 属性 225 | <<<<<<< HEAD 226 | ======= 227 | UI | 用户界面 228 | >>>>>>> upstream/master 229 | 230 | ### 与原文同步机制 231 | 232 | TBD 233 | 234 | ### 翻译流程 235 | 236 | 一、fork 本 repo 237 | 238 | 二、如果是新翻译章节,应参照对应的[原文](https://github.com/rackt/redux/tree/master/docs)进行翻译;如果是校对则直接修改 239 | 240 | 三、翻译时启动 watch 来实时看结果 241 | ``` 242 | npm run watch 243 | ``` 244 | 打开:[localhost:4000](http://localhost:4000) 245 | 246 | 四、提交并发 Pull Request 247 | 248 | 五、fork 后的 repo 如何同步本 repo? 249 | 250 | ``` 251 | // 添加 upstream 源,只需执行一次 252 | git remote add upstream git@github.com:camsong/redux-in-chinese.git 253 | // 拉取远程代码 254 | git pull upstream master 255 | // 更新 fork 仓库 256 | git push origin master 257 | ``` 258 | 259 | 更多参考:https://help.github.com/articles/syncing-a-fork/ 260 | 261 | ### 建议与反馈 262 | 欢迎任何建议!直接开一个 github issues 就可以了。 263 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "structure": { 3 | "summary": "docs/README.md" 4 | }, 5 | "plugins": ["ga"], 6 | "pluginsConfig": { 7 | "ga": { 8 | "token": "UA-66122997-1" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /contributes.md: -------------------------------------------------------------------------------- 1 | 翻译计划列表 2 | ============ 3 | 4 | ``` 5 | url: advanced/Middleware.html 6 | work_in_progress: true 7 | contributor: jasonslyvia 8 | ``` 9 | 10 | ``` 11 | url: advanced/AsyncActions.html 12 | work_in_progress: true 13 | contributor: jasonslyvia 14 | ``` 15 | 16 | ``` 17 | url: advanced/AsyncFlow.html 18 | work_in_progress: true 19 | contributor: camsong 20 | ``` 21 | 22 | ``` 23 | url: advanced/ExampleRedditAPI.html 24 | work_in_progress: true 25 | contributor: demohi 26 | ``` 27 | 28 | ``` 29 | url: recipes/MigratingToRedux.html 30 | work_in_progress: true 31 | contributor: demohi 32 | ``` 33 | 34 | ``` 35 | url: recipes/ReducingBoilerplate.html 36 | work_in_progress: true 37 | contributor: omytea 38 | ``` 39 | 40 | ``` 41 | url: recipes/ComputingDerivedData.html 42 | work_in_progress: false 43 | ``` 44 | 45 | ``` 46 | url: recipes/WritingTests.html 47 | work_in_progress: true 48 | contributor: omytea 49 | ``` 50 | 51 | ``` 52 | url: recipes/MigratingToRedux.html 53 | work_in_progress: false 54 | ``` 55 | 56 | ``` 57 | url: Troubleshooting.html 58 | work_in_progress: false 59 | ``` 60 | 61 | ``` 62 | url: Glossary.html 63 | work_in_progress: true 64 | contributor: omytea 65 | ``` 66 | 67 | ``` 68 | url: api/index.html 69 | work_in_progress: false 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/Glossary.md: -------------------------------------------------------------------------------- 1 | # 词汇表 2 | 3 | 这是有关Redux中的一些核心概念的词汇表,以及他们的类型签名。这些类型使用了 [流标注法](http://flowtype.org/docs/quick-reference.html)进行记录。 4 | 5 | ## State 6 | 7 | ```js 8 | type State = any; 9 | ``` 10 | 11 | *State* (也叫 *state tree*) 是一个宽泛的概念,但是在 Redux API 中它通常与被 store 所管理的,可以被 [`getState()`](api/Store.md#getState) 返回的,单独 state 值相关。 它表示了一个 Redux应用的全部状态,通常为一个多层嵌套的对象。 12 | 13 | 约定俗成,顶层 state 为一个对象,或几个像 Map 那样的键-值集合,当然是任意类型的话也成。当然,你仍然可以尽可能保持状态的串行化。不要把什么都放进去导致无法容易地转换成 JSON 。 14 | 15 | ## Action 16 | 17 | ```js 18 | type Action = Object; 19 | ``` 20 | 21 | *Action* 是一个用以表示要改变的 state 的意图的普通对象。Action 是将数据拿到 store 里的唯一方法。无论是 UI 事件,网络回调,还是其他诸如 WebSocket 之类的其他源,任何数据都或多或少的被 dispatch 成 action 。 22 | 23 | 约定俗成,action 应该有一个 `type` 域指明了需要被演算的 action 类型。Type 可以被定义为常数从其他 module 中导入。比起用 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 表示 `type` 使用 String 是更好的方法因为 string 是可被串行化的。 24 | 25 | 除了 `type`之外,action 对象的结构其实完全取决于你自己。如果你感兴趣的话,请参考 [Flux Standard Action](https://github.com/acdlite/flux-standard-action) 作为如何组织 actions 的建议。 26 | 27 | 还有就是请看后面的 [异步 action](#async-action) 。 28 | 29 | ## Reducer 30 | 31 | ```js 32 | type Reducer = (state: S, action: A) => S; 33 | ``` 34 | 35 | *Reducer* (也叫 *reducing function*) 是一个接受累积运算和一个值,返回新的累积函数的函数。用来把一个集合 reduce 到一个单独值。 36 | 37 | Reducer 并不是 Redux 特有的——它是函数式编程中的一个基本概念。甚至大部分的非函数式语言比如 JavaScript,都有一个内建的 reduce API。在 JavaScript 中的话是 [`Array.prototype.reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce). 38 | 39 | 在 Redux 中,累计运算的结果是个 state 对象,被累积的值就是 action 。Reducer 由上一个 state 和一个 action 计算得到一个新 state 。它必须是 *纯函数* 也就是由完全相同的输入会返回完全相同的输出。它应该是没有副作用的。这使得一些很棒的功能诸如热重载和时间旅行成为可能。 40 | 41 | Reducer 是 Redux 之中最重要的概念。 42 | 43 | *不要在 reducer 中有 API 调用* 44 | 45 | ## dispatch function 46 | 47 | ```js 48 | type BaseDispatch = (a: Action) => Action; 49 | type Dispatch = (a: Action | AsyncAction) => any; 50 | ``` 51 | 52 | 一个 *dispatching function* (或者简单点叫 *dispatch function*) 是一个接收一个 action 或者[异步 action](#async-action)的函数,它可以或不可以分发一个或多个 action 到 store。 53 | 54 | 我们必须搞清 dispatch function 和由没有中间件的 store 实例提供的 base [`dispatch`](api/Store.md#dispatch) function 其中的区别。 55 | 56 | Base dispatch function *总是* 同步发 action 给 store 的 reducer,以及由 store 返回的上一个 state 计算出新 state。它期望 actions 会是一个准备好被 reducer 消费掉的普通对象。 57 | 58 | [中间件](#middleware) 封装了base dispatch function。它允许了 dispatch function 处理 action 之外的 [异步 action](#async-action) 。中间件可以被变形,延迟,忽略,以及其他在将 action 或异步 action 传递给下一个中间件之前作出解释。获取更多信息请往后看。 59 | 60 | ## Action Creator 61 | 62 | ```js 63 | type ActionCreator = (...args: any) => Action | AsyncAction; 64 | ``` 65 | 66 | *Action Creator* 很简单,就是一个创建 action 的函数。别把这两个概念搞混。Action 是一个信息的负载,而 action 创建者是一个创建 action 的工厂。 67 | 68 | 调用 action creator 只会生产出 action ,但不分发。你需要调用 store 的 [`dispatch`](api/Store.md#dispatch) function 才会真正引起变化。有时我们讲 *bound action creator* 意味着函数调用 action creator并立即将结果分发给一个特定的 store 实例。 69 | 70 | 如果 action 创建者需要读取当前状态、做出 API 调用、或引起诸如路由变位等副作用,应该返回一个 [异步 action](#async-action) 而不是 action 。 71 | 72 | ## 异步 Action 73 | 74 | ```js 75 | type AsyncAction = any; 76 | ``` 77 | 78 | *异步 action* 是一个发给分发函数,但还没有准备好被 reducer 消费的值。它会在被发往 base [`dispatch()`](api/Store.md#dispatch) function 之前,被 [中间件](#middleware) 变为一个或一组 action 。异步 actions 可以有多个 type ,取决于使用的中间件。通常为 Promise 或者 thunk 之类的异步原生,虽然没有被马上传给 reducer ,但是操作一旦完成就会触发 action 分发。 79 | 80 | ## 中间件 81 | 82 | ```js 83 | type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }; 84 | type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch; 85 | ``` 86 | 87 | 中间件是一个高阶函数,它将 [dispatch function](#dispatching-function) 组合并返回一个新的 dispatch function。它通常将 [异步 actions](#async-action) 变为 actions 。 88 | 89 | 中间件是使用了复合函数的可构建的。它可在 action 日志,表现副作用例如路由,或将异步 API 调用变为一组同步 actions。 90 | 91 | 请见 [`applyMiddleware(...middlewares)`](./api/applyMiddleware.md) 获取有关中间件的详细内容。 92 | 93 | ## Store 94 | 95 | ```js 96 | type Store = { 97 | dispatch: Dispatch; 98 | getState: () => State; 99 | subscribe: (listener: () => void) => () => void; 100 | getReducer: () => Reducer; 101 | replaceReducer: (reducer: Reducer) => void; 102 | }; 103 | ``` 104 | 105 | Store 是一个承载有应用 state tree 的对象。 106 | 一个 Redux 应用中应当只有一个 Store ,因为构建发生于 reducer 级。 107 | 108 | - [`dispatch(action)`](api/Store.md#dispatch) 是上面描述过的 base dispatch function 。 109 | - [`getState()`](api/Store.md#getState) 返回当前 store 的 state。 110 | - [`subscribe(listener)`](api/Store.md#subscribe) 注册 funtion 用于在 state 改变时调用。 111 | - [`getReducer()`](api/Store.md#getReducer) 和 [`replaceReducer(nextReducer)`](api/Store.md#replaceReducer) 可被用于实现热重载荷代码分割。通常你用不上他们。 112 | 113 | 请见完整的 [store API reference](api/Store.md#dispatch) 获取更多细节。 114 | 115 | ## Store Creator 116 | 117 | ```js 118 | type StoreCreator = (reducer: Reducer, initialState: ?State) => Store; 119 | ``` 120 | 121 | Store creator 是一个创建 Redux store 的函数。就像 dispatching function 那样,我们必须分清由 [`createStore(reducer, initialState)`](api/createStore.md) 从 Redux 包中导出的 base store creator ,和从 store enhancer 返回的 store creator 。 122 | 123 | ## Store enhancer 124 | 125 | ```js 126 | type StoreEnhancer = (next: StoreCreator) => StoreCreator; 127 | ``` 128 | 129 | Store enhancer 是一个高阶函数将 store creator 组合,返回一个新的强化过的 store creator 。这与允许你使用可组合方式 变更 store 接口的中间件有点相似。 130 | 131 | Store enhancer 是与 React 中概念非常相同的高阶 component , 通常也会被叫做 “component enhancers” 。 132 | 133 | 因为 store 并非一个实例,而更像是几个函数的集合普通对象。复制可以被简单的创建或修改而不需变动原先的 store 。在 [`compose`](api/compose.md) 文档中有一个示例演示了这种做法。 134 | 135 | 大多数时候你不可能去写 store enhancer ,但你会用得着 [developer tools](https://github.com/gaearon/redux-devtools) 提供的。它使得app对其发生无察觉的时间旅行变得可能。搞笑的是,[Redux middleware implementation](api/applyMiddleware.md) 本身就是一个 store enhancer 。 136 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | 3 | * [自述](README.md) 4 | * [介绍](/docs/introduction/README.md) 5 | * [动机](/docs/introduction/Motivation.md) 6 | * [三大原则](/docs/introduction/ThreePrinciples.md) 7 | * [先前技术](/docs/introduction/PriorArt.md) 8 | * [生态](/docs/introduction/Ecosystem.md) 9 | * [示例](/docs/introduction/Examples.md) 10 | * [基础](/docs/basics/README.md) 11 | * [Actions](/docs/basics/Actions.md) 12 | * [Reducers](/docs/basics/Reducers.md) 13 | * [Store](/docs/basics/Store.md) 14 | * [数据流](/docs/basics/DataFlow.md) 15 | * [搭配 React](/docs/basics/UsageWithReact.md) 16 | * [示例:Todo List](/docs/basics/ExampleTodoList.md) 17 | * [高级](/docs/advanced/README.md) 18 | * [Middleware](/docs/advanced/Middleware.md) 19 | * [异步 Actions](/docs/advanced/AsyncActions.md) 20 | * [异步数据流](/docs/advanced/AsyncFlow.md) 21 | * [搭配 React Router](/docs/advanced/UsageWithReactRouter.md) 22 | * [示例:Reddit API](/docs/advanced/ExampleRedditAPI.md) 23 | * [下一步](/docs/advanced/NextSteps.md) 24 | * [技巧](/docs/recipes/README.md) 25 | * [迁移到 Redux](/docs/recipes/MigratingToRedux.md) 26 | * [减少样板代码](/docs/recipes/ReducingBoilerplate.md) 27 | * [服务端渲染](/docs/recipes/ServerRendering.md) 28 | * [Computing Derived Data](/docs/recipes/ComputingDerivedData.md) 29 | * [编写测试](/docs/recipes/WritingTests.md) 30 | * [排错](/docs/Troubleshooting.md) 31 | * [词汇表](/docs/Glossary.md) 32 | * [API 文档](/docs/api/README.md) 33 | * [createStore](/docs/api/createStore.md) 34 | * [Store](/docs/api/Store.md) 35 | * [combineReducers](/docs/api/combineReducers.md) 36 | * [applyMiddleware](/docs/api/applyMiddleware.md) 37 | * [bindActionCreators](/docs/api/bindActionCreators.md) 38 | * [compose](/docs/api/compose.md) 39 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # 排错 2 | 3 | 这里会列出常见的问题和对应的解决方案。 4 | 虽然使用 React 做示例,即使你使用要其它库仍然会有帮助。 5 | 6 | ### dispatch action 后什么也没有发生 7 | 8 | 有时,你 dispatch action 后,view 却没有更新。这是为什么呢?可能有下面几种原因。 9 | 10 | #### 永远不要直接修改 reducer 的参数 11 | 12 | 如果你想修改 Redux 给你传入的 `state` 或 `action`,请住手! 13 | 14 | Redux 假定你永远不会修改 reducer 里传入的对象。**任何时候,你都应该返回一个新的 state 对象。**即使你没有使用 [Immutable](https://facebook.github.io/immutable-js/) 这样的库,也要保证做到不修改对象。 15 | 16 | 不变性(Immutability)可以让 [react-redux](https://github.com/gaearon/react-redux) 高效的监听 state 的细粗度更新。它也让 [redux-devtools](http://github.com/gaearon/redux-devtools) 能提供“时间旅行”这类强大特性。 17 | 18 | 例如,下面的 reducer 就是错误的,因为它改变了 state: 19 | 20 | ```js 21 | function todos(state = [], action) { 22 | switch (action.type) { 23 | case 'ADD_TODO': 24 | // 错误!这会改变 state.actions。 25 | state.actions.push({ 26 | text: action.text, 27 | completed: false 28 | }); 29 | case 'COMPLETE_TODO': 30 | // 错误!这会改变 state.actions[action.index]. 31 | state.actions[action.index].completed = true; 32 | } 33 | 34 | return state 35 | } 36 | ``` 37 | 38 | 应该重写成这样: 39 | 40 | ```js 41 | function todos(state = [], action) { 42 | switch (action.type) { 43 | case 'ADD_TODO': 44 | // 返回新数组 45 | return [...state, { 46 | text: action.text, 47 | completed: false 48 | }]; 49 | case 'COMPLETE_TODO': 50 | // 返回新数组 51 | return [ 52 | ...state.slice(0, action.index), 53 | // 修改之前复制数组 54 | Object.assign({}, state[action.index], { 55 | completed: true 56 | }), 57 | ...state.slice(action.index + 1) 58 | ]; 59 | default: 60 | return state; 61 | } 62 | } 63 | ``` 64 | 65 | 虽然需要写更多代码,但是让 Redux 变得可具有可预测性和高效。如果你想减少代码量,你可以用一些辅助方法类似 66 | [`React.addons.update`](https://facebook.github.io/react/docs/update.html) 来让这样的不可变转换操作变得更简单: 67 | 68 | ```js 69 | // 修改前 70 | return [ 71 | ...state.slice(0, action.index), 72 | Object.assign({}, state[action.index], { 73 | completed: true 74 | }), 75 | ...state.slice(action.index + 1) 76 | ] 77 | 78 | // 修改后 79 | return update(state, { 80 | [action.index]: { 81 | completed: { 82 | $set: true 83 | } 84 | } 85 | }); 86 | ``` 87 | 88 | 最后,如果需要更新 object,你需要使用 Underscore 提供的 `_.extend` 方法,或者更好的,使用 [`Object.assign`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 的 polyfill 89 | 90 | 要注意 `Object.assign` 的使用方法。例如,在 reducer 里不要这样使用 `Object.assign(state, newData)`,应该用 `Object.assign({}, state, newData)`。这样它才不会覆盖以前的 `state`。 91 | 92 | 你也可以通过使用 [Babel 阶段 1](http://babeljs.io/docs/usage/experimental/) 模式来开启 [ES7 对象的 spread 操作](https://github.com/sebmarkbage/ecmascript-rest-spread): 93 | 94 | ```js 95 | // 修改前: 96 | return [ 97 | ...state.slice(0, action.index), 98 | Object.assign({}, state[action.index], { 99 | completed: true 100 | }), 101 | ...state.slice(action.index + 1) 102 | ] 103 | 104 | // 修改后: 105 | return [ 106 | ...state.slice(0, action.index), 107 | { ...state[action.index], completed: true }, 108 | ...state.slice(action.index + 1) 109 | ] 110 | ``` 111 | 112 | 注意还在实验阶段的特性注定经常改变,最好不要在大的项目里过多依赖它们。 113 | 114 | #### 不要忘记调用 [`dispatch(action)`](api/Store.md#dispatch) 115 | 116 | 如果你定义了一个 action 创建函数,调用它并**不**会自动 dispatch 这个 action。比如,下面的代码什么也不会做: 117 | 118 | #### `TodoActions.js` 119 | 120 | ```js 121 | export function addTodo(text) { 122 | return { type: 'ADD_TODO', text }; 123 | } 124 | ``` 125 | 126 | #### `AddTodo.js` 127 | 128 | ```js 129 | import { Component } from 'react'; 130 | import { addTodo } from './TodoActions'; 131 | 132 | class AddTodo extends Component { 133 | handleClick() { 134 | // 不起作用! 135 | addTodo('Fix the issue'); 136 | } 137 | 138 | render() { 139 | return ( 140 | 143 | ); 144 | } 145 | } 146 | ``` 147 | 148 | 它不起作用是因为你的 action 创建函数只是一个**返回** action 的函数而已。你需要手动 dispatch 它。我们不能在定义时把 action 创建函数绑定到指定的 Store 上,因为应用在服务端渲染时需要为每个请求都对应一个独立的 Redux store。 149 | 150 | 解法是调用 [store](api/Store.md) 实例上的 [`dispatch()`](api/Store.md#dispatch) 方法。 151 | 152 | ```js 153 | handleClick() { 154 | // 生效!(但你需要先以某种方式拿到 store) 155 | store.dispatch(addTodo('Fix the issue')); 156 | } 157 | ``` 158 | 159 | 如果组件的层级非常深,把 store 一层层传下去很麻烦。因此 [react-redux](https://github.com/gaearon/react-redux) 提供了 `connect` 这个 [高阶组件](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750),它除了可以帮你监听 Redux store,还会把 `dispatch` 注入到组件的 props 中。 160 | 161 | 修复后的代码是这样的: 162 | 163 | ```js 164 | import { Component } from 'react'; 165 | import { connect } from 'react-redux'; 166 | import { addTodo } from './TodoActions'; 167 | 168 | class AddTodo extends Component { 169 | handleClick() { 170 | // 生效! 171 | this.props.dispatch(addTodo('Fix the issue')); 172 | } 173 | 174 | render() { 175 | return ( 176 | 179 | ); 180 | } 181 | } 182 | 183 | // 除了 state,`connect` 还把 `dispatch` 放到 props 里。 184 | export default connect(AddTodo, state => ({})) 185 | ``` 186 | 187 | 如果你想的话也可以把 `dispatch` 手动传给其它组件。 188 | 189 | ## 其它问题 190 | 191 | 在 Slack [Reactiflux](http://reactiflux.com/) 里的 **redux** 频道里提问,或者[提交一个 issue](https://github.com/rackt/redux/issues)。 192 | 如果问题终于解决了,请把解法[写到文档里](https://github.com/rackt/redux/edit/master/docs/Troubleshooting.md),以便别人遇到同样问题时参考。 193 | -------------------------------------------------------------------------------- /docs/advanced/AsyncActions.md: -------------------------------------------------------------------------------- 1 | # Async Actions 2 | 3 | In the [previous section](Middleware.md), we explored how Redux middleware can intercept, delay or transform actions before they reach the reducer. There is a variety of use cases for middleware, logging and crash reporting being great examples. However the most common use case for middleware is expressing asynchronous API calls. 4 | 5 | In the [basics guide](../basics/README.md), we built a simple todo application. It was fully synchronous. Every time an action was dispatched, the state was updated immediately. 6 | 7 | In this guide, we will build a different, asynchronous application. It will use the Reddit API to show the current headlines for a select subreddit. How does asynchronicity fit into Redux flow? 8 | 9 | ## Actions 10 | 11 | Even if you call an asynchronous API, you need to dispatch actions to change the stored data, and they will be processed by reducers synchronously. Usually, for any API request you’ll want to dispatch at least three different kinds of actions: 12 | 13 | * **An action informing the reducers that the request began.** 14 | 15 | The reducers may handle this action by toggling `isFetching` flag in the state. This way the UI knows it’s time to show a spinner. 16 | 17 | * **An action informing the reducers that the request finished successfully.** 18 | 19 | The reducers may handle this action by merging the new data into the state they manage and resetting `isFetching` flag. The UI would hide the spinner, and display the fetched data. 20 | 21 | * **An action informing the reducers that the request failed.** 22 | 23 | The reducers may handle this action by resetting `isFetching`. Maybe, some reducers will also want to store the error message so the UI can display it. 24 | 25 | You may use a dedicated `status` field in your actions: 26 | 27 | ```js 28 | { type: 'FETCH_POSTS' } 29 | { type: 'FETCH_POSTS', status: 'error', error: 'Oops' } 30 | { type: 'FETCH_POSTS', status: 'success', response: { ... } } 31 | ``` 32 | 33 | Or you can define separate types for them: 34 | 35 | ```js 36 | { type: 'FETCH_POSTS_REQUEST' } 37 | { type: 'FETCH_POSTS_SUCCESS', error: 'Oops' } 38 | { type: 'FETCH_POSTS_FAILURE', response: { ... } } 39 | ``` 40 | 41 | Choosing whether to use a single action type with flags, or multiple action types, is up to you. It’s a convention you need to decide with your team. Multiple types leave less room for a mistake, but this is not an issue if you generate action creators and reducers with a helper library like [redux-actions](https://github.com/acdlite/redux-actions). 42 | 43 | Whatever convention you choose, stick with it throughout the application. 44 | We’ll use separate types in this tutorial. 45 | 46 | ## Synchronous Action Creators 47 | 48 | Let’s start by defining several synchronous action types and action creators for them. In our app, the user can select a reddit to display: 49 | 50 | ```js 51 | export const SELECT_REDDIT = 'SELECT_REDDIT'; 52 | 53 | export function selectReddit(reddit) { 54 | return { 55 | type: SELECT_REDDIT, 56 | reddit 57 | }; 58 | } 59 | ``` 60 | 61 | They can also press a “refresh” button to update it: 62 | 63 | ```js 64 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'; 65 | 66 | export function invalidateReddit(reddit) { 67 | return { 68 | type: INVALIDATE_REDDIT, 69 | reddit 70 | }; 71 | } 72 | ``` 73 | 74 | These were the actions governed by the user interaction. We will also have another kind of action, governed by the network requests. We will see how to dispatch them later, but for now, we just want to define them. 75 | 76 | When it’s time to fetch the posts for some reddit, we will dispatch a `REQUEST_POSTS` action: 77 | 78 | ```js 79 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 80 | 81 | export function requestPosts(reddit) { 82 | return { 83 | type: REQUEST_POSTS, 84 | reddit 85 | }; 86 | } 87 | ``` 88 | 89 | It is important for it to be separate from `SELECT_REDDIT` or `INVALIDATE_REDDIT`. While they may occur one after another, as the app grows more complex, you might want to fetch some data independently of the user action (for example, to prefetch the most popular reddits, or to refresh stale data once in a while). You may also want to fetch in response to a route change, so it’s not wise to couple fetching to some particular UI event early on. 90 | 91 | Finally, when the network request comes through, we will dispatch `RECEIVE_POSTS`: 92 | 93 | ```js 94 | export const RECEIVE_POSTS = 'RECEIVE_POSTS'; 95 | 96 | export function receivePosts(reddit, json) { 97 | return { 98 | type: RECEIVE_POSTS, 99 | reddit: reddit, 100 | posts: json.data.children.map(child => child.data), 101 | receivedAt: Date.now() 102 | }; 103 | } 104 | ``` 105 | 106 | This is all we need to know for now. The particular mechanism to dispatch these actions together with network requests will be discussed later. 107 | 108 | >##### Note on Error Handling 109 | 110 | >In a real app, you’d also want to dispatch an action on request failure. We won’t implement error handling in this tutorial, but the [real world example](../introduction/Examples.html#real-world) shows one of the possible approaches. 111 | 112 | ## Designing the State Shape 113 | 114 | Just like in the basic tutorial, you’ll need to [design the shape of your application’s state](../basics/Reducers.md#designing-the-state-shape) before rushing into the implementation. With asynchronous code, there is more state to take care of, so we need to think it through. 115 | 116 | This part is often confusing to beginners, because it is not immediately clear what information describes the state of an asynchronous application, and how to organize it in a single tree. 117 | 118 | We’ll start with the most common use case: lists. Web applications often show lists of things. For example, a list of posts, or a list of friends. You’ll need to figure out what sorts of lists your app can show. You want to store them separately in the state, because this way you can cache them and only fetch again if necessary. 119 | 120 | Here’s what the state shape for our “Reddit headlines” app might look like: 121 | 122 | ```js 123 | { 124 | selectedReddit: 'frontend', 125 | postsByReddit: { 126 | frontend: { 127 | isFetching: true, 128 | didInvalidate: false, 129 | items: [] 130 | }, 131 | reactjs: { 132 | isFetching: false, 133 | didInvalidate: false, 134 | lastUpdated: 1439478405547, 135 | items: [{ 136 | id: 42, 137 | title: 'Confusion about Flux and Relay' 138 | }, { 139 | id: 500, 140 | title: 'Creating a Simple Application Using React JS and Flux Architecture' 141 | }] 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | There are a few important bits here: 148 | 149 | * We store each subreddit information separately so we can cache every subreddit. When the user switches between them the second time, the update will be instant, and we won’t need to refetch unless we want to. Don’t worry about all these items being in memory: unless you’re dealing with tens of thousands of items, and your user rarely closes the tab, you won’t need any sort of cleanup. 150 | 151 | * For every list of items, you’ll want to store `isFetching` to show a spinner, `didInvalidate` so you can later toggle it when the data is stale, `lastUpdated` so you know when it was fetched the last time, and the `items` themselves. In a real app, you’ll also want to store pagination state like `fetchedPageCount` and `nextPageUrl`. 152 | 153 | >##### Note on Nested Entities 154 | 155 | >In this example, we store the received items together with the pagination information. However, this approach won’t work well if you have nested entities referencing each other, or if you let the user edit items. Imagine the user wants to edit a fetched post, but this post is duplicated in several places in the state tree. This would be really painful to implement. 156 | 157 | >If you have nested entities, or if you let users edit received entities, you should keep them separately in the state as if it was a database. In pagination information, you would only refer to them by their IDs. This lets you always keep them up to date. The [real world example](../introduction/Examples.html#real-world) shows this approach, together with [normalizr](https://github.com/gaearon/normalizr) to normalize the nested API responses. With this approach, your state might look like this: 158 | 159 | >```js 160 | > { 161 | > selectedReddit: 'frontend', 162 | > entities: { 163 | > users: { 164 | > 2: { 165 | > id: 2, 166 | > name: 'Andrew' 167 | > } 168 | > }, 169 | > posts: { 170 | > 42: { 171 | > id: 42, 172 | > title: 'Confusion about Flux and Relay', 173 | > author: 2 174 | > }, 175 | > 100: { 176 | > id: 100, 177 | > title: 'Creating a Simple Application Using React JS and Flux Architecture', 178 | > author: 2 179 | > } 180 | > } 181 | > }, 182 | > postsByReddit: { 183 | > frontend: { 184 | > isFetching: true, 185 | > didInvalidate: false, 186 | > items: [] 187 | > }, 188 | > reactjs: { 189 | > isFetching: false, 190 | > didInvalidate: false, 191 | > lastUpdated: 1439478405547, 192 | > items: [42, 100] 193 | > } 194 | > } 195 | > } 196 | >``` 197 | 198 | >In this guide, we won’t normalize entities, but it’s something you should consider for a more dynamic application. 199 | 200 | ## Handling Actions 201 | 202 | Before going into the details of dispatching actions together with network requests, we will write the reducers for the actions we defined above. 203 | 204 | #### `reducers.js` 205 | 206 | ```js 207 | import { combineReducers } from 'redux'; 208 | import { 209 | SELECT_REDDIT, INVALIDATE_REDDIT, 210 | REQUEST_POSTS, RECEIVE_POSTS 211 | } from '../actions'; 212 | 213 | function selectedReddit(state = 'reactjs', action) { 214 | switch (action.type) { 215 | case SELECT_REDDIT: 216 | return action.reddit; 217 | default: 218 | return state; 219 | } 220 | } 221 | 222 | function posts(state = { 223 | isFetching: false, 224 | didInvalidate: false, 225 | items: [] 226 | }, action) { 227 | switch (action.type) { 228 | case INVALIDATE_REDDIT: 229 | return Object.assign({}, state, { 230 | didInvalidate: true 231 | }); 232 | case REQUEST_POSTS: 233 | return Object.assign({}, state, { 234 | isFetching: true, 235 | didInvalidate: false 236 | }); 237 | case RECEIVE_POSTS: 238 | return Object.assign({}, state, { 239 | isFetching: false, 240 | didInvalidate: false, 241 | items: action.posts, 242 | lastUpdated: action.receivedAt 243 | }); 244 | default: 245 | return state; 246 | } 247 | } 248 | 249 | function postsByReddit(state = { }, action) { 250 | switch (action.type) { 251 | case INVALIDATE_REDDIT: 252 | case RECEIVE_POSTS: 253 | case REQUEST_POSTS: 254 | return Object.assign({}, state, { 255 | [action.reddit]: posts(state[action.reddit], action) 256 | }); 257 | default: 258 | return state; 259 | } 260 | } 261 | 262 | const rootReducer = combineReducers({ 263 | postsByReddit, 264 | selectedReddit 265 | }); 266 | 267 | export default rootReducer; 268 | ``` 269 | 270 | In this code, there are two interesting parts: 271 | 272 | * We use ES6 computed property syntax so we can update `state[action.reddit]` with `Object.assign()` in a terse way. This: 273 | 274 | ```js 275 | return Object.assign({}, state, { 276 | [action.reddit]: posts(state[action.reddit], action) 277 | }); 278 | ``` 279 | is equivalent to this: 280 | 281 | ```js 282 | let nextState = {}; 283 | nextState[action.reddit] = posts(state[action.reddit], action); 284 | return Object.assign({}, state, nextState); 285 | ``` 286 | * We extracted `posts(state, action)` that manages the state of a specific post list. This is just [reducer composition](../basics/Reducers.md#splitting-reducers)! It is our choice how to split reducer into smaller reducers, and in this case, we’re delegating updating items inside an object to a `posts` reducer. The [real world example](../introduction/Examples.html#real-world) goes even further, showing how to create a reducer factory for parameterized pagination reducers. 287 | 288 | Remember that reducers are just functions, so you can use functional composition and higher-order functions as much as you feel comfortable. 289 | 290 | ## Async Action Creators 291 | 292 | Finally, how do we use the synchronous action creators we [defined earlier](#synchronous-action-creators) together with network requests? Surely, we can do this manually: 293 | 294 | ```js 295 | import fetch from 'isomorphic-fetch'; 296 | import { createStore } from 'redux'; 297 | import { selectReddit, requestPosts, receivePosts } from './actions'; 298 | import rootReducer from './reducers'; 299 | 300 | const store = createStore(rootReducer); 301 | 302 | store.dispatch(selectReddit('reactjs')); 303 | 304 | store.dispatch(requestPosts('reactjs')); 305 | fetch(`http://www.reddit.com/r/${reddit}.json`) 306 | .then(req => req.json()) 307 | .then(json => 308 | store.dispatch(receivePosts(reddit, json)) 309 | ) 310 | .then(() => { 311 | console.log(store.getState()); 312 | }); 313 | ``` 314 | 315 | We can do the same from our components. However it quickly gets tedious. Usually you want some kind of common logic before performing a request, such as looking up something in the state, and maybe deciding not to fetch because the data is cached. 316 | 317 | Clearly, actions can’t express control flow. The best tool for control flow is a function. A function can have an `if` statement, or an early `return`. If a function has access to a `dispatch` method, it can call it many times, potentially asynchronously. Does this ring a bell? 318 | 319 | In the [previous section](Middleware.md), we explored the most common extension mechanism for Redux: the middleware. Middleware lets you inject custom logic between the initial `dispatch()` call and the time the action reaches a reducer. 320 | 321 | What if we wrote a middleware that lets us **return functions from action creators**? By the way, functions that return functions are traditionally called “thunks”, so we’ll call it “thunk middleware”. It could look like this: 322 | 323 | ```js 324 | const thunkMiddleware = store => next => action => { 325 | if (typeof action !== 'function') { 326 | // Normal action, pass it on 327 | return next(action); 328 | } 329 | 330 | // Woah, somebody tried to dispatch a function! 331 | // We will invoke it immediately and give `store.dispatch` 332 | // to it. This will invert control and let it dispatch 333 | // many times. We will also pass `getState` to it so it 334 | // can peek into the current state and make decisions based on it. 335 | 336 | const result = action(store.dispatch, store.getState); 337 | 338 | // Whatever the user returned from that function, we'll return too, 339 | // so it becomes `dispatch()` returns value. This is convenient 340 | // in case user wants to return a Promise to wait for. 341 | 342 | return result; 343 | }; 344 | ``` 345 | 346 | If this doesn’t make sense, you need to go back to the [middleware introduction](Middleware.md). This lets us rewrite our example so that `fetchPosts()` is just another action creator, but it returns a function: 347 | 348 | #### `actions.js` 349 | 350 | ```js 351 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 352 | function requestPosts(reddit) { 353 | return { 354 | type: REQUEST_POSTS, 355 | reddit 356 | }; 357 | } 358 | 359 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' 360 | function receivePosts(reddit, json) { 361 | return { 362 | type: RECEIVE_POSTS, 363 | reddit: reddit, 364 | posts: json.data.children.map(child => child.data), 365 | receivedAt: Date.now() 366 | }; 367 | } 368 | 369 | export function fetchPosts(reddit) { 370 | // thunk middleware knows how to handle functions 371 | return function (dispatch) { 372 | dispatch(requestPosts(reddit)); 373 | 374 | // Return a promise to wait for 375 | // (this is not required by thunk middleware, but it is convenient for us) 376 | return fetch(`http://www.reddit.com/r/${reddit}.json`) 377 | .then(req => req.json()) 378 | .then(json => 379 | // We can dispatch many times! 380 | dispatch(receivePosts(reddit, json)) 381 | ); 382 | }; 383 | } 384 | ``` 385 | 386 | #### `index.js` 387 | 388 | ```js 389 | import thunkMiddleware from 'redux-thunk'; 390 | import loggerMiddleware from 'redux-logger'; 391 | import { createStore, applyMiddleware } from 'redux'; 392 | import { selectReddit, fetchPosts } from './actions'; 393 | import rootReducer from './reducers'; 394 | 395 | const createStoreWithMiddleware = applyMiddleware( 396 | thunkMiddleware, // lets us dispatch() functions 397 | loggerMiddleware // neat middleware that logs actions 398 | )(createStore); 399 | 400 | const store = createStoreWithMiddleware(rootReducer); 401 | 402 | store.dispatch(selectReddit('reactjs')); 403 | store.dispatch(fetchPosts('reactjs')).then(() => 404 | console.log(store.getState()); 405 | ); 406 | ``` 407 | 408 | The nice thing about thunks it they can dispatch results of each other: 409 | 410 | #### `actions.js` 411 | 412 | ```js 413 | function fetchPosts(reddit) { 414 | return dispatch => { 415 | dispatch(requestPosts(reddit)); 416 | return fetch(`http://www.reddit.com/r/${reddit}.json`) 417 | .then(req => req.json()) 418 | .then(json => dispatch(receivePosts(reddit, json))); 419 | }; 420 | } 421 | 422 | function shouldFetchPosts(state, reddit) { 423 | const posts = state.postsByReddit[reddit]; 424 | if (!posts) { 425 | return true; 426 | } else if (posts.isFetching) { 427 | return false; 428 | } else { 429 | return posts.didInvalidate; 430 | } 431 | } 432 | 433 | export function fetchPostsIfNeeded(reddit) { 434 | return (dispatch, getState) => { 435 | if (shouldFetchPosts(getState(), reddit)) { 436 | // Dispatch a thunk from thunk! 437 | return dispatch(fetchPosts(reddit)); 438 | } else { 439 | // Let the calling code know there's nothing to wait for. 440 | return Promise.resolve(); 441 | } 442 | }; 443 | } 444 | ``` 445 | 446 | This lets us write more sophisticated async control flow gradually, while the consuming code can stay pretty much the same: 447 | 448 | #### `index.js` 449 | 450 | ```js 451 | store.dispatch(fetchPostsIfNeeded('reactjs')).then(() => 452 | console.log(store.getState()); 453 | ); 454 | ``` 455 | 456 | >##### Note about Server Rendering 457 | 458 | >Async action creators are especially convenient for server rendering. You can create a store, dispatch a single async action creator that dispatches other async action creators to fetch data for a whole section of your app, and only render after the Promise it returns, completes. Then your store will already be hydrated with the state you need before rendering. 459 | 460 | [Thunk middleware](https://github.com/gaearon/redux-thunk) isn’t the only way to orchestrate asynchronous action in Redux. You can use [redux-promise](https://github.com/acdlite/redux-promise) or [redux-promise-middleware](https://github.com/pburtchaell/redux-promise-middleware) to dispatch Promises instead of functions. You can dispatch Observables with [redux-rx](https://github.com/acdlite/redux-rx). You can even write a custom middleware to describe calls to your API, like the [real world example](../introduction/Examples.html#real-world) does it. It is up to you to try a few options, choose a convention you like, and follow it, whether with, or without the middleware. 461 | 462 | ## Connecting to UI 463 | 464 | Dispatching async actions is no different from dispatching synchronous actions, so we won’t discuss this in detail. See [Usage with React](../basics/UsageWithReact.md) for an introduction into using Redux from React components. See [Example: Reddit API](ExampleRedditAPI.md) for the complete source code discussed in this example. 465 | 466 | ## Next Steps 467 | 468 | Read [Async Flow](AsyncFlow.md) to recap how async actions fit into the Redux flow. 469 | -------------------------------------------------------------------------------- /docs/advanced/AsyncFlow.md: -------------------------------------------------------------------------------- 1 | # 异步数据流 2 | 3 | 如果不使用 [middleware](Middleware.md),Redux store 只支持 [同步数据流](../basics/DataFlow.md)。这也是 [`createStore()`](../api/createStore.md) 默认返回的。 4 | 5 | You may enhance [`createStore()`](../api/createStore.md) with [`applyMiddleware()`](../api/applyMiddleware.md). It is not required, but it lets you [express asynchronous actions in a convenient way](AsyncActions.md). 6 | 7 | Asynchronous middleware like [redux-thunk](https://github.com/gaearon/redux-thunk) or [redux-promise](https://github.com/acdlite/redux-promise) wraps the store’s [`dispatch()`](../api/Store.md#dispatch) method and allows you to dispatch something other than actions, for example, functions or Promises. Any middleware you use can then interpret anything you dispatch, and in turn, can pass actions to the next middleware in chain. For example, a Promise middleware can intercept Promises and dispatch a pair of begin/end actions asynchronously in response to each Promise. 8 | 9 | When the last middleware in the chain dispatches an action, it has to be a plain object. This is when the [synchronous Redux data flow](../basics/DataFlow.md) takes place. 10 | 11 | ## Next Steps 12 | 13 | Now you know everything about data flow in a Redux app! Check out [the source code for the async example](ExampleRedditAPI.md), or read on about [React Router integration](UsageWithReactRouter.md). -------------------------------------------------------------------------------- /docs/advanced/ExampleRedditAPI.md: -------------------------------------------------------------------------------- 1 | # 示例:Reddit API 2 | 3 | 这是一个[高级教程](README.md)的例子,包含使用 Reddit API 请求文章标题的全部源码。 4 | 5 | ## 入口 6 | 7 | #### `index.js` 8 | 9 | ```js 10 | import 'babel-core/polyfill'; 11 | 12 | import React from 'react'; 13 | import Root from './containers/Root'; 14 | 15 | React.render( 16 | , 17 | document.getElementById('root') 18 | ); 19 | ``` 20 | 21 | ## Action Creators and Constants 22 | 23 | #### `actions.js` 24 | 25 | ```js 26 | import fetch from 'isomorphic-fetch'; 27 | 28 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 29 | export const RECEIVE_POSTS = 'RECEIVE_POSTS'; 30 | export const SELECT_REDDIT = 'SELECT_REDDIT'; 31 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'; 32 | 33 | export function selectReddit(reddit) { 34 | return { 35 | type: SELECT_REDDIT, 36 | reddit 37 | }; 38 | } 39 | 40 | export function invalidateReddit(reddit) { 41 | return { 42 | type: INVALIDATE_REDDIT, 43 | reddit 44 | }; 45 | } 46 | 47 | function requestPosts(reddit) { 48 | return { 49 | type: REQUEST_POSTS, 50 | reddit 51 | }; 52 | } 53 | 54 | function receivePosts(reddit, json) { 55 | return { 56 | type: RECEIVE_POSTS, 57 | reddit: reddit, 58 | posts: json.data.children.map(child => child.data), 59 | receivedAt: Date.now() 60 | }; 61 | } 62 | 63 | function fetchPosts(reddit) { 64 | return dispatch => { 65 | dispatch(requestPosts(reddit)); 66 | return fetch(`http://www.reddit.com/r/${reddit}.json`) 67 | .then(req => req.json()) 68 | .then(json => dispatch(receivePosts(reddit, json))); 69 | } 70 | } 71 | 72 | function shouldFetchPosts(state, reddit) { 73 | const posts = state.postsByReddit[reddit]; 74 | if (!posts) { 75 | return true; 76 | } else if (posts.isFetching) { 77 | return false; 78 | } else { 79 | return posts.didInvalidate; 80 | } 81 | } 82 | 83 | export function fetchPostsIfNeeded(reddit) { 84 | return (dispatch, getState) => { 85 | if (shouldFetchPosts(getState(), reddit)) { 86 | return dispatch(fetchPosts(reddit)); 87 | } 88 | }; 89 | } 90 | ``` 91 | 92 | ## Reducers 93 | 94 | #### `reducers.js` 95 | 96 | ```js 97 | import { combineReducers } from 'redux'; 98 | import { 99 | SELECT_REDDIT, INVALIDATE_REDDIT, 100 | REQUEST_POSTS, RECEIVE_POSTS 101 | } from '../actions'; 102 | 103 | function selectedReddit(state = 'reactjs', action) { 104 | switch (action.type) { 105 | case SELECT_REDDIT: 106 | return action.reddit; 107 | default: 108 | return state; 109 | } 110 | } 111 | 112 | function posts(state = { 113 | isFetching: false, 114 | didInvalidate: false, 115 | items: [] 116 | }, action) { 117 | switch (action.type) { 118 | case INVALIDATE_REDDIT: 119 | return Object.assign({}, state, { 120 | didInvalidate: true 121 | }); 122 | case REQUEST_POSTS: 123 | return Object.assign({}, state, { 124 | isFetching: true, 125 | didInvalidate: false 126 | }); 127 | case RECEIVE_POSTS: 128 | return Object.assign({}, state, { 129 | isFetching: false, 130 | didInvalidate: false, 131 | items: action.posts, 132 | lastUpdated: action.receivedAt 133 | }); 134 | default: 135 | return state; 136 | } 137 | } 138 | 139 | function postsByReddit(state = { }, action) { 140 | switch (action.type) { 141 | case INVALIDATE_REDDIT: 142 | case RECEIVE_POSTS: 143 | case REQUEST_POSTS: 144 | return Object.assign({}, state, { 145 | [action.reddit]: posts(state[action.reddit], action) 146 | }); 147 | default: 148 | return state; 149 | } 150 | } 151 | 152 | const rootReducer = combineReducers({ 153 | postsByReddit, 154 | selectedReddit 155 | }); 156 | 157 | export default rootReducer; 158 | ``` 159 | 160 | ## Store 161 | 162 | #### `configureStore.js` 163 | 164 | ```js 165 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 166 | import thunkMiddleware from 'redux-thunk'; 167 | import loggerMiddleware from 'redux-logger'; 168 | import rootReducer from '../reducers'; 169 | 170 | const createStoreWithMiddleware = applyMiddleware( 171 | thunkMiddleware, 172 | loggerMiddleware 173 | )(createStore); 174 | 175 | export default function configureStore(initialState) { 176 | return createStoreWithMiddleware(rootReducer, initialState); 177 | } 178 | ``` 179 | 180 | ## 智能组件 181 | 182 | #### `containers/Root.js` 183 | 184 | ```js 185 | import React, { Component } from 'react'; 186 | import { Provider } from 'react-redux'; 187 | import configureStore from '../configureStore'; 188 | import AsyncApp from './AsyncApp'; 189 | 190 | const store = configureStore(); 191 | 192 | export default class Root extends Component { 193 | render() { 194 | return ( 195 | 196 | {() => } 197 | 198 | ); 199 | } 200 | } 201 | ``` 202 | 203 | #### `containers/AsyncApp.js` 204 | 205 | ```js 206 | import React, { Component, PropTypes } from 'react'; 207 | import { connect } from 'react-redux'; 208 | import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions'; 209 | import Picker from '../components/Picker'; 210 | import Posts from '../components/Posts'; 211 | 212 | class AsyncApp extends Component { 213 | constructor(props) { 214 | super(props); 215 | this.handleChange = this.handleChange.bind(this); 216 | this.handleRefreshClick = this.handleRefreshClick.bind(this); 217 | } 218 | 219 | componentDidMount() { 220 | const { dispatch, selectedReddit } = this.props; 221 | dispatch(fetchPostsIfNeeded(selectedReddit)); 222 | } 223 | 224 | componentWillReceiveProps(nextProps) { 225 | if (nextProps.selectedReddit !== this.props.selectedReddit) { 226 | const { dispatch, selectedReddit } = nextProps; 227 | dispatch(fetchPostsIfNeeded(selectedReddit)); 228 | } 229 | } 230 | 231 | handleChange(nextReddit) { 232 | this.props.dispatch(selectReddit(nextReddit)); 233 | } 234 | 235 | handleRefreshClick(e) { 236 | e.preventDefault(); 237 | 238 | const { dispatch, selectedReddit } = this.props; 239 | dispatch(invalidateReddit(selectedReddit)); 240 | dispatch(fetchPostsIfNeeded(selectedReddit)); 241 | } 242 | 243 | render () { 244 | const { selectedReddit, posts, isFetching, lastUpdated } = this.props; 245 | return ( 246 |
247 | 250 |

251 | {lastUpdated && 252 | 253 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}. 254 | {' '} 255 | 256 | } 257 | {!isFetching && 258 | 260 | Refresh 261 | 262 | } 263 |

264 | {isFetching && posts.length === 0 && 265 |

Loading...

266 | } 267 | {!isFetching && posts.length === 0 && 268 |

Empty.

269 | } 270 | {posts.length > 0 && 271 |
272 | 273 |
274 | } 275 |
276 | ); 277 | } 278 | } 279 | 280 | AsyncApp.propTypes = { 281 | selectedReddit: PropTypes.string.isRequired, 282 | posts: PropTypes.array.isRequired, 283 | isFetching: PropTypes.bool.isRequired, 284 | lastUpdated: PropTypes.number, 285 | dispatch: PropTypes.func.isRequired 286 | }; 287 | 288 | function mapStateToProps(state) { 289 | const { selectedReddit, postsByReddit } = state; 290 | const { 291 | isFetching, 292 | lastUpdated, 293 | items: posts 294 | } = postsByReddit[selectedReddit] || { 295 | isFetching: true, 296 | items: [] 297 | }; 298 | 299 | return { 300 | selectedReddit, 301 | posts, 302 | isFetching, 303 | lastUpdated 304 | }; 305 | } 306 | 307 | export default connect(mapStateToProps)(AsyncApp); 308 | ``` 309 | 310 | ## 木偶组件 311 | 312 | #### `components/Picker.js` 313 | 314 | ```js 315 | import React, { Component, PropTypes } from 'react'; 316 | 317 | export default class Picker extends Component { 318 | render () { 319 | const { value, onChange, options } = this.props; 320 | 321 | return ( 322 | 323 |

{value}

324 | 332 |
333 | ); 334 | } 335 | } 336 | 337 | Picker.propTypes = { 338 | options: PropTypes.arrayOf( 339 | PropTypes.string.isRequired 340 | ).isRequired, 341 | value: PropTypes.string.isRequired, 342 | onChange: PropTypes.func.isRequired 343 | }; 344 | ``` 345 | 346 | #### `components/Posts.js` 347 | 348 | ```js 349 | import React, { PropTypes, Component } from 'react'; 350 | 351 | export default class Posts extends Component { 352 | render () { 353 | return ( 354 | 359 | ); 360 | } 361 | } 362 | 363 | Posts.propTypes = { 364 | posts: PropTypes.array.isRequired 365 | }; 366 | ``` 367 | -------------------------------------------------------------------------------- /docs/advanced/Middleware.md: -------------------------------------------------------------------------------- 1 | # Middleware 2 | 3 | If you used server-side libraries like [Express](http://expressjs.com/) and [Koa](http://koajs.com/), you are familiar with a concept of *middleware*. In these frameworks, middleware is some code you can put between the framework receiving a request, and framework generating a response. For example, Express or Koa middleware may add CORS headers, logging, compression, and more. The best feature of middleware is that it’s composable in a chain. You can use multiple independent third-party middleware in a single project. 4 | 5 | Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way. **It provides a third-party extension point between dispatching an action, and the moment it reaches the store.** People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more. 6 | 7 | This article is divided in an in-depth intro to help you grok the concept, and [a few practical examples](#seven-examples) to show the power of middleware at the very end. You may find it helpful to switch back and forth between them, as you flip between feeling bored and inspired. 8 | 9 | >##### Note for the Impatient 10 | 11 | >You will find some practical advice on using middleware for asynchronous actions [in the next section](AsyncActions.md). However we strongly advise you to resist the urge to skip this article. 12 | 13 | >Middleware is the most “magical” part of Redux you are likely to encounter. Learning how it works and how to write your own is the best investment you can make into your productivity using Redux. 14 | 15 | >If you’re really impatient, skip ahead to [seven examples](#seven-examples) and come back. 16 | 17 | ## Understanding Middleware 18 | 19 | While middleware can be used for a variety of things, including asynchronous API calls, it’s really important that you understand where it comes from. We’ll guide you through the thought process leading to middleware, by using logging and crash reporting as examples. 20 | 21 | ### Problem: Logging 22 | 23 | One of the benefits of Redux is that it makes state changes predictable and transparent. Every time an action is dispatched, the new state is computed and saved. The state cannot change by itself, it can only change as a consequence of a specific action. 24 | 25 | Wouldn’t it be nice if we logged every action that happens in the app, together with the state computed after it? When something goes wrong, we can look back at our log, and figure out which action corrupted the state. 26 | 27 | 28 | 29 | How do we approach this with Redux? 30 | 31 | ### Attempt #1: Logging Manually 32 | 33 | The most naïve solution is just to log the action and the next state yourself every time you call [`store.dispatch(action)`](../api/Store.md#dispatch). It’s not really a solution, but just a first step towards understanding the problem. 34 | 35 | >##### Note 36 | 37 | >If you’re using [react-redux](https://github.com/gaearon/react-redux) or similar bindings, you likely won’t have direct access to the store instance in your components. For the next few paragraphs, just assume you pass the store down explicitly. 38 | 39 | Say, you call this when creating a todo: 40 | 41 | ```js 42 | store.dispatch(addTodo('Use Redux')); 43 | ``` 44 | 45 | To log the action and state, you can change it to something like this: 46 | 47 | ```js 48 | let action = addTodo('Use Redux'); 49 | 50 | console.log('dispatching', action); 51 | store.dispatch(action); 52 | console.log('next state', store.getState()); 53 | ``` 54 | 55 | This produces the desired effect, but you wouldn’t want to do it every time. 56 | 57 | ### Attempt #2: Wrapping Dispatch 58 | 59 | You can extract logging into a function: 60 | 61 | ```js 62 | function dispatchAndLog(store, action) { 63 | console.log('dispatching', action); 64 | store.dispatch(action); 65 | console.log('next state', store.getState()); 66 | } 67 | ``` 68 | 69 | You can then use it everywhere instead of `store.dispatch()`: 70 | 71 | ```js 72 | dispatchAndLog(store, addTodo('Use Redux')); 73 | ``` 74 | 75 | We could end this here, but it’s not very convenient to import a special function every time. 76 | 77 | ### Attempt #3: Monkeypatching Dispatch 78 | 79 | What if we just replace the `dispatch` function on the store instance? Redux store is just a plain object with [a few methods](../api/Store.md), and we’re writing JavaScript, so we can just monkeypatch the `dispatch` implementation: 80 | 81 | ```js 82 | let next = store.dispatch; 83 | store.dispatch = function dispatchAndLog(action) { 84 | console.log('dispatching', action); 85 | let result = next(action); 86 | console.log('next state', store.getState()); 87 | return result; 88 | }; 89 | ``` 90 | 91 | This is already closer to what we want! No matter where we dispatch an action, it is guaranteed to be logged. Monkeypatching never feels right, but we can live with this for now. 92 | 93 | ### Problem: Crash Reporting 94 | 95 | What if we want to apply **more then one** such transformation to `dispatch`? 96 | 97 | A different useful transformation that comes to my mind is reporting JavaScript errors in production. The global `window.onerror` event is not reliable because it doesn’t provide stack information in some older browsers, which is crucial to understand why an error is happening. 98 | 99 | Wouldn’t it be useful if, any time an error is thrown as a result of dispatching an action, we would send it to a crash reporting service like [Sentry](https://getsentry.com/welcome/) with the stack trace, the action that caused the error, and the current state? This way it’s much easier to reproduce the error in development. 100 | 101 | However, it is important that we keep logging and crash reporting separate. Ideally we want them to be different modules, potentially in different packages. Otherwise we can’t have an ecosystem of such utilities. (Hint: we’re slowly getting to what middleware is!) 102 | 103 | If logging and crash reporting are separate utilities, they might look like this: 104 | 105 | ```js 106 | function patchStoreToAddLogging(store) { 107 | let next = store.dispatch; 108 | store.dispatch = function dispatchAndLog(action) { 109 | console.log('dispatching', action); 110 | let result = next(action); 111 | console.log('next state', store.getState()); 112 | return result; 113 | }; 114 | } 115 | 116 | function patchStoreToAddCrashReporting(store) { 117 | let next = store.dispatch; 118 | store.dispatch = function dispatchAndReportErrors(action) { 119 | try { 120 | return next(action); 121 | } catch (err) { 122 | console.error('Caught an exception!', err); 123 | Raven.captureException(err, { 124 | extra: { 125 | action, 126 | state: store.getState() 127 | } 128 | }); 129 | throw err; 130 | } 131 | }; 132 | } 133 | ``` 134 | 135 | If these functions are published as separate modules, we can later use them to patch our store: 136 | 137 | ```js 138 | patchStoreToAddLogging(store); 139 | patchStoreToAddCrashReporting(store); 140 | ``` 141 | 142 | Still, this isn’t nice. 143 | 144 | ### Attempt #4: Hiding Monkeypatching 145 | 146 | Monkeypatching is a hack. “Replace any method you like”, what kind of API is that? Let’s figure out the essence of it instead. Previously, our functions replaced `store.dispatch`. What if they *returned* the new `dispatch` function instead? 147 | 148 | ```js 149 | function logger(store) { 150 | let next = store.dispatch; 151 | 152 | // Previously: 153 | // store.dispatch = function dispatchAndLog(action) { 154 | 155 | return function dispatchAndLog(action) { 156 | console.log('dispatching', action); 157 | let result = next(action); 158 | console.log('next state', store.getState()); 159 | return result; 160 | }; 161 | } 162 | ``` 163 | 164 | We could provide a helper inside Redux that would apply the actual monkeypatching as an implementation detail: 165 | 166 | ```js 167 | function applyMiddlewareByMonkeypatching(store, middlewares) { 168 | middlewares = middlewares.slice(); 169 | middlewares.reverse(); 170 | 171 | // Transform dispatch function with each middleware. 172 | middlewares.forEach(middleware => 173 | store.dispatch = middleware(store) 174 | ); 175 | } 176 | ``` 177 | 178 | We could use it to apply multiple middleware like this: 179 | 180 | ```js 181 | applyMiddlewareByMonkeypatching(store, [logger, crashReporter]); 182 | ``` 183 | 184 | However, it is still monkeypatching. 185 | The fact that we hide it inside the library doesn’t alter this fact. 186 | 187 | ### Attempt #5: Removing Monkeypatching 188 | 189 | Why do we even overwrite `dispatch`? Of course, to be able to call it later, but there’s also another reason: so that every middleware can access (and call) the previously wrapped `store.dispatch`: 190 | 191 | ```js 192 | function logger(store) { 193 | // Must point to the function returned by the previous middleware: 194 | let next = store.dispatch; 195 | 196 | return function dispatchAndLog(action) { 197 | console.log('dispatching', action); 198 | let result = next(action); 199 | console.log('next state', store.getState()); 200 | return result; 201 | }; 202 | } 203 | ``` 204 | 205 | It is essential to chaining middleware! 206 | 207 | If `applyMiddlewareByMonkeypatching` doesn’t assign `store.dispatch` immediately after processing the first middleware, `store.dispatch` will keep pointing to the original `dispatch` function. Then the second middleware will also be bound to the original `dispatch` function. 208 | 209 | But there’s also a different way to enable chaining. The middleware could accept the `next()` dispatch function as a parameter instead of reading it from the `store` instance. 210 | 211 | ```js 212 | function logger(store) { 213 | return function wrapDispatchToAddLogging(next) { 214 | return function dispatchAndLog(action) { 215 | console.log('dispatching', action); 216 | let result = next(action); 217 | console.log('next state', store.getState()); 218 | return result; 219 | }; 220 | } 221 | } 222 | ``` 223 | 224 | It’s a [“we need to go deeper”](http://knowyourmeme.com/memes/we-need-to-go-deeper) kind of moment, so it might take a while for this to make sense. The function cascade feels intimidating. ES6 arrow functions make this [currying](https://en.wikipedia.org/wiki/Currying) easier on eyes: 225 | 226 | ```js 227 | const logger = store => next => action => { 228 | console.log('dispatching', action); 229 | let result = next(action); 230 | console.log('next state', store.getState()); 231 | return result; 232 | }; 233 | 234 | const crashReporter = store => next => action => { 235 | try { 236 | return next(action); 237 | } catch (err) { 238 | console.error('Caught an exception!', err); 239 | Raven.captureException(err, { 240 | extra: { 241 | action, 242 | state: store.getState() 243 | } 244 | }); 245 | throw err; 246 | } 247 | } 248 | ``` 249 | 250 | **This is exactly what Redux middleware looks like.** 251 | 252 | Now middleware takes the `next()` dispatch function, and returns a dispatch function, which in turn serves as `next()` to the middleware to the left, and so on. It’s still useful to have access to some store methods like `getState()`, so `store` stays available as the top-level argument. 253 | 254 | ### Attempt #6: Naïvely Applying the Middleware 255 | 256 | Instead of `applyMiddlewareByMonkeypatching()`, we could write `applyMiddleware()` that first obtains the final, fully wrapped `dispatch()` function, and returns a copy of the store using it: 257 | 258 | ```js 259 | // Warning: Naïve implementation! 260 | // That's *not* Redux API. 261 | 262 | function applyMiddleware(store, middlewares) { 263 | middlewares = middlewares.slice(); 264 | middlewares.reverse(); 265 | 266 | let dispatch = store.dispatch; 267 | middlewares.forEach(middleware => 268 | dispatch = middleware(store)(dispatch) 269 | ); 270 | 271 | return Object.assign({}, store, { dispatch }); 272 | } 273 | ``` 274 | 275 | The implementation of [`applyMiddleware()`](../api/applyMiddleware.md) that ships with Redux is similar, but **different in three important aspects**: 276 | 277 | * It only exposes a subset of [store API](../api/Store.md) to the middleware: [`dispatch(action)`](../api/Store.md#dispatch) and [`getState()`](../api/Store. 278 | md#getState). 279 | 280 | * It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we will see [later](AsyncActions.md). 281 | 282 | * To ensure that you may only apply middleware once, it operates on `createStore()` rather than on `store` itself. Instead of `(store, middlewares) => store`, its signature is `(...middlewares) => (createStore) => createStore`. 283 | 284 | ### The Final Approach 285 | 286 | Given this middleware we just wrote: 287 | 288 | ```js 289 | const logger = store => next => action => { 290 | console.log('dispatching', action); 291 | let result = next(action); 292 | console.log('next state', store.getState()); 293 | return result; 294 | }; 295 | 296 | const crashReporter = store => next => action => { 297 | try { 298 | return next(action); 299 | } catch (err) { 300 | console.error('Caught an exception!', err); 301 | Raven.captureException(err, { 302 | extra: { 303 | action, 304 | state: store.getState() 305 | } 306 | }); 307 | throw err; 308 | } 309 | } 310 | ``` 311 | 312 | Here’s how to apply it to a Redux store: 313 | 314 | ```js 315 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 316 | 317 | // applyMiddleware takes createStore() and returns 318 | // a function with a compatible API. 319 | let createStoreWithMiddleware = applyMiddleware( 320 | logger, 321 | crashReporter 322 | )(createStore); 323 | 324 | // Use it like you would use createStore() 325 | let todoApp = combineReducers(reducers); 326 | let store = createStoreWithMiddleware(todoApp); 327 | ``` 328 | 329 | This is it! Now any actions dispatched to the store instance will flow through `logger` and `crashReporter`: 330 | 331 | ```js 332 | // Will flow through both logger and crashReporter middleware! 333 | store.dispatch(addTodo('Use Redux')); 334 | ``` 335 | 336 | ## Seven Examples 337 | 338 | If your head boiled from reading the above section, imagine what it was like to write it. This part is meant to be a relaxation for you and me, and will help get your gears turning. 339 | 340 | Each function below is a valid Redux middleware. They are not equally useful, but at least they are equally fun. 341 | 342 | ```js 343 | /** 344 | * Logs all actions and states after they are dispatched. 345 | */ 346 | const logger = store => next => action => { 347 | console.group(action.type); 348 | console.info('dispatching', action); 349 | let result = next(action); 350 | console.log('next state', store.getState()); 351 | console.groupEnd(action.type); 352 | return result; 353 | }; 354 | 355 | /** 356 | * Sends crash reports as state is updated and listeners are notified. 357 | */ 358 | const crashReporter = store => next => action => { 359 | try { 360 | return next(action); 361 | } catch (err) { 362 | console.error('Caught an exception!', err); 363 | Raven.captureException(err, { 364 | extra: { 365 | action, 366 | state: store.getState() 367 | } 368 | }); 369 | throw err; 370 | } 371 | } 372 | 373 | /** 374 | * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds. 375 | * Makes `dispatch` return a function to cancel the interval in this case. 376 | */ 377 | const timeoutScheduler = store => next => action => { 378 | if (!action.meta || !action.meta.delay) { 379 | return next(action); 380 | } 381 | 382 | let intervalId = setTimeout( 383 | () => next(action), 384 | action.meta.delay 385 | ); 386 | 387 | return function cancel() { 388 | clearInterval(intervalId); 389 | }; 390 | }; 391 | 392 | /** 393 | * Schedules actions with { meta: { raf: true } } to be dispatched inside a rAF loop frame. 394 | * Makes `dispatch` return a function to remove the action from queue in this case. 395 | */ 396 | const rafScheduler = store => next => { 397 | let queuedActions = []; 398 | let frame = null; 399 | 400 | function loop() { 401 | frame = null; 402 | try { 403 | if (queuedActions.length) { 404 | next(queuedActions.shift()); 405 | } 406 | } finally { 407 | maybeRaf(); 408 | } 409 | } 410 | 411 | function maybeRaf() { 412 | if (queuedActions.length && !frame) { 413 | frame = requestAnimationFrame(loop); 414 | } 415 | } 416 | 417 | return action => { 418 | if (!action.meta || !action.meta.raf) { 419 | return next(action); 420 | } 421 | 422 | queuedActions.push(action); 423 | maybeRaf(); 424 | 425 | return function cancel() { 426 | queuedActions = queuedActions.filter(a => a !== action) 427 | }; 428 | }; 429 | }; 430 | 431 | /** 432 | * Lets you dispatch promises in addition to actions. 433 | * If the promise is resolved, its result will be dispatched as an action. 434 | * The promise is returned from `dispatch` so the caller may handle rejection. 435 | */ 436 | const vanillaPromise = store => next => action => { 437 | if (typeof action.then !== 'function') { 438 | return next(action); 439 | } 440 | 441 | return Promise.resolve(action).then(store.dispatch); 442 | }; 443 | 444 | /** 445 | * Lets you dispatch special actions with a { promise } field. 446 | * 447 | * This middleware will turn them into a single action at the beginning, 448 | * and a single success (or failure) action when the `promise` resolves. 449 | * 450 | * For convenience, `dispatch` will return the promise so the caller can wait. 451 | */ 452 | const readyStatePromise = store => next => action => { 453 | if (!action.promise) { 454 | return next(action) 455 | } 456 | 457 | function makeAction(ready, data) { 458 | let newAction = Object.assign({}, action, { ready }, data); 459 | delete newAction.promise; 460 | return newAction; 461 | } 462 | 463 | next(makeAction(false)); 464 | return action.promise.then( 465 | result => next(makeAction(true, { result })), 466 | error => next(makeAction(true, { error })) 467 | ); 468 | }; 469 | 470 | /** 471 | * Lets you dispatch a function instead of an action. 472 | * This function will receive `dispatch` and `getState` as arguments. 473 | * 474 | * Useful for early exits (conditions over `getState()`), as well 475 | * as for async control flow (it can `dispatch()` something else). 476 | * 477 | * `dispatch` will return the return value of the dispatched function. 478 | */ 479 | const thunk = store => next => action => 480 | typeof action === 'function' ? 481 | action(store.dispatch, store.getState) : 482 | next(action); 483 | 484 | 485 | // You can use all of them! (It doesn’t mean you should.) 486 | let createStoreWithMiddleware = applyMiddleware( 487 | rafScheduler, 488 | timeoutScheduler, 489 | thunk, 490 | vanillaPromise, 491 | readyStatePromise, 492 | logger, 493 | errorHandler 494 | )(createStore); 495 | let todoApp = combineReducers(reducers); 496 | let store = createStoreWithMiddleware(todoApp); 497 | ``` 498 | -------------------------------------------------------------------------------- /docs/advanced/NextSteps.md: -------------------------------------------------------------------------------- 1 | # Next Steps 2 | 3 | Sorry, but we’re still writing this doc. 4 | Stay tuned, it will appear in a day or two. 5 | -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | # 高级 2 | 3 | [基础章节](../basics/README.md)介绍了如何组织简单的 Redux 应用。在这一章节中,将要学习如何使用 AJAX 和路由。 4 | 5 | * [Middleware](Middleware.md) 6 | * [异步 Actions](Async Actions.md) 7 | * [异步数据流](AsyncFlow.md) 8 | * [搭配 React Router](UsageWithReactRouter.md) 9 | * [示例:Reddit API](ExampleRedditAPI.md) 10 | * [下一步](NextSteps.md) 11 | -------------------------------------------------------------------------------- /docs/advanced/UsageWithReactRouter.md: -------------------------------------------------------------------------------- 1 | # Usage with React Router 2 | 3 | Sorry, but we’re still writing this doc. 4 | Stay tuned, it will appear in a day or two. 5 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API 文档 2 | 3 | Redux 的 API 非常少。Redux 定义了一系列的约定(contract)来让你来实现(例如 [reducers](../Glossary.md#reducer)),同时提供少量辅助函数来把这些约定整合到一起。 4 | 5 | 这一章会介绍所有的 Redux API。记住,Redux 只关心如何管理 state。在实际的项目中,你还需要使用 UI 绑定库如 [react-redux](https://github.com/gaearon/react-redux)。 6 | 7 | ### 顶级暴露的方法 8 | 9 | * [createStore(reducer, [initialState])](createStore.md) 10 | * [combineReducers(reducers)](combineReducers.md) 11 | * [applyMiddleware(...middlewares)](applyMiddleware.md) 12 | * [bindActionCreators(actionCreators, dispatch)](bindActionCreators.md) 13 | * [compose(...functions)](compose.md) 14 | 15 | ### Store API 16 | 17 | * [Store](Store.md) 18 | * [getState()](Store.md#getState) 19 | * [dispatch(action)](Store.md#dispatch) 20 | * [subscribe(listener)](Store.md#subscribe) 21 | * [getReducer()](Store.md#getReducer) 22 | * [replaceReducer(nextReducer)](Store.md#replaceReducer) 23 | 24 | ### 引入 25 | 26 | 上面介绍的所有函数都是顶级暴露的方法。都可以这样引入: 27 | 28 | #### ES6 29 | 30 | ```js 31 | import { createStore } from 'redux'; 32 | ``` 33 | 34 | #### ES5 (CommonJS) 35 | 36 | ```js 37 | var createStore = require('redux').createStore; 38 | ``` 39 | 40 | #### ES5 (UMD build) 41 | 42 | ```js 43 | var createStore = Redux.createStore; 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/api/Store.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | Store 就是用来维持应用所有的 [state 树](../Glossary.md#state) 的一个对象。 4 | 改变 store 内 state 的惟一途径是对它 dispatch 一个 [action](../Glossary.md#action)。 5 | 6 | Store 不是类。它只是有几个方法的对象。 7 | 要创建它,只需要把根部的 [reducing 函数](../Glossary.md#reducer) 传递给 [`createStore`](createStore.md)。 8 | 9 | >##### Flux 用户使用注意 10 | 11 | >如果你以前使用 Flux,那么你只需要注意一个重要的区别。Redux 没有 Dispatcher 且不支持多个 store。相反,只有一个单一的 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。 12 | 13 | ### Store 方法 14 | 15 | - [`getState()`](#getState) 16 | - [`dispatch(action)`](#dispatch) 17 | - [`subscribe(listener)`](#subscribe) 18 | - [`getReducer()`](#getReducer) 19 | - [`replaceReducer(nextReducer)`](#replaceReducer) 20 | 21 | ## Store 方法 22 | 23 | ### [`getState()`](#getState) 24 | 25 | 返回应用当前的 state 树。 26 | 它与 store 的最后一个 reducer 返回值相同。 27 | 28 | #### 返回 29 | 30 | *(any)*: 应用当前的 state 树。 31 | 32 |
33 | 34 | ### [`dispatch(action)`](#dispatch) 35 | 36 | 分发 action。这是触发 state 变化的惟一途径。 37 | 38 | 会使用当前 [`getState()`](#getState) 的结果和传入的 `action` 以同步方式的调用 store 的 reduce 函数。返回值会被作为下一个 state。从现在开始,这就成为了 [`getState()`](#getState) 的返回值,同时变化监听器(change listener)会被触发。 39 | 40 | >##### Flux 用户使用注意 41 | >当你在 [reducer](../Glossary.md#reducer) 内部调用 `dispatch` 时,将会抛出错误提示“Reducers may not dispatch actions.(Reducer 内不能 dispatch action)”。这就相当于 Flux 里的 “Cannot dispatch in a middle of dispatch(dispatch 过程中不能再 dispatch)”,但并不会引起对应的错误。在 Flux 里,当 Store 处理 action 和触发 update 事件时,dispatch 是禁止的。这个限制并不好,因为他限制了不能在生命周期回调里 dispatch action,还有其它一些本来很正常的地方。 42 | 43 | >在 Redux 里,只会在根 reducer 返回新 state 结束后再会调用事件监听器,因此,你可以在事件监听器里再做 dispatch。惟一使你不能在 reducer 中途 dispatch 的原因是要确保 reducer 没有副作用。如果 action 处理会产生副作用,正确的做法是使用异步 [action 创建函数](../Glossary.md#action-creator)。 44 | 45 | #### 参数 46 | 47 | 1. `action` (*Object*): 描述应用变化的普通对象。Action 是把数据传入 store 的惟一途径,所以任何数据,无论来自 UI 事件,网络回调或者是其它资源如 WebSockets,最终都应该以 action 的形式被 dispatch。按照约定,action 具有 `type` 字段来表示它的类型。type 也可被定义为常量或者是从其它模块引入。最好使用字符串,而不是 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作为 action,因为字符串是可以被序列化的。除了 `type` 字段外,action 对象的结构完全取决于你。参照 [Flux 标准 Action](https://github.com/acdlite/flux-standard-action) 获取如何组织 action 的建议。 48 | 49 | #### 返回 50 | 51 | (Object): 要 dispatch 的 action。 52 | 53 | #### 注意 54 | 55 | 使用 [`createStore`](createStore.md) 创建的 “纯正” store 只支持普通对象类型的 action,而且会立即传到 reducer 来执行。 56 | 57 | 但是,如果你用 [`applyMiddleware`](applyMiddleware.md) 来套住 [`createStore`](createStore.md) 时,middleware 可以修改 action 的执行,并支持执行 dispatch [intent(意图)](../Glossary.md#intent)。Intent 一般是异步操作如 Promise、Observable 或者 Thunk。 58 | 59 | Middleware 是由社区创建,并不会同 Redux 一起发行。你需要手动安装 [redux-thunk](https://github.com/gaearon/redux-thunk) 或者 [redux-promise](https://github.com/acdlite/redux-promise) 库。你也可以创建自己的 middleware。 60 | 61 | 想学习如何描述异步 API 调用?看一下 action 创建函数里当前的 state,执行一个有副作用的操作,或者以链式操作执行它们,参照 [`applyMiddleware`](applyMiddleware.md) 中的示例。 62 | 63 | #### 示例 64 | 65 | ```js 66 | import { createStore } from 'redux'; 67 | let store = createStore(todos, ['Use Redux']); 68 | 69 | function addTodo(text) { 70 | return { 71 | type: 'ADD_TODO', 72 | text 73 | }; 74 | } 75 | 76 | store.dispatch(addTodo('Read the docs')); 77 | store.dispatch(addTodo('Read about the middleware')); 78 | ``` 79 | 80 |
81 | 82 | ### [`subscribe(listener)`](#subscribe) 83 | 84 | 添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 [`getState()`](#getState) 来拿到当前 state。 85 | 86 | 这是一个底层 API。多数情况下,你不会直接使用它,会使用一些 React(或其它库)的绑定。如果你想让回调函数执行的时候使用当前的 state,你可以 [把 store 转换成一个 Observable 或者写一个定制的 `observeStore` 工具](https://github.com/rackt/redux/issues/303#issuecomment-125184409)。 87 | 88 | 如果需要解绑这个变化监听器,执行 `subscribe` 返回的函数即可。 89 | 90 | #### 参数 91 | 92 | 1. `listener` (*Function*): 每当 dispatch action 的时候都会执行的回调。state 树中的一部分可能已经变化。你可以在回调函数里调用 [`getState()`](#getState) 来拿到当前 state。store 的 reducer 应该是纯函数,因此你可能需要对 state 树中的引用做深度比较来确定它的值是否有变化。 93 | 94 | ##### 返回 95 | 96 | (*Function*): 一个可以解绑变化监听器的函数。 97 | 98 | ##### 示例 99 | 100 | ```js 101 | function select(state) { 102 | return state.some.deep.property; 103 | } 104 | 105 | let currentValue; 106 | function handleChange() { 107 | let previousValue = currentValue; 108 | currentValue = select(store.getState()); 109 | 110 | if (previousValue !== currentValue) { 111 | console.log('Some deep nested property changed from', previousValue, 'to', currentValue); 112 | } 113 | } 114 | 115 | let unsubscribe = store.subscribe(handleChange); 116 | handleChange(); 117 | ``` 118 | 119 |
120 | 121 | ### [`getReducer()`](#getReducer) 122 | 123 | >##### 已过期 124 | 125 | >此 API 已[过期](https://github.com/rackt/redux/issues/350). 126 | >我们找到更好的方式来处理后会移除它。 127 | 128 | 返回 store 当前用来计算 state 的 reducer。 129 | 130 | 这是一个高级 API。只有在你需要实现 Redux 热加载机制的时候才可能用到它。 131 | 132 | #### 返回 133 | 134 | (*Function*): store 当前的 reducer. 135 | 136 |
137 | 138 | ### [`replaceReducer(nextReducer)`](#replaceReducer) 139 | 140 | >##### 已过期 141 | 142 | >此 API 已[过期](https://github.com/rackt/redux/issues/350). 143 | >我们找到更好的方式来处理后会移除它。 144 | 145 | 替换 store 当前用来计算 state 的 reducer。 146 | 147 | 这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。 148 | 149 | #### 参数 150 | 151 | 1. `reducer` (*Function*) store 会使用的下一个 reducer。 152 | -------------------------------------------------------------------------------- /docs/api/applyMiddleware.md: -------------------------------------------------------------------------------- 1 | # `applyMiddleware(...middlewares)` 2 | 3 | Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store’s [`dispatch`](Store.md#dispatch) method for fun and profit. The key feature of middleware is that it is composable. Multiple middleware can be combined together, where each middleware requires no knowledge of what comes before or after it in the chain. 4 | 5 | The most common use case for the middleware is to support asynchronous actions without much boilerplate code or a dependency on a library like [Rx](https://github.com/Reactive-Extensions/RxJS). It does so by letting you dispatch [async actions](../Glossary.md#async-action) in addition to normal actions. 6 | 7 | For example, [redux-thunk](https://github.com/gaearon/redux-thunk) lets the action creators invert control by dispatching functions. They would receive [`dispatch`](Store.md#dispatch) as an argument and may call it asynchronously. Such functions are called *thunks*. Another example of middleware is [redux-promise](https://github.com/acdlite/redux-promise). It lets you dispatch a [Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) async action, and dispatches a normal action when the Promise resolves. 8 | 9 | Middleware is not baked into [`createStore`](createStore.md) and is not a fundamental part of the Redux architecture, but we consider it useful enough to be supported right in the core. This way, there is a single standard way to extend [`dispatch`](Store.md#dispatch) in the ecosystem, and different middleware may compete in expressiveness and utility. 10 | 11 | #### 参数 12 | 13 | * `...middlewares` (*arguments*): Functions that conform to the Redux *middleware API*. Each middleware receives [`Store`](Store.md)’s [`dispatch`](Store.md#dispatch) and [`getState`](Store.md#getState) functions as named arguments, and returns a function. That function will be given the `next` middleware’s dispatch method, and is expected to return a function of `action` calling `next(action)` with a potentially different argument, or at a different time, or maybe not calling it at all. The last middleware in chain will receive the real store’s [`dispatch`](Store.md#dispatch) method as the `next` parameter, thus closing the chain. So, the middleware signature is `({ getState, dispatch }) => next => action`. 14 | 15 | #### 返回 16 | 17 | (*Function*) A store enhancer that applies the given middleware. The store enhancer is a function that needs to be applied to `createStore`. It will return a different `createStore` which has the middleware enabled. 18 | 19 | #### Example: Using Thunk Middleware for Async Actions 20 | 21 | ```js 22 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 23 | import thunk from 'redux-thunk'; 24 | import * as reducers from './reducers'; 25 | 26 | // applyMiddleware supercharges createStore with middleware: 27 | let createStoreWithMiddleware = applyMiddleware(thunk)(createStore); 28 | 29 | // We can use it exactly like “vanilla” createStore. 30 | let reducer = combineReducers(reducers); 31 | let store = createStoreWithMiddleware(reducer); 32 | 33 | function fetchSecretSauce() { 34 | return fetch('https://www.google.com/search?q=secret+sauce'); 35 | } 36 | 37 | // These are the normal action creators you have seen so far. 38 | // The actions they return can be dispatched without any middleware. 39 | // However, they only express “facts” and not the “async flow”. 40 | 41 | function makeASandwich(forPerson, secretSauce) { 42 | return { 43 | type: 'MAKE_SANDWICH', 44 | forPerson, 45 | secretSauce 46 | }; 47 | } 48 | 49 | function apologize(fromPerson, toPerson, error) { 50 | return { 51 | type: 'APOLOGIZE', 52 | fromPerson, 53 | toPerson, 54 | error 55 | }; 56 | } 57 | 58 | function withdrawMoney(amount) { 59 | return { 60 | type: 'WITHDRAW', 61 | amount 62 | }; 63 | } 64 | 65 | // Even without a middleware, you can dispatch an action: 66 | store.dispatch(withdrawMoney(100)); 67 | 68 | // But what do you do when you need to start an asynchronous action, 69 | // such as an API call, or a router transition? 70 | 71 | // Meet thunks. 72 | // A thunk is a function that returns a function. 73 | // This is a thunk. 74 | 75 | function makeASandwichWithSecretSauce(forPerson) { 76 | 77 | // Invert control! 78 | // Return a function that accepts `dispatch` so we can dispatch later. 79 | // Thunk middleware knows how to turn thunk async actions into actions. 80 | 81 | return function (dispatch) { 82 | return fetchSecretSauce().then( 83 | sauce => dispatch(makeASandwich(forPerson, sauce)), 84 | error => dispatch(apologize('The Sandwich Shop', forPerson, error)) 85 | ); 86 | }; 87 | } 88 | 89 | // Thunk middleware let me dispatch thunk async actions 90 | // as if they were actions! 91 | 92 | store.dispatch( 93 | makeASandwichWithSecretSauce('Me') 94 | ); 95 | 96 | // It even takes care to return the thunk’s return value 97 | // from the dispatch, so I can chain Promises as long as I return them. 98 | 99 | store.dispatch( 100 | makeASandwichWithSecretSauce('My wife') 101 | ).then(() => { 102 | console.log('Done!'); 103 | }); 104 | 105 | // In fact I can write action creators that dispatch 106 | // actions and async actions from other action creators, 107 | // and I can build my control flow with Promises. 108 | 109 | function makeSandwichesForEverybody() { 110 | return function (dispatch, getState) { 111 | if (!getState().sandwiches.isShopOpen) { 112 | 113 | // You don’t have to return Promises, but it’s a handy convention 114 | // so the caller can always call .then() on async dispatch result. 115 | 116 | return Promise.resolve(); 117 | } 118 | 119 | // We can dispatch both plain object actions and other thunks, 120 | // which lets us compose the asynchronous actions in a single flow. 121 | 122 | return dispatch( 123 | makeASandwichWithSecretSauce('My Grandma') 124 | ).then(() => 125 | Promise.all([ 126 | dispatch(makeASandwichWithSecretSauce('Me')), 127 | dispatch(makeASandwichWithSecretSauce('My wife')) 128 | ]) 129 | ).then(() => 130 | dispatch(makeASandwichWithSecretSauce('Our kids')) 131 | ).then(() => 132 | dispatch(getState().myMoney > 42 ? 133 | withdrawMoney(42) : 134 | apologize('Me', 'The Sandwich Shop') 135 | ) 136 | ); 137 | }; 138 | } 139 | 140 | // This is very useful for server rendering, 141 | // because I can wait to prefill the data before 142 | // sending synchronously rendering the app. 143 | 144 | store.dispatch( 145 | makeSandwichesForEverybody() 146 | ).then(() => 147 | response.send(React.renderToString()) 148 | ); 149 | 150 | // I can also dispatch a thunk async action from a component 151 | // any times its props change to load the missing data. 152 | 153 | import { connect } from 'react-redux'; 154 | import { Component } from 'react'; 155 | 156 | class SandwichShop extends Component { 157 | componentDidMount() { 158 | this.props.dispatch( 159 | makeASandwichWithSecretSauce(this.props.forPerson) 160 | ); 161 | } 162 | 163 | componentWillReceiveProps(nextProps) { 164 | if (nextProps.forPerson !== this.props.forPerson) { 165 | this.props.dispatch( 166 | makeASandwichWithSecretSauce(nextProps.forPerson) 167 | ); 168 | } 169 | } 170 | 171 | render() { 172 | return

{this.props.sandwiches.join('mustard')}

173 | } 174 | } 175 | 176 | export default connect( 177 | SandwichShop, 178 | state => ({ 179 | sandwiches: state.sandwiches 180 | }) 181 | ); 182 | ``` 183 | 184 | #### 示例:自定义 Logger 日志 Middleware 185 | 186 | ```js 187 | import { createStore, applyMiddleware } from 'redux'; 188 | import todos from './reducers'; 189 | 190 | function logger({ getState }) { 191 | return (next) => (action) => { 192 | console.log('will dispatch', action); 193 | 194 | // Call the next dispatch method in the middleware chain. 195 | let returnValue = next(action); 196 | 197 | console.log('state after dispatch', getState()); 198 | 199 | // This will likely be the action itself, unless 200 | // a middleware further in chain changed it. 201 | return returnValue; 202 | }; 203 | } 204 | 205 | let createStoreWithMiddleware = applyMiddleware(logger)(createStore); 206 | let store = createStoreWithMiddleware(todos, ['Use Redux']); 207 | 208 | store.dispatch({ 209 | type: 'ADD_TODO', 210 | text: 'Understand the middleware' 211 | }); 212 | // (These lines will be logged by the middleware:) 213 | // will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' } 214 | // state after dispatch: ['Use Redux', 'Understand the middleware'] 215 | ``` 216 | 217 | 218 | #### 小贴士 219 | 220 | * Middleware only wraps the store’s [`dispatch`](Store.md#dispatch) function. Technically, anything a middleware can do, you can do manually by wrapping every `dispatch` call, but it’s easier to manage this in a single place and define action transformations on the scale of the whole project. 221 | 222 | * If you use other store enhancers in addition to `applyMiddleware`, make sure to put `applyMiddleware` before them in the composition chain because the middleware is potentially asynchronous. For example, it should go before [redux-devtools](https://github.com/gaearon/redux-devtools) because otherwise the DevTools won’t see the raw actions emitted by the Promise middleware and such. 223 | 224 | * Ever wondered what `applyMiddleware` itself is? It ought to be an extension mechanism more powerful than the middleware itself. Indeed, `applyMiddleware` is an example of the most poweful Redux extension mechanism called [store enhancers](../Glossary.md#store-enhancer). It is highly unlikely you’ll ever want to write a store enhancer yourself. Another example of a store enhancer is [redux-devtools](https://github.com/gaearon/redux-devtools). Middleware is less powerful than a store enhancer, but it is easier to write. 225 | 226 | * Middleware sounds much more complicated than it really is. The only way to really understand the middleware is to see how the existing middleware works, and try to write your own. The function nesting can be intimidating, but most of the middleware you’ll find are in fact 10-liners, and the nesting and composability is what makes the middleware system powerful. 227 | -------------------------------------------------------------------------------- /docs/api/bindActionCreators.md: -------------------------------------------------------------------------------- 1 | # `bindActionCreators(actionCreators, dispatch)` 2 | 3 | Turns an object whose values are [action creators](../Glossary.md#action-creator), into an object with the same keys, but with every action creator wrapped into a [`dispatch`](Store.md#dispatch) call so they may be invoked directly. 4 | 5 | Normally you should just call [`dispatch`](Store.md#dispatch) directly on your [`Store`](Store.md) instance. If you use Redux with React, [react-redux](https://github.com/gaearon/react-redux) will provide you with the [`dispatch`](Store.md#dispatch) function so you can call it directly, too. 6 | 7 | The only use case for `bindActionCreators` is when you want to pass some action creators down to a component that isn’t aware of Redux, and you don’t want to pass [`dispatch`](Store.md#dispatch) or the Redux store to it. 8 | 9 | 为方便起见,你可以在第一个参数的位置传入一个函数,它又会返回一个函数。 10 | For convenience, you can also pass a single function as the first argument, and get a function in return. 11 | 12 | #### 参数 13 | 14 | 1. `actionCreators` (*Function* or *Object*): An [action creator](../Glossary.md#action-creator), or an object whose values action creators. 15 | 16 | 2. `dispatch` (*Function*): A [`dispatch`](Store.md#dispatch) function available on the [`Store`](Store.md) instance. 17 | 18 | #### 返回 19 | 20 | (*Function* or *Object*): An object mimicking the original object, but with each function immediately dispatching the action returned by the corresponding action creator. If you passed a function as `actionCreators`, the return value will also be a single function. 21 | 22 | #### 示例 23 | 24 | #### `TodoActionCreators.js` 25 | 26 | ```js 27 | export function addTodo(text) { 28 | return { 29 | type: 'ADD_TODO', 30 | text 31 | }; 32 | } 33 | 34 | export function removeTodo(id) { 35 | return { 36 | type: 'REMOVE_TODO', 37 | id 38 | }; 39 | } 40 | ``` 41 | 42 | #### `SomeComponent.js` 43 | 44 | ```js 45 | import { Component } from 'react'; 46 | import { bindActionCreators } from 'redux'; 47 | import { connect } from 'react-redux'; 48 | 49 | import * as TodoActionCreators from './TodoActionCreators'; 50 | console.log(TodoActionCreators); 51 | // { 52 | // addTodo: Function, 53 | // removeTodo: Function 54 | // } 55 | 56 | class TodoListContainer extends Component { 57 | componentDidMount() { 58 | // Injected by react-redux: 59 | let { dispatch } = this.props; 60 | 61 | // 注意:这样做行不通: 62 | // TodoActionCreators.addTodo('Use Redux'); 63 | 64 | // 你只是调用了创建 action 的方法。 65 | // 你必须要 dispatch action 而已。 66 | 67 | // 这样做行得通: 68 | let action = TodoActionCreators.addTodo('Use Redux'); 69 | dispatch(action); 70 | } 71 | 72 | render() { 73 | // 由 react-redux 注入: 74 | let { todos, dispatch } = this.props; 75 | 76 | // 这是应用 bindActionCreators 比较好的场景: 77 | // 在子组件里,可以完全不知道 Redux 的存在。 78 | 79 | let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch); 80 | console.log(boundActionCreators); 81 | // { 82 | // addTodo: Function, 83 | // removeTodo: Function 84 | // } 85 | 86 | return ( 87 | 89 | ); 90 | 91 | // An alternative to bindActionCreators is to pass 92 | // just the dispatch function down, but then your child component 93 | // needs to import action creators and know about them. 94 | 95 | // return ; 96 | } 97 | } 98 | 99 | export default connect( 100 | TodoListContainer, 101 | state => ({ todos: state.todos }) 102 | ) 103 | ``` 104 | 105 | #### 小贴士 106 | 107 | * You might ask: why don’t we bind the action creators to the store instance right away, like in classical Flux? The problem is that this won’t work well with universal apps that need to render on the server. Most likely you want to have a separate store instance per request so you can prepare them with different data, but binding action creators during their definition means you’re stuck with a single store instance for all requests. 108 | 109 | * If you use ES5, instead of `import * as` syntax you can just pass `require('./TodoActionCreators')` to `bindActionCreators` as the first argument. The only thing it cares about is that the values of the `actionCreators` arguments are functions. The module system doesn’t matter. 110 | -------------------------------------------------------------------------------- /docs/api/combineReducers.md: -------------------------------------------------------------------------------- 1 | # `combineReducers(reducers)` 2 | 3 | As your app grows more complex, you’ll want to split your [reducing function](../Glossary.md#reducer) into separate functions, each managing independent parts of the [state](../Glossary.md#state). 4 | 5 | The `combineReducers` helper function turns an object whose values are different reducing functions into a single 6 | reducing function you can pass to [`createStore`](createStore.md). 7 | 8 | The resulting reducer calls every child reducer, and gather their results into a single state object. The shape of the state object matches the keys of the passed `reducers`. 9 | 10 | >##### Flux 用户使用注意 11 | 12 | >This function helps you organize your reducers to manage their own slices of state, similar to how you would have different Flux Stores to manage different state. With Redux, there is just one store, but `combineReducers` helps you keep the same logical division between reducers. 13 | 14 | #### 参数 15 | 16 | 1. `reducers` (*Object*): An object whose values correspond to different reducing functions that need to be combined into one. One handy way to obtain it is to use ES6 `import * as reducers` syntax, but you can also construct this object manually. See the notes below for some rules every passed reducer must follow. 17 | 18 | #### 返回 19 | 20 | (*Function*): A reducer that invokes every reducer inside the `reducers` object, and constructs a state object with the same shape. 21 | 22 | #### 注意 23 | 24 | This function is mildly opinionated and is skewed towards helping beginners avoid common pitfalls. This is why it attempts to enforce some rules that you don’t have to follow if you write the root reducer manually. 25 | 26 | Any reducer passed to `combineReducers` must satisfy these rules: 27 | 28 | * For any action that is not recognized, it must return the `state` given to it as the first argument. 29 | 30 | * It may never return `undefined`. It is too easy to do this by mistake via an early `return` statement, so `combineReducers` throws if you do that instead of letting the error manifest itself somewhere else. 31 | 32 | * If the `state` given to it is `undefined`, it must return the initial state for this specific reducer. According to the previous rule, the initial state must not be `undefined` either. It is handy to specify it with ES6 optional arguments syntax, but you can also explicitly check the first argument for being `undefined`. 33 | 34 | While `combineReducers` attempts to check that your reducers conform to some of these rules, you should remember them, and do your best to follow them. 35 | 36 | #### 示例 37 | 38 | #### `reducers.js` 39 | 40 | ```js 41 | export function todos(state = [], action) { 42 | switch (action.type) { 43 | case 'ADD_TODO': 44 | return state.concat([action.text]); 45 | default: 46 | return state; 47 | } 48 | } 49 | 50 | export function counter(state = 0, action) { 51 | switch (action.type) { 52 | case 'INCREMENT': 53 | return state + 1; 54 | case 'DECREMENT': 55 | return state - 1; 56 | default: 57 | return state; 58 | } 59 | } 60 | ``` 61 | 62 | #### `App.js` 63 | 64 | ```js 65 | import { createStore, combineReducers } from 'redux'; 66 | 67 | import * as reducers from './reducers'; 68 | console.log(reducers); 69 | // { 70 | // todos: Function, 71 | // counter: Function 72 | // } 73 | 74 | let reducer = combineReducers(reducers); 75 | let store = createStore(reducer); 76 | console.log(store.getState()); 77 | // { 78 | // counter: 0, 79 | // todos: [] 80 | // } 81 | 82 | store.dispatch({ 83 | type: 'ADD_TODO', 84 | text: 'Use Redux' 85 | }); 86 | console.log(store.getState()); 87 | // { 88 | // counter: 0, 89 | // todos: ['Use Redux'] 90 | // } 91 | ``` 92 | 93 | #### 小贴士 94 | 95 | * This helper is just a convenience! You can write your own `combineReducers` that [works differently](https://github.com/acdlite/reduce-reducers), or even assemble the state object from the child reducers manually and write a root reducing function explicitly, like you would write any other function. 96 | 97 | * You may call `combineReducers` at any level of the reducer hierarchy. It doesn’t have to happen at the top. In fact you may use it again to split the child reducers that get too complicated into independent grandchildren, and so on. 98 | -------------------------------------------------------------------------------- /docs/api/compose.md: -------------------------------------------------------------------------------- 1 | # `compose(...functions)` 2 | 3 | 从左到右来组合多个函数。 4 | 5 | 这是函数式编程中的方法,为了方便,被放到了 Redux 里。 6 | 当需要把多个 [store 增强器](../Glossary.md#store-enhancer) 依次执行的时候,需要用到它。 7 | 8 | #### 参数 9 | 10 | 1. (*arguments*): 需要合成的多个函数。每个函数都接收一个函数作为参数,然后返回一个函数。 11 | 12 | #### 返回 13 | 14 | (*Function*): 从左到右把接收到的函数合成后的最终函数。 15 | 16 | #### 示例 17 | 18 | 下面示例演示了如何使用 `compose` 增强 [store](Store.md),这个 store 与 [`applyMiddleware`](applyMiddleware.md) 和 [redux-devtools](https://github.com/gaearon/redux-devtools) 一起使用。 19 | 20 | ```js 21 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 22 | import thunk from 'redux-thunk'; 23 | import * as reducers from '../reducers/index'; 24 | 25 | let reducer = combineReducers(reducers); 26 | let middleware = [thunk]; 27 | 28 | let finalCreateStore; 29 | 30 | // 生产环境中,我们希望只使用 middleware。 31 | // 而在开发环境中,我们还希望使用一些 redux-devtools 提供的一些 store 增强器。 32 | // UglifyJS 会在构建过程中把一些不会执行的死代码去除掉。 33 | 34 | if (process.env.NODE_ENV === 'production') { 35 | finalCreateStore = applyMiddleware(...middleware)(createStore); 36 | } else { 37 | finalCreateStore = compose( 38 | applyMiddleware(...middleware), 39 | require('redux-devtools').devTools(), 40 | require('redux-devtools').persistState( 41 | window.location.href.match(/[?&]debug_session=([^&]+)\b/) 42 | ), 43 | createStore 44 | ); 45 | 46 | // 不使用 compose 来写是这样子: 47 | // 48 | // finalCreateStore = 49 | // applyMiddleware(middleware)( 50 | // devTools()( 51 | // persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))( 52 | // createStore 53 | // ) 54 | // ) 55 | // ); 56 | } 57 | 58 | let store = finalCreateStore(reducer); 59 | ``` 60 | 61 | #### 小贴士 62 | 63 | * `compse` 做的只是让你不使用深度右括号的情况下来写深度嵌套的函数。不要觉得它很复杂。 64 | -------------------------------------------------------------------------------- /docs/api/createStore.md: -------------------------------------------------------------------------------- 1 | # `createStore(reducer, [initialState])` 2 | 3 | 创建一个 Redux [store](Store.md) 来以存放应用中所有的 state。 4 | 应用中应有且仅有一个 store。 5 | 6 | #### 参数 7 | 8 | 1. `reducer` *(Function)*: 接收两个参数,分别是当前的 state 树和要处理的 [action](../Glossary.md#action),返回新的 [state 树](../Glossary.md#state)。 9 | 10 | 2. [`initialState`] *(any)*: 初始时的 state。 11 | 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 [`combineReducers`](combineReducers.md) 创建 `reducer`,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 `reducer` 可理解的内容。[TODO: Optimize] 12 | 13 | #### 返回 14 | 15 | ([*`Store`*](Store.md)): 保存了应用所有 state 的对象。改变 state 的惟一方法是 [dispatch](Store.md#dispatch) action。你也可以 [subscribe 监听](Store.md#subscribe) state 的变化,然后更新 UI。 16 | 17 | #### 示例 18 | 19 | ```js 20 | import { createStore } from 'redux'; 21 | 22 | function todos(state = [], action) { 23 | switch (action.type) { 24 | case 'ADD_TODO': 25 | return state.concat([action.text]); 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | let store = createStore(todos, ['Use Redux']); 32 | 33 | store.dispatch({ 34 | type: 'ADD_TODO', 35 | text: 'Read the docs' 36 | }); 37 | 38 | console.log(store.getState()); 39 | // ['Use Redux', 'Read the docs'] 40 | ``` 41 | 42 | #### 小贴士 43 | 44 | * 应用中不要创建多个 store!相反,使用 [`combineReducers`](combineReducers.md) 来把多个 reducer 创建成一个根 reducer。 45 | 46 | * 你可以决定 state 的格式。你可以使用普通对象或者 [Immutable](http://facebook.github.io/immutable-js/) 这类的实现。如果你不知道如何做,刚开始可以使用普通对象。 47 | 48 | * 如果 state 是普通对象,永远不要修改它!比如,reducer 里不要使用 `Object.assign(state, newData)`,应该使用 `Object.assign({}, state, newData)`。这样才不会覆盖旧的 `state`。也可以使用 [Babel 阶段 1](http://babeljs.io/docs/usage/experimental/) 中的 [ES7 对象的 spread 操作](https://github.com/sebmarkbage/ecmascript-rest-spread) 特性中的 `return { ...state, ...newData }`。 49 | 50 | * 对于服务端运行的同构应用,为每一个请求创建一个 store 实例,以此让 store 相隔离。dispatch 一系列请求数据的 action 到 store 实例上,等待请求完成后再在服务端渲染应用。 51 | 52 | * 当 store 创建后,Redux 会 dispatch 一个 action 到 reducer 上,来用初始的 state 来填充 store。你不需要处理这个 action。但要记住,如果第一个参数也就是传入的 state 如果是 `undefined` 的话,reducer 应该返回初始的 state 值。 53 | -------------------------------------------------------------------------------- /docs/basics/Actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | 首先,让我们来给 action 下个定义。 4 | 5 | **Actions** 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的**惟一**来源。用法是通过 [`store.dispatch()`](../api/Store.md#dispatch) 把 action 传到 store。 6 | 7 | 添加新 todo 任务的 action 是这样的: 8 | 9 | ```js 10 | { 11 | type: 'ADD_TODO', 12 | text: 'Build my first Redux app' 13 | } 14 | ``` 15 | 16 | Action 本质是 JavaScript 普通对象。我们约定,action 内使用一个字符串类型的 `type` 字段来表示将要执行的动作。多数情况下,`type` 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。 17 | 18 | ```js 19 | import { ADD_TODO, REMOVE_TODO } from '../actionTypes'; 20 | ``` 21 | 22 | >##### 样板文件使用提醒 23 | 24 | >使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中最多把它们显式地定义成常量。参照 [减少样板代码](../recipes/ReducingBoilerplate.md) 获取保持代码干净的实践经验。 25 | 26 | 除了 `type` 字段外,action 对象的结构完全取决于你。参照 [Flux 标准 Action](https://github.com/acdlite/flux-standard-action) 获取如何组织 action 的建议。 27 | 28 | 这时,我们还需要再添加一个 action type 来标记任务完成。因为数据是存放在数组中的,我们通过 `index` 来标识任务。实际项目中一般会在新建内容的时候生成惟一的 ID 做标识。 29 | 30 | ```js 31 | { 32 | type: COMPLETE_TODO, 33 | index: 5 34 | } 35 | ``` 36 | **action 中传递的数据越少越好**。比如,这里传递 `index` 就比把整个任务对象传过去要好。 37 | 38 | 最后,再添加一个 action 类型来表示当前展示的任务状态。 39 | 40 | ```js 41 | { 42 | type: SET_VISIBILITY_FILTER, 43 | filter: SHOW_COMPLETED 44 | } 45 | ``` 46 | 47 | ## Action 创建函数 48 | 49 | **Action 创建函数** 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。 50 | 51 | 在 [传统的 Flux](http://facebook.github.io/flux) 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,像这样: 52 | 53 | ```js 54 | function addTodoWithDispatch(text) { 55 | const action = { 56 | type: ADD_TODO, 57 | text 58 | }; 59 | dispatch(action); 60 | } 61 | ``` 62 | 不同的是,Redux 中的 action 创建函数是 **纯函数**,它没有任何副作用,只是返回 action 对象而已。 63 | 64 | ```js 65 | function addTodo(text) { 66 | return { 67 | type: ADD_TODO, 68 | text 69 | }; 70 | } 71 | ``` 72 | 73 | 这让代码更易于测试和移植。只需把 action 创建函数的结果传给 `dispatch()` 方法即可实例化 dispatch。 74 | 75 | ```js 76 | dispatch(addTodo(text)); 77 | dispatch(completeTodo(index)); 78 | ``` 79 | 80 | 或者创建一个 **被绑定的 action 创建函数** 来自动 dispatch: 81 | 82 | ```js 83 | const boundAddTodo = (text) => dispatch(addTodo(text)); 84 | const boundCompleteTodo = (index) => dispatch(CompleteTodo(index)); 85 | ``` 86 | 87 | 可以这样调用: 88 | 89 | ``` 90 | boundAddTodo(text); 91 | boundCompleteTodo(index); 92 | ``` 93 | 94 | store 里能直接通过 [`store.dispatch()`](../api/Store.md#dispatch) 调用 `dispatch()` 方法,但是多数情况下你会使用 [react-redux](http://github.com/gaearon/react-redux) 提供的 `connect()` 帮助器来调用。[`bindActionCreators()`](../api/bindActionCreators.md) 可以自动把多个 action 创建函数 绑定到 `dispatch()` 方法上。 95 | 96 | ## 源码 97 | 98 | ### `actions.js` 99 | 100 | ```js 101 | /* 102 | * action 类型 103 | */ 104 | 105 | export const ADD_TODO = 'ADD_TODO'; 106 | export const COMPLETE_TODO = 'COMPLETE_TODO'; 107 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'; 108 | 109 | /* 110 | * 其它的常量 111 | */ 112 | 113 | export const VisibilityFilters = { 114 | SHOW_ALL: 'SHOW_ALL', 115 | SHOW_COMPLETED: 'SHOW_COMPLETED', 116 | SHOW_ACTIVE: 'SHOW_ACTIVE' 117 | }; 118 | 119 | /* 120 | * action 创建函数 121 | */ 122 | 123 | export function addTodo(text) { 124 | return { type: ADD_TODO, text }; 125 | } 126 | 127 | export function completeTodo(index) { 128 | return { type: COMPLETE_TODO, index }; 129 | } 130 | 131 | export function setVisibilityFilter(filter) { 132 | return { type: SET_VISIBILITY_FILTER, filter }; 133 | } 134 | ``` 135 | 136 | ## 下一步 137 | 138 | 现在让我们 [开发一些 reducers](Reducers.md) 来指定发起 action 后 state 应该如何更新。 139 | 140 | >##### 高级用户建议 141 | >如果你已经熟悉这些基本概念且已经完成了这个示例,不要忘了看一下在 [高级教程](../advanced/README.md) 中的 [异步 actions] (../advanced/AsyncActions.md),你将学习如何处理 AJAX 响应和如何把 action 创建函数组合成异步控制流。 -------------------------------------------------------------------------------- /docs/basics/DataFlow.md: -------------------------------------------------------------------------------- 1 | # 数据流 2 | 3 | **严格的单向数据流**是 Redux 架构的设计核心。 4 | 5 | 这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个,独立的无法相互引用的重复数据。 6 | 7 | 如何这些理由还不足以今你信服,读一下 [动机](../introduction/Motivation.md) 和 [Flux 案例](https://medium.com/@dan_abramov/the-case-for-flux-379b7d1982c6),这里面有更加详细的单向数据流优势分析。虽然 [Redux 就不是严格意义上的 [Flux](../introduction/Relation to Other Libraries.md),但它们有共同的设计思想。 8 | 9 | Redux 应用中数据的生命周期遵循下面 4 个步骤: 10 | 11 | 1. **调用** [`store.dispatch(action)`](../api/Store.md#dispatch)。 12 | 13 | action 就是一个描述“发生了什么”的普通对象。比如: 14 | 15 | ```js 16 | { type: 'LIKE_ARTICLE', articleId: 42 }; 17 | { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Megan' } }; 18 | { type: 'ADD_TODO', text: 'Read the Redux docs.'}; 19 | ``` 20 | 21 | 可以把 action 理解成新闻的摘要。如 “玛丽喜欢42号文章。” 或者 “任务列表里添加了'学习 Redux 文档'”。 22 | 23 | 你可以在任何地方调用 [`store.dispatch(action)`](../api/Store.md#dispatch),包括组件中、XHR 回调中、甚至定时器中。 24 | 25 | 2. **Redux store 调用传入的 reducer 函数。** 26 | 27 | Store 会把两个参数传入 reducer,当前的 state 树和 action。例如,在这个 todo 应用中,根 reducer 可能接收这样的数据: 28 | 29 | ```js 30 | // 当前应用的 state(todos 列表和选中的过滤器) 31 | let previousState = { 32 | visibleTodoFilter: 'SHOW_ALL', 33 | todos: [{ 34 | text: 'Read the docs.', 35 | complete: false 36 | }] 37 | }; 38 | 39 | // 将要执行的 action(添加一个 todo) 40 | let action = { 41 | type: 'ADD_TODO', 42 | text: 'Understand the flow.' 43 | }; 44 | 45 | // render 返回处理后的应用状态 46 | let nextState = todoApp(previousState, action); 47 | ``` 48 | 49 | 注意 reducer 是纯函数。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。 50 | 51 | 3. **根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。** 52 | 53 | 根 reducer 的结构完全由你决定。Redux 原生提供[`combineReducers()`](../api/combineReducers.md)辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。 54 | 55 | 下面演示 [`combineReducers()`](../api/combineReducers.md) 如何使用。假如你有一个 todos 列表,使用当前的选择过滤器来追踪两个 reducers(原文:and the currently selected filter setting to keep track of with two reducers): 56 | 57 | ```js 58 | function todos(state = [], action) { 59 | // 省略处理逻辑... 60 | return nextState; 61 | } 62 | 63 | function visibleTodoFilter(state = 'SHOW_ALL', action) { 64 | // 省略处理逻辑... 65 | return nextState; 66 | } 67 | 68 | let todoApp = combineReducers({ 69 | todos, 70 | visibleTodoFilter 71 | }); 72 | ``` 73 | 74 | 当你触发 action 后,`combineReducers` 返回的 `todoApp` 会负责调用两个 reducer: 75 | 76 | ```js 77 | let nextTodos = todos(state.todos, action); 78 | let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action); 79 | ``` 80 | 81 | 然后会把两个结果集合并成一个 state 树: 82 | 83 | ```js 84 | return { 85 | todos: nextTodos, 86 | visibleTodoFilter: nextVisibleTodoFilter 87 | }; 88 | ``` 89 | 90 | 虽然 [`combineReducers()`](../api/combineReducers.md) 是一个很方便的辅助工具,你也可以选择不用;你可以自行实现自己的根 reducer! 91 | 92 | 4. **Redux store 保存了根 reducer 返回的完整 state 树。** 93 | 94 | 这个新的树就是应用的下一个 state!所有订阅 [`store.subscribe(listener)`](../api/Store.md#subscribe) 的监听器都将被调用;监听器里可以调用 [`store.getState()`](../api/Store.md#getState) 获得当前 state。 95 | 96 | 现在,可以应用新的 state 来更新 UI。如果你使用了 [React Redux](https://github.com/gaearon/react-redux) 这类的绑定库,这时就应该调用 `component.setState(newState)` 来更新。 97 | 98 | ## 下一步 99 | 100 | 现在你已经理解了 Redux 如何工作,是时候[结合 React 开发应用](UsageWithReact.md)了。 101 | 102 | >##### 高级用户使用注意 103 | >如果你已经熟悉了基础概念且完成了这个教程,可以学习[高级教程](../advanced/README.md)中的[异步数据流](../advanced/AsyncFlow.md),你将学到如何使用 middleware 在 [异步 action](../advanced/AsyncActions.md) 到达 reducer 前处理它们。 104 | -------------------------------------------------------------------------------- /docs/basics/ExampleTodoList.md: -------------------------------------------------------------------------------- 1 | # 示例: Todo 列表 2 | 3 | 这是我们在[基础教程](./README.md)里开发的迷你型的任务管理应用的完整源码。 4 | 5 | ## 入口文件 6 | 7 | #### `index.js` 8 | 9 | ```js 10 | import React from 'react'; 11 | import { createStore } from 'redux'; 12 | import { Provider } from 'react-redux'; 13 | import App from './containers/App'; 14 | import todoApp from './reducers'; 15 | 16 | let store = createStore(todoApp); 17 | 18 | let rootElement = document.getElementById('root'); 19 | React.render( 20 | // 为了解决 React 0.13 的问题, 21 | // 一定要把 child 用函数包起来。 22 | 23 | {() => } 24 | , 25 | rootElement 26 | ); 27 | ``` 28 | 29 | ## Action 创建函数和常量 30 | 31 | #### `actions.js` 32 | 33 | ```js 34 | /* 35 | * action types 36 | */ 37 | 38 | export const ADD_TODO = 'ADD_TODO'; 39 | export const COMPLETE_TODO = 'COMPLETE_TODO'; 40 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' 41 | 42 | /* 43 | * 其它的常量 44 | */ 45 | 46 | export const VisibilityFilters = { 47 | SHOW_ALL: 'SHOW_ALL', 48 | SHOW_COMPLETED: 'SHOW_COMPLETED', 49 | SHOW_ACTIVE: 'SHOW_ACTIVE' 50 | }; 51 | 52 | /* 53 | * action 创建函数 54 | */ 55 | 56 | export function addTodo(text) { 57 | return { type: ADD_TODO, text }; 58 | } 59 | 60 | export function completeTodo(index) { 61 | return { type: COMPLETE_TODO, index }; 62 | } 63 | 64 | export function setVisibilityFilter(filter) { 65 | return { type: SET_VISIBILITY_FILTER, filter }; 66 | } 67 | ``` 68 | 69 | ## Reducers 70 | 71 | #### `reducers.js` 72 | 73 | ```js 74 | import { combineReducers } from 'redux'; 75 | import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'; 76 | const { SHOW_ALL } = VisibilityFilters; 77 | 78 | function visibilityFilter(state = SHOW_ALL, action) { 79 | switch (action.type) { 80 | case SET_VISIBILITY_FILTER: 81 | return action.filter; 82 | default: 83 | return state; 84 | } 85 | } 86 | 87 | function todos(state = [], action) { 88 | switch (action.type) { 89 | case ADD_TODO: 90 | return [...state, { 91 | text: action.text, 92 | completed: false 93 | }]; 94 | case COMPLETE_TODO: 95 | return [ 96 | ...state.slice(0, action.index), 97 | Object.assign({}, state[action.index], { 98 | completed: true 99 | }), 100 | ...state.slice(action.index + 1) 101 | ]; 102 | default: 103 | return state; 104 | } 105 | } 106 | 107 | const todoApp = combineReducers({ 108 | visibilityFilter, 109 | todos 110 | }); 111 | 112 | export default todoApp; 113 | ``` 114 | 115 | ## 智能组件 116 | 117 | #### `containers/App.js` 118 | 119 | ```js 120 | import React, { Component, PropTypes } from 'react'; 121 | import { connect } from 'react-redux'; 122 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'; 123 | import AddTodo from '../components/AddTodo'; 124 | import TodoList from '../components/TodoList'; 125 | import Footer from '../components/Footer'; 126 | 127 | class App extends Component { 128 | render() { 129 | // Injected by connect() call: 130 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 131 | return ( 132 |
133 | 135 | dispatch(addTodo(text)) 136 | } /> 137 | 140 | dispatch(completeTodo(index)) 141 | } /> 142 |
145 | dispatch(setVisibilityFilter(nextFilter)) 146 | } /> 147 |
148 | ); 149 | } 150 | } 151 | 152 | App.propTypes = { 153 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 154 | text: PropTypes.string.isRequired, 155 | completed: PropTypes.bool.isRequired 156 | })), 157 | visibilityFilter: PropTypes.oneOf([ 158 | 'SHOW_ALL', 159 | 'SHOW_COMPLETED', 160 | 'SHOW_ACTIVE' 161 | ]).isRequired 162 | }; 163 | 164 | function selectTodos(todos, filter) { 165 | switch (filter) { 166 | case VisibilityFilters.SHOW_ALL: 167 | return todos; 168 | case VisibilityFilters.SHOW_COMPLETED: 169 | return todos.filter(todo => todo.completed); 170 | case VisibilityFilters.SHOW_ACTIVE: 171 | return todos.filter(todo => !todo.completed); 172 | } 173 | } 174 | 175 | // Which props do we want to inject, given the global state? 176 | // Note: use https://github.com/faassen/reselect for better performance. 177 | function select(state) { 178 | return { 179 | visibleTodos: selectTodos(state.todos, state.visibilityFilter), 180 | visibilityFilter: state.visibilityFilter 181 | }; 182 | } 183 | 184 | // Wrap the component to inject dispatch and state into it 185 | export default connect(select)(App); 186 | ``` 187 | 188 | ## 木偶组件 189 | 190 | #### `components/AddTodo.js` 191 | 192 | ```js 193 | import React, { findDOMNode, Component, PropTypes } from 'react'; 194 | 195 | export default class AddTodo extends Component { 196 | render() { 197 | return ( 198 |
199 | 200 | 203 |
204 | ); 205 | } 206 | 207 | handleClick(e) { 208 | const node = findDOMNode(this.refs.input); 209 | const text = node.value.trim(); 210 | this.props.onAddClick(text); 211 | node.value = ''; 212 | } 213 | } 214 | 215 | AddTodo.propTypes = { 216 | onAddClick: PropTypes.func.isRequired 217 | }; 218 | ``` 219 | 220 | #### `components/Footer.js` 221 | 222 | ```js 223 | import React, { Component, PropTypes } from 'react'; 224 | 225 | export default class Footer extends Component { 226 | renderFilter(filter, name) { 227 | if (filter === this.props.filter) { 228 | return name; 229 | } 230 | 231 | return ( 232 | { 233 | e.preventDefault(); 234 | this.props.onFilterChange(filter); 235 | }}> 236 | {name} 237 | 238 | ); 239 | } 240 | 241 | render() { 242 | return ( 243 |

244 | Show: 245 | {' '} 246 | {this.renderFilter('SHOW_ALL', 'All')} 247 | {', '} 248 | {this.renderFilter('SHOW_COMPLETED', 'Completed')} 249 | {', '} 250 | {this.renderFilter('SHOW_ACTIVE', 'Active')} 251 | . 252 |

253 | ); 254 | } 255 | } 256 | 257 | Footer.propTypes = { 258 | onFilterChange: PropTypes.func.isRequired, 259 | filter: PropTypes.oneOf([ 260 | 'SHOW_ALL', 261 | 'SHOW_COMPLETED', 262 | 'SHOW_ACTIVE' 263 | ]).isRequired 264 | }; 265 | ``` 266 | 267 | #### `components/Todo.js` 268 | 269 | ```js 270 | import React, { Component, PropTypes } from 'react'; 271 | 272 | export default class Todo extends Component { 273 | render() { 274 | return ( 275 |
  • 281 | {this.props.text} 282 |
  • 283 | ); 284 | } 285 | } 286 | 287 | Todo.propTypes = { 288 | onClick: PropTypes.func.isRequired, 289 | text: PropTypes.string.isRequired, 290 | completed: PropTypes.bool.isRequired 291 | }; 292 | ``` 293 | 294 | #### `components/TodoList.js` 295 | 296 | ```js 297 | import React, { Component, PropTypes } from 'react'; 298 | import Todo from './Todo'; 299 | 300 | export default class TodoList extends Component { 301 | render() { 302 | return ( 303 |
      304 | {this.props.todos.map((todo, index) => 305 | this.props.onTodoClick(index)} /> 308 | )} 309 |
    310 | ); 311 | } 312 | } 313 | 314 | TodoList.propTypes = { 315 | onTodoClick: PropTypes.func.isRequired, 316 | todos: PropTypes.arrayOf(PropTypes.shape({ 317 | text: PropTypes.string.isRequired, 318 | completed: PropTypes.bool.isRequired 319 | }).isRequired).isRequired 320 | }; 321 | ``` 322 | -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # 基础 2 | 3 | 不要被各种关于 reducers, middleware, store 的演讲所蒙蔽。Redux 实际是非常简单的。如果你有 Flux 开发经验,用起来会非常习惯。(没用过 Flux 也不怕,很容易!) 4 | 5 | 下面的教程将会一步步教你开发简单的 Todo 应用。 6 | 7 | * [Actions](Actions.md) 8 | * [Reducers](Reducers.md) 9 | * [Store](Store.md) 10 | * [数据流](DataFlow.md) 11 | * [搭配 React](UsageWithReact.md) 12 | * [示例:Todo 列表](ExampleTodoList.md) 13 | -------------------------------------------------------------------------------- /docs/basics/Reducers.md: -------------------------------------------------------------------------------- 1 | # Reducers 2 | 3 | [Actions](./Actions.md) 只是描述了**有事情发生了**这一事实,并没有指明应用如何更新 state。这是 reducer 要做的事情。 4 | 5 | ## 设计 State 结构 6 | 7 | 应用所有的 state 都被保存在一个单一对象中(我们称之为 state 树)。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来? 8 | 9 | 以 todo 应用为例,需要保存两个不同的内容: 10 | 11 | * 当前选中的任务过滤条件; 12 | * 真实的任务列表。 13 | 14 | 通常,这个 state 树还需要存放其它一些数据,还有界面 state。这样做没问题,但尽量把这些数据与界面 state 分开。 15 | 16 | ```js 17 | { 18 | visibilityFilter: 'SHOW_ALL', 19 | todos: [{ 20 | text: 'Consider using Redux', 21 | completed: true, 22 | }, { 23 | text: 'Keep all state in a single tree', 24 | completed: false 25 | }] 26 | } 27 | ``` 28 | 29 | >##### 处理 Reducer 关系时注意 30 | 31 | >开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同数据相互引用时通过 ID 来查找。把应用的 state 想像成数据库。这种方法在 [normalizr](https://github.com/gaearon/normalizr) 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 `todosById: { id -> todo }` 和 `todos: array` 是比较好的方式(虽然你可以觉得冗余)。 32 | 33 | ## Action 处理 34 | 35 | 现在我们已经确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个函数,接收旧的 state 和 action,返回新的 state。 36 | 37 | ```js 38 | (previousState, action) => newState 39 | ``` 40 | 41 | 之所以称作 reducer 是因为和 [`Array.prototype.reduce(reducer, ?initialValue)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 格式很像。保持 reducer 纯净非常重要。永远**不要**在 reducer 里做这些操作: 42 | 43 | * 修改传入参数; 44 | * 执行有副作用的操作,如 API 请求和路由跳转。 45 | 46 | 在[高级篇](../advanced/README.md)里会介绍如何执行有副作用的操作。现在只需要谨记 reducer 一定要保持纯净。**只要传入参数一样,返回必须一样。没有特殊情况、没有副作用,没有 API 请求、没有修改参数,单纯执行计算。** 47 | 48 | 明白了这些之后,就可以开始编写 reducer,并让它来处理之前定义过的 [actions](Actions.md)。 49 | 50 | 我们在开始时定义默认的 state。Redux 首次执行时,state 为 `undefined`,这时候会返回默认 state。 51 | 52 | ```js 53 | import { VisibilityFilters } from './actions'; 54 | 55 | const initialState = { 56 | visibilityFilter: VisibilityFilters.SHOW_ALL, 57 | todos: [] 58 | }; 59 | 60 | function todoApp(state, action) { 61 | if (typeof state === 'undefined') { 62 | return initialState; 63 | } 64 | 65 | // 这里暂不处理任何 action, 66 | // 仅返回传入的 state。 67 | return state; 68 | } 69 | ``` 70 | 71 | 这里一个技巧是使用 [ES6 参数默认值语法](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/default_parameters) 来精简代码。 72 | 73 | ```js 74 | function todoApp(state = initialState, action) { 75 | // 这里暂不处理任何 action, 76 | // 仅返回传入的 state。 77 | return state; 78 | } 79 | ``` 80 | 81 | 现在可以处理 `SET_VISIBILITY_FILTER`。需要做的只是改变 state 中的 `visibilityFilter`。 82 | 83 | ```js 84 | function todoApp(state = initialState, action) { 85 | switch (action.type) { 86 | case SET_VISIBILITY_FILTER: 87 | return Object.assign({}, state, { 88 | visibilityFilter: action.filter 89 | }); 90 | default: 91 | return state; 92 | } 93 | } 94 | ``` 95 | 96 | 注意: 97 | 98 | 1. **不要修改 `state`。** 使用 [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 新建了一个副本。不能这样使用 `Object.assign(state, { visibilityFilter: action.filter })`,因为它会改变第一个参数的值。**一定**要把第一个参数设置为空对象。也可以使用 ES7 中还在试验阶段的特性 `{ ...state, ...newState }`,参考 [对象展开语法](https://github.com/sebmarkbage/ecmascript-rest-spread)。 99 | 100 | 2. **在 `default` 情况下返回旧的 `state`。**遇到未知的 action 时,一定要返回旧的 `state`。 101 | 102 | >##### `Object.assign` 使用提醒 103 | 104 | >[`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 是 ES6 特性,但多数浏览器并不支持。你要么使用 polyfill,[Babel 插件](https://github.com/babel-plugins/babel-plugin-object-assign),或者使用其它库如 [`_.assign()`](https://lodash.com/docs#assign) 提供的帮助方法。 105 | 106 | >##### `switch` 和样板代码提醒 107 | 108 | >`state` 语句并不是严格意义上的样板代码。Flux 中真实的样板代码是概念性的:更新必须要发送、Store 必须要注册到 Dispatcher、Store 必须是对象(开发同构应用时变得非常复杂)。为了解决这些问题,Redux 放弃了 event emitters(事件发送器),转而使用纯 reducer。 109 | 110 | >很不幸到现在为步,还有很多人存在一个误区:根据文档中是否使用 `switch` 来决定是否使用它。如果你不喜欢 `switch`,完全可以自定义一个 `createReducer` 函数来接收一个事件处理函数列表,参照["减少样板代码"](../recipes/ReducingBoilerplate.md#reducers)。 111 | 112 | ## 处理多个 action 113 | 114 | 还有两个 action 需要处理。让我们先处理 `ADD_TODO`。 115 | 116 | ```js 117 | function todoApp(state = initialState, action) { 118 | switch (action.type) { 119 | case SET_VISIBILITY_FILTER: 120 | return Object.assign({}, state, { 121 | visibilityFilter: action.filter 122 | }); 123 | case ADD_TODO: 124 | return Object.assign({}, state, { 125 | todos: [...state.todos, { 126 | text: action.text, 127 | completed: false 128 | }] 129 | }); 130 | default: 131 | return state; 132 | } 133 | } 134 | ``` 135 | 136 | 如上,不直接修改 `state` 中的字段,而是返回新对象。新的 `todos` 对象就相当于旧的 `todos` 在末尾加上新建的 todo。而这个新的 todo 又是在 action 中创建的。 137 | 138 | 最后,`COMPLETE_TODO` 的实现也很好理解: 139 | 140 | ```js 141 | case COMPLETE_TODO: 142 | return Object.assign({}, state, { 143 | todos: [ 144 | ...state.todos.slice(0, action.index), 145 | Object.assign({}, state.todos[action.index], { 146 | completed: true 147 | }), 148 | ...state.todos.slice(action.index + 1) 149 | ] 150 | }); 151 | ``` 152 | 153 | 因为我们不能直接修改却要更新数组中指定的一项数据,这里需要先把前面和后面都切开。如果经常需要这类的操作,可以选择使用帮助类 [React.addons.update](https://facebook.github.io/react/docs/update.html),[updeep](https://github.com/substantial/updeep),或者使用原生支持深度更新的库 [Immutable](http://facebook.github.io/immutable-js/)。最后,时刻谨记永远不要在克隆 `state` 前修改它。 154 | 155 | ## 拆分 Reducer 156 | 157 | 目前的代码看起来有些冗余: 158 | 159 | ```js 160 | function todoApp(state = initialState, action) { 161 | switch (action.type) { 162 | case SET_VISIBILITY_FILTER: 163 | return Object.assign({}, state, { 164 | visibilityFilter: action.filter 165 | }); 166 | case ADD_TODO: 167 | return Object.assign({}, state, { 168 | todos: [...state.todos, { 169 | text: action.text, 170 | completed: false 171 | }] 172 | }); 173 | case COMPLETE_TODO: 174 | return Object.assign({}, state, { 175 | todos: [ 176 | ...state.todos.slice(0, action.index), 177 | Object.assign({}, state.todos[action.index], { 178 | completed: true 179 | }), 180 | ...state.todos.slice(action.index + 1) 181 | ] 182 | }); 183 | default: 184 | return state; 185 | } 186 | } 187 | ``` 188 | 189 | 上面代码能否变得更通俗易懂?这里的 `todos` 和 `visibilityFilter` 的更新看起来是相互独立的。有时 state 中的字段是相互依赖的,需要认真考虑,但在这个案例中我们可以把 `todos` 更新的业务逻辑拆分到一个单独的函数里: 190 | 191 | ```js 192 | function todos(state = [], action) { 193 | switch (action.type) { 194 | case ADD_TODO: 195 | return [...state, { 196 | text: action.text, 197 | completed: false 198 | }]; 199 | case COMPLETE_TODO: 200 | return [ 201 | ...state.slice(0, action.index), 202 | Object.assign({}, state[action.index], { 203 | completed: true 204 | }), 205 | ...state.slice(action.index + 1) 206 | ]; 207 | default: 208 | return state; 209 | } 210 | } 211 | 212 | function todoApp(state = initialState, action) { 213 | switch (action.type) { 214 | case SET_VISIBILITY_FILTER: 215 | return Object.assign({}, state, { 216 | visibilityFilter: action.filter 217 | }); 218 | case ADD_TODO: 219 | case COMPLETE_TODO: 220 | return Object.assign({}, state, { 221 | todos: todos(state.todos, action) 222 | }); 223 | default: 224 | return state; 225 | } 226 | } 227 | ``` 228 | 229 | 注意 `todos` 依旧接收 `state`,但它变成了一个数组!现在 `todoApp` 只把需要更新的一部分 state 传给 `todos` 函数,`todos` 函数自己确定如何更新这部分数据。**这就是所谓的 **reducer 合成**,它是开发 Redux 应用最基础的模式。** 230 | 231 | 下面深入探讨一下如何做 reducer 合成。能否抽出一个 reducer 来专门管理 `visibilityFilter`?当然可以: 232 | 233 | ```js 234 | function visibilityFilter(state = SHOW_ALL, action) { 235 | switch (action.type) { 236 | case SET_VISIBILITY_FILTER: 237 | return action.filter; 238 | default: 239 | return state; 240 | } 241 | } 242 | ``` 243 | 244 | 现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果给子 reducer 传入 `undefined` 只要返回它们的默认值即可。 245 | 246 | ```js 247 | function todos(state = [], action) { 248 | switch (action.type) { 249 | case ADD_TODO: 250 | return [...state, { 251 | text: action.text, 252 | completed: false 253 | }]; 254 | case COMPLETE_TODO: 255 | return [ 256 | ...state.slice(0, action.index), 257 | Object.assign({}, state[action.index], { 258 | completed: true 259 | }), 260 | ...state.slice(action.index + 1) 261 | ]; 262 | default: 263 | return state; 264 | } 265 | } 266 | 267 | function visibilityFilter(state = SHOW_ALL, action) { 268 | switch (action.type) { 269 | case SET_VISIBILITY_FILTER: 270 | return action.filter; 271 | default: 272 | return state; 273 | } 274 | } 275 | 276 | function todoApp(state = {}, action) { 277 | return { 278 | visibilityFilter: visibilityFilter(state.visibilityFilter, action), 279 | todos: todos(state.todos, action) 280 | }; 281 | } 282 | ``` 283 | 284 | **注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 `state` 参数都不同,分别对应它管理的那部分 state 数据。** 285 | 286 | 现在看过起来好多了!随着应用的膨胀,我们已经学会把 reducer 拆分成独立文件来分别处理不同的数据域了。 287 | 288 | 最后,Redux 提供了 [`combineReducers()`](../api/combineReducers.md) 工具类来做上面 `todoApp` 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 `todoApp`: 289 | 290 | ```js 291 | import { combineReducers } from 'redux'; 292 | 293 | const todoApp = combineReducers({ 294 | visibilityFilter, 295 | todos 296 | }); 297 | 298 | export default todoApp; 299 | ``` 300 | 301 | 注意上面的写法和下面完全等价: 302 | 303 | ```js 304 | export default function todoApp(state, action) { 305 | return { 306 | visibilityFilter: visibilityFilter(state.visibilityFilter, action), 307 | todos: todos(state.todos, action) 308 | }; 309 | } 310 | ``` 311 | 312 | 你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价: 313 | 314 | ```js 315 | const reducer = combineReducers({ 316 | a: doSomethingWithA, 317 | b: processB, 318 | c: c 319 | }); 320 | ``` 321 | 322 | ```js 323 | function reducer(state, action) { 324 | return { 325 | a: doSomethingWithA(state.a, action), 326 | b: processB(state.b, action), 327 | c: c(state.c, action) 328 | }; 329 | } 330 | ``` 331 | 332 | [`combineReducers()`](../api/combineReducers.md) 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer **根据它们的 key 来筛选出 state 中的一部分数据并处理**,然后这个生成的函数所所有 reducer 的结果合并成一个大的对象。[没有任何魔法。](https://github.com/gaearon/redux/issues/428#issuecomment-129223274) 333 | 334 | >##### ES6 用户使用注意 335 | 336 | >`combineReducers` 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 `export` 暴露出每个 reducer 函数,然后使用 `import * as reducers` 得到一个以它们名字作为 key 的 object: 337 | 338 | >```js 339 | >import { combineReducers } from 'redux'; 340 | >import * as reducers from './reducers'; 341 | > 342 | >const todoApp = combineReducers(reducers); 343 | >``` 344 | > 345 | >由于 `import *` 还是比较新的语法,为了避免[困惑](https://github.com/gaearon/redux/issues/428#issuecomment-129223274),我们不会在文档使用它。但在一些社区示例中你可能会遇到它们。 346 | 347 | ## 源码 348 | 349 | #### `reducers.js` 350 | 351 | ```js 352 | import { combineReducers } from 'redux'; 353 | import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'; 354 | const { SHOW_ALL } = VisibilityFilters; 355 | 356 | function visibilityFilter(state = SHOW_ALL, action) { 357 | switch (action.type) { 358 | case SET_VISIBILITY_FILTER: 359 | return action.filter; 360 | default: 361 | return state; 362 | } 363 | } 364 | 365 | function todos(state = [], action) { 366 | switch (action.type) { 367 | case ADD_TODO: 368 | return [...state, { 369 | text: action.text, 370 | completed: false 371 | }]; 372 | case COMPLETE_TODO: 373 | return [ 374 | ...state.slice(0, action.index), 375 | Object.assign({}, state[action.index], { 376 | completed: true 377 | }), 378 | ...state.slice(action.index + 1) 379 | ]; 380 | default: 381 | return state; 382 | } 383 | } 384 | 385 | const todoApp = combineReducers({ 386 | visibilityFilter, 387 | todos 388 | }); 389 | 390 | export default todoApp; 391 | ``` 392 | 393 | ## 下一步 394 | 395 | 接下来会学习 [创建 Redux store](Store.md)。store 能维持应用的 state,并在当你发起 action 的时候调用 reducer。 396 | -------------------------------------------------------------------------------- /docs/basics/Store.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | 上面章节中,我们学会了使用 [action](Action.md) 来描述“发生了什么”,和使用 [reducers](Reducers.md) 来根据 action 更新 state 的用法。 4 | 5 | **Store** 就是把它们联系到一起的对象。Store 有以下职责: 6 | 7 | * 维持应用的 state; 8 | * 提供 [`getState()`](../api/Store.md#getState) 方法获取 state; 9 | * 提供 [`dispatch(action)`](../api/Store.md#dispatch) 方法更新 state; 10 | * 通过 [`subscribe(listener)`](../api/Store.md#subscribe) 注册监听器。 11 | 12 | 再次强调一下 **Redux 应用只有一个单一的 store**。当需要拆分处理数据的逻辑时,使用 [reducer 组合](Reducers.md#splitting-reducers) 而不是创建多个 store。 13 | 14 | 根据 reducer 创建 store 非常容易。例如,假如应用中只有一个 `todoApp` 的 reducer,可以这样写: 15 | 16 | ```js 17 | import { createStore } from 'redux'; 18 | import todoApp from './reducers'; 19 | 20 | let store = createStore(todoApp); 21 | ``` 22 | 23 | 为了提高可维护性,拆分成多个 reducer,这时需要使用 [`combineReducers()`](../api/combineReducers.md) 来把它们组合起来。 24 | 25 | ```js 26 | import { combineReducers, createStore } from 'redux'; 27 | import * as reducers from './reducers'; 28 | 29 | let todoApp = combineReducers(reducers); 30 | let store = createStore(todoApp); 31 | ``` 32 | 33 | [`createStore()`](../api/createStore.md) 的第二个参数可以设置初始状态。 34 | 这对开发同构应用时非常有用,可以用于把服务器端生成的 state 转变后在浏览器端传给应用。 35 | 36 | ```js 37 | let store = createStore(todoApp, window.STATE_FROM_SERVER); 38 | ``` 39 | 40 | ## 发起 Actions 41 | 42 | 创建好了 store 后,就可以验证程序是否工作。虽然还没有界面,我们已经可以测试更新逻辑了。 43 | 44 | ```js 45 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from './actions'; 46 | 47 | // 打印初始状态 48 | console.log(store.getState()); 49 | 50 | // 监听 state 更新时,打印日志 51 | let unsubscribe = store.subscribe(() => 52 | console.log(store.getState()) 53 | ); 54 | 55 | // 发起一系列 action 56 | store.dispatch(addTodo('Learn about actions')); 57 | store.dispatch(addTodo('Learn about reducers')); 58 | store.dispatch(addTodo('Learn about store')); 59 | store.dispatch(completeTodo(0)); 60 | store.dispatch(completeTodo(1)); 61 | store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)); 62 | 63 | // 停止监听 state 更新 64 | unsubscribe(); 65 | ``` 66 | 67 | 可以看到 store 里的 state 是如何变化的: 68 | 69 | 70 | 71 | 可以看到,在还没有开发界面的时候,我们就可以定义程序的行为。而且这时候已经可以写 reducer 和 action 创建函数的测试。不需要模拟任何东西,因为它们都是纯函数。只需调用一下,对返回值做断言,写测试就是这么简单。 72 | 73 | ## 源码 74 | 75 | #### `index.js` 76 | 77 | ```js 78 | import { combineReducers, createStore } from 'redux'; 79 | import * as reducers from './reducers'; 80 | 81 | let todoApp = combineReducers(reducers); 82 | let store = createStore(todoApp); 83 | ``` 84 | 85 | ## 下一步 86 | 87 | 在创建 todo 应用界面之前,我们先穿插学习一下[数据在 Redux 应用中如何流动的](DataFlow.md) 88 | -------------------------------------------------------------------------------- /docs/basics/UsageWithReact.md: -------------------------------------------------------------------------------- 1 | # 搭配 React 2 | 3 | 这里需要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。 4 | 5 | 尽管如此,Redux 还是和 [React](http://facebook.github.io/react/) 和 [Deku](https://github.com/dekujs/deku) 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。 6 | 7 | 下面使用 React 来开发一个 todo 任务管理应用。 8 | 9 | ## 安装 React Redux 10 | 11 | Redux 默认并不包含 [React 绑定库](https://github.com/gaearon/react-redux),需要单独安装。 12 | 13 | ``` 14 | npm install --save react-redux 15 | ``` 16 | 17 | ## 智能组件(Smart Components)和木偶组件(Dumb Components) 18 | 19 | Redux 的 React 绑定库拥抱了 [智能组件和木偶组件相分离](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 的开发思想。 20 | 21 | 明智的做法是只在最顶层组件(如路由操作)里使用 Redux。内部组件应该像木偶一样保持“呆滞”,所有数据都通过 props 传入。 22 | 23 |
    24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 |
    LocationAware of ReduxTo read dataTo change data
    “Smart” ComponentsTop level, route handlersYes 39 | Subscribe to Redux stateDispatch Redux actions
    “Dumb” ComponentsMiddle and leaf componentsNo 46 | Read data from propsInvoke callbacks from props
    51 |
    52 | 53 | 在这个 todo 应用中,只应有一个智能组件,它存在于组件的最顶层。在复杂的应用中,也有可能会有多个智能组件。虽然你也可以嵌套使用智能组件,但应该尽可能的使用传递 props 的形式。 54 | 55 | ## 设计组件层次结构 56 | 57 | 还记得当初如何 [设计 reducer 结构](Reducers.md) 吗?现在就要定义与它匹配的界面的层次结构。其实这不是 Redux 相关的工作,[React 开发思想](https://facebook.github.io/react/docs/thinking-in-react.html)在这方面解释的非常棒。 58 | 59 | Our design brief is simple. We want to show a list of todo items. On click, a todo item is crossed out as completed. We want to show a field where user may add a new todo. In the footer, we want to show a toggle to show all / only completed / only incompleted todos. 60 | 61 | I see the following components (and their props) emerge from this brief: 62 | 63 | * **`AddTodo`** is an input field with a button. 64 | - `onAddClick(text: string)` is a callback to invoke when a button is pressed. 65 | * **`TodoList`** is a list showing visible todos. 66 | - `todos: Array` is an array of todo items with `{ text, completed }` shape. 67 | - `onTodoClick(index: number)` is a callback to invoke when a todo is clicked. 68 | * **`Todo`** is a single todo item. 69 | - `text: string` is the text to show. 70 | - `completed: boolean` is whether todo should appear crossed out. 71 | - `onClick()` is a callback to invoke when a todo is clicked. 72 | * **`Footer`** is a component where we let user change visible todo filter. 73 | - `filter: string` is the current filter: `'SHOW_ALL'`, `'SHOW_COMPLETED'` or `'SHOW_ACTIVE'`. 74 | - `onFilterChange(nextFilter: string)`: Callback to invoke when user chooses a different filter. 75 | 76 | These are all “dumb” components. They don’t know *where* the data comes from, or *how* to change it. They only render what’s given to them. 77 | 78 | If you migrate from Redux to something else, you’ll be able to keep all these components exactly the same. They have no dependency on Redux. 79 | 80 | Let’s write them! We don’t need to think about binding to Redux yet. You can just give them fake data while you experiment until they render correctly. 81 | 82 | ## 木偶组件 83 | 84 | 这就是普通的 React 组件,所以就不在详述。直接看代码: 85 | 86 | #### `components/AddTodo.js` 87 | 88 | ```js 89 | import React, { findDOMNode, Component, PropTypes } from 'react'; 90 | 91 | export default class AddTodo extends Component { 92 | render() { 93 | return ( 94 |
    95 | 96 | 99 |
    100 | ); 101 | } 102 | 103 | handleClick(e) { 104 | const node = findDOMNode(this.refs.input); 105 | const text = node.value.trim(); 106 | this.props.onAddClick(text); 107 | node.value = ''; 108 | } 109 | } 110 | 111 | AddTodo.propTypes = { 112 | onAddClick: PropTypes.func.isRequired 113 | }; 114 | ``` 115 | 116 | #### `components/Todo.js` 117 | 118 | ```js 119 | import React, { Component, PropTypes } from 'react'; 120 | 121 | export default class Todo extends Component { 122 | render() { 123 | return ( 124 |
  • 130 | {this.props.text} 131 |
  • 132 | ); 133 | } 134 | } 135 | 136 | Todo.propTypes = { 137 | onClick: PropTypes.func.isRequired, 138 | text: PropTypes.string.isRequired, 139 | completed: PropTypes.bool.isRequired 140 | }; 141 | ``` 142 | 143 | #### `components/TodoList.js` 144 | 145 | ```js 146 | import React, { Component, PropTypes } from 'react'; 147 | import Todo from './Todo'; 148 | 149 | export default class TodoList extends Component { 150 | render() { 151 | return ( 152 |
      153 | {this.props.todos.map((todo, index) => 154 | this.props.onTodoClick(index)} /> 157 | )} 158 |
    159 | ); 160 | } 161 | } 162 | 163 | TodoList.propTypes = { 164 | onTodoClick: PropTypes.func.isRequired, 165 | todos: PropTypes.arrayOf(PropTypes.shape({ 166 | text: PropTypes.string.isRequired, 167 | completed: PropTypes.bool.isRequired 168 | }).isRequired).isRequired 169 | }; 170 | ``` 171 | 172 | #### `components/Footer.js` 173 | 174 | ```js 175 | import React, { Component, PropTypes } from 'react'; 176 | 177 | export default class Footer extends Component { 178 | renderFilter(filter, name) { 179 | if (filter === this.props.filter) { 180 | return name; 181 | } 182 | 183 | return ( 184 | { 185 | e.preventDefault(); 186 | this.props.onFilterChange(filter); 187 | }}> 188 | {name} 189 | 190 | ); 191 | } 192 | 193 | render() { 194 | return ( 195 |

    196 | Show: 197 | {' '} 198 | {this.renderFilter('SHOW_ALL', 'All')} 199 | {', '} 200 | {this.renderFilter('SHOW_COMPLETED', 'Completed')} 201 | {', '} 202 | {this.renderFilter('SHOW_ACTIVE', 'Active')} 203 | . 204 |

    205 | ); 206 | } 207 | } 208 | 209 | Footer.propTypes = { 210 | onFilterChange: PropTypes.func.isRequired, 211 | filter: PropTypes.oneOf([ 212 | 'SHOW_ALL', 213 | 'SHOW_COMPLETED', 214 | 'SHOW_ACTIVE' 215 | ]).isRequired 216 | }; 217 | ``` 218 | 219 | 就这些,现在开发一个木偶型的组件 `App` 把它们渲染出来,验证下是否工作。 220 | 221 | #### `containers/App.js` 222 | 223 | ```js 224 | import React, { Component } from 'react'; 225 | import AddTodo from '../components/AddTodo'; 226 | import TodoList from '../components/TodoList'; 227 | import Footer from '../components/Footer'; 228 | 229 | export default class App extends Component { 230 | render() { 231 | return ( 232 |
    233 | 235 | console.log('add todo', text) 236 | } /> 237 | 246 | console.log('todo clicked', todo) 247 | } /> 248 |
    251 | console.log('filter change', filter) 252 | } /> 253 |
    254 | ); 255 | } 256 | } 257 | ``` 258 | 259 | 渲染 `` 结果如下: 260 | 261 | 262 | 263 | 单独来看,并没有什么特别,现在把它和 Redux 连起来。 264 | 265 | ## 连接到 Redux 266 | 267 | We need to do two changes to connect our `App` component to Redux and make it dispatch actions and read state from the Redux store. 268 | 269 | First, we need to import `Provider` from [`react-redux`](http://github.com/gaearon/react-redux) we installed earlier, and **wrap the root component in ``** before rendering. 270 | 271 | #### `index.js` 272 | 273 | ```js 274 | import React from 'react'; 275 | import { createStore } from 'redux'; 276 | import { Provider } from 'react-redux'; 277 | import App from './containers/App'; 278 | import todoApp from './reducers'; 279 | 280 | let store = createStore(todoApp); 281 | 282 | let rootElement = document.getElementById('root'); 283 | React.render( 284 | // The child must be wrapped in a function 285 | // to work around an issue in React 0.13. 286 | 287 | {() => } 288 | , 289 | rootElement 290 | ); 291 | ``` 292 | 293 | This makes our store instance available to the components below. (Internally, this is done via React [undocumented “context” feature](http://www.youtube.com/watch?v=H7vlH-wntD4), but it’s not exposed directly in the API so don’t worry about it.) 294 | 295 | Then, we **wrap the components we want to connect to Redux with `connect()` function from [`react-redux`](http://github.com/gaearon/react-redux)**. Try to only do this for a top-level component, or route handlers. While technically you can `connect()` any component in your app to Redux store, avoid doing this too deeply because it will make the data flow harder to trace. 296 | 297 | **Any component wrapped with `connect()` call will receive a [`dispatch`](../api/Store.md#dispatch) function as a prop, and any state it needs from the global state.** The only argument to `connect()` is a function we call a **selector**. This function takes the global Redux store’s state, and returns the props you need for the component. In the simplest case, you can just return the `state` given to you, but you may also wish to transform it first. 298 | 299 | To make performant memoized transformations with composable selectors, check out [reselect](https://github.com/faassen/reselect). In this example, we won’t use it, but it works great for larger apps. 300 | 301 | #### `containers/App.js` 302 | 303 | ```js 304 | import React, { Component, PropTypes } from 'react'; 305 | import { connect } from 'react-redux'; 306 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'; 307 | import AddTodo from '../components/AddTodo'; 308 | import TodoList from '../components/TodoList'; 309 | import Footer from '../components/Footer'; 310 | 311 | class App extends Component { 312 | render() { 313 | // Injected by connect() call: 314 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 315 | return ( 316 |
    317 | 319 | dispatch(addTodo(text)) 320 | } /> 321 | 324 | dispatch(completeTodo(index)) 325 | } /> 326 |
    329 | dispatch(setVisibilityFilter(nextFilter)) 330 | } /> 331 |
    332 | ); 333 | } 334 | } 335 | 336 | App.propTypes = { 337 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 338 | text: PropTypes.string.isRequired, 339 | completed: PropTypes.bool.isRequired 340 | })), 341 | visibilityFilter: PropTypes.oneOf([ 342 | 'SHOW_ALL', 343 | 'SHOW_COMPLETED', 344 | 'SHOW_ACTIVE' 345 | ]).isRequired 346 | }; 347 | 348 | function selectTodos(todos, filter) { 349 | switch (filter) { 350 | case VisibilityFilters.SHOW_ALL: 351 | return todos; 352 | case VisibilityFilters.SHOW_COMPLETED: 353 | return todos.filter(todo => todo.completed); 354 | case VisibilityFilters.SHOW_ACTIVE: 355 | return todos.filter(todo => !todo.completed); 356 | } 357 | } 358 | 359 | // Which props do we want to inject, given the global state? 360 | // Note: use https://github.com/faassen/reselect for better performance. 361 | function select(state) { 362 | return { 363 | visibleTodos: selectTodos(state.todos, state.visibilityFilter), 364 | visibilityFilter: state.visibilityFilter 365 | }; 366 | } 367 | 368 | // Wrap the component to inject dispatch and state into it 369 | export default connect(select)(App); 370 | ``` 371 | 372 | 到此为止,迷你型的任务管理应用就开发完毕。 373 | 374 | ## 下一步 375 | 376 | 参照 [本示例完整](ExampleTodoList.md) 来深化理解。然后就可以跳到 [高级教程](../advanced/README.md) 学习网络请求处理和路由。 377 | -------------------------------------------------------------------------------- /docs/introduction/Ecosystem.md: -------------------------------------------------------------------------------- 1 | # 生态 2 | 3 | Redux 是一个体小精悍的库,但它相关的内容和 API 都是精挑细选的,为的是衍生出丰富的工具集和可扩展的生态系统。 4 | 5 | 如果需要关于 Redux 所有内容的列表,推荐移步到 [Awesome Redux](https://github.com/xgrommx/awesome-redux)。它包含了示例、样板代码、中间件、工具库还有很多其它相关内容。 6 | 7 | 本页内容将只列出其中 Redux 维护者亲自维护的一部分内容。不要因此而打消你尝试其它工具的信心!整个生态发展得太快,我们没有足够的时间去关注所有内容。建议只把这里的当作“内部推荐”,如果你使用 Redux 创建了很酷的内容,不要犹豫,马上发个 PR 吧。 8 | 9 | ## 不同框架绑定 10 | 11 | * [react-redux](https://github.com/gaearon/react-redux) — React 12 | * [ng-redux](https://github.com/wbuchwalter/ng-redux) — Angular 13 | 14 | ## 中间件 15 | 16 | * [redux-thunk](http://github.com/gaearon/redux-thunk) — 用最简单的方式写异步 action 构造器 17 | * [redux-promise](https://github.com/acdlite/redux-promise) — 遵从[FSA](https://github.com/acdlite/flux-standard-action) 的 promise 中间件 18 | * [redux-rx](https://github.com/acdlite/redux-rx) — 给 Redux 用的 RxJS 工具,包括观察者的中件间 19 | * [redux-batched-updates](https://github.com/acdlite/redux-batched-updates) — 以 React 分发器的形式分批更新 React 20 | * [redux-logger](https://github.com/fcomb/redux-logger) — 记录所有 Redux action 和下一次 state 的日志 21 | 22 | ## 工具集 23 | 24 | * [reselect](https://github.com/faassen/reselect) — 有效地派生数据选择器,这从NuclearJS 产生的想法 25 | * [normalizr](https://github.com/gaearon/normalizr) — 标准化内嵌 API 的响应,为了通过 reducers 更方便地作处理 26 | * [redux-actions](https://github.com/acdlite/redux-actions) — 在写 reducers and action 构造器的初始化 27 | * [redux-transducers](https://github.com/acdlite/redux-transducers) — Redux 的编译器工具 28 | 29 | ## 开发者工具 30 | 31 | * [redux-devtools](http://github.com/gaearon/redux-devtools) — 一个像是时间旅行似的 action 日志工具,包括热更新和 reducers 的错误处理器器 [最早演示于 React Europe](https://www.youtube.com/watch?v=xsSnOQynTHs) 32 | 33 | ## 教程与文章 34 | 35 | * [redux-tutorial](https://github.com/happypoulp/redux-tutorial) - 学习如何一步步使用 redux 36 | * [What the Flux?! Let’s Redux.](https://blog.andyet.com/2015/08/06/what-the-flux-lets-redux) 37 | * [Handcrafting an Isomorphic Redux Application (With Love)](https://medium.com/@bananaoomarang/handcrafting-an-isomorphic-redux-application-with-love-40ada4468af4) 38 | 39 | ## 更多 40 | 41 | [Awesome Redux](https://github.com/xgrommx/awesome-redux) 是一个展示丰富 Redux 相关信息的列表。 42 | -------------------------------------------------------------------------------- /docs/introduction/Examples.md: -------------------------------------------------------------------------------- 1 | # 示例 2 | 3 | Redux [源码](https://github.com/gaearon/redux/tree/master/examples)里包含一些示例一起发行。 4 | **运行示例的方法是,先 clone 仓库,然后分别在根目录和示例目录下执行 `npm install`。** 5 | 6 | >##### 复制代码时注意 7 | >如果你把 Redux 示例代码复制到其它目录,删除 `webpack.config.js` 里的这几行: 8 | > 9 | >```js 10 | >alias: { 11 | > 'redux': path.join(__dirname, '..', '..', 'src') 12 | >}, 13 | >``` 14 | >还有 15 | >```js 16 | >{ 17 | > test: /\.js$/, 18 | > loaders: ['babel'], 19 | > include: path.join(__dirname, '..', '..', 'src') 20 | >}, 21 | ``` 22 | > 23 | > 否则运行时扔会试图从相对的 `src` 目录来定位 Redux,导致构建失败。 24 | 25 | ## Counter 计数器示例 26 | 27 | 运行 [Counter](https://github.com/gaearon/redux/tree/master/examples/counter) 示例: 28 | 29 | ``` 30 | git clone https://github.com/gaearon/redux.git 31 | 32 | cd redux 33 | npm install 34 | 35 | cd examples/counter 36 | npm install 37 | 38 | npm start 39 | open http://localhost:3000/ 40 | ``` 41 | 42 | 这个示例包含: 43 | 44 | * 基本的 Redux 应用开发流程; 45 | * 测试代码。 46 | 47 | ## TodoMVC 示例 48 | 49 | 运行 [TodoMVC](https://github.com/gaearon/redux/tree/master/examples/todomvc) 示例: 50 | 51 | ``` 52 | git clone https://github.com/gaearon/redux.git 53 | 54 | cd redux 55 | npm install 56 | 57 | cd examples/todomvc 58 | npm install 59 | 60 | npm start 61 | open http://localhost:3000/ 62 | ``` 63 | 64 | 这个示例包含: 65 | 66 | * Redux 中使用两个 reducer 的方法; 67 | * 嵌套数据的更新; 68 | * 测试样例。 69 | 70 | ## 异步 71 | 72 | 运行 [Async](https://github.com/gaearon/redux/tree/master/examples/async) 示例: 73 | 74 | ``` 75 | git clone https://github.com/gaearon/redux.git 76 | 77 | cd redux 78 | npm install 79 | 80 | cd examples/async 81 | npm install 82 | 83 | npm start 84 | open http://localhost:3000/ 85 | ``` 86 | 87 | 这个示例包含: 88 | 89 | * 使用 [redux-thunk](https://github.com/gaearon/redux-thunk) 处理简单的异步开发流程; 90 | * 缓存服务器响应数据和在获取数据过程中显示加载进度条; 91 | * 缓存数据过期方法。 92 | 93 | ## Real World 示例 94 | 95 | 运行 [Real World](https://github.com/gaearon/redux/tree/master/examples/real-world) 示例: 96 | 97 | ``` 98 | git clone https://github.com/gaearon/redux.git 99 | 100 | cd redux 101 | npm install 102 | 103 | cd examples/real-world 104 | npm install 105 | 106 | npm start 107 | open http://localhost:3000/ 108 | ``` 109 | 110 | 这个示例包含: 111 | 112 | * 实际应用中如何做异步处理; 113 | * 使用 [normalized](https://github.com/gaearon/normalizr) 结果集来保存并缓存数据; 114 | * 自定义 middleware 来做 API 请求; 115 | * 缓存服务器响应数据和在获取数据过程中显示加载进度条; 116 | * 分页; 117 | * 路由。 118 | 119 | ## 更多 120 | 121 | 参考 [Awesome Redux](https://github.com/xgrommx/awesome-redux) 获取更多示例。 122 | -------------------------------------------------------------------------------- /docs/introduction/Motivation.md: -------------------------------------------------------------------------------- 1 | # 动机 2 | 3 | 随着 JavaScript 单页应用开发日趋复杂,**JavaScript 需要管理比任何时候都多的state (状态)**。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载中动效或者分页器等等。 4 | 5 | 管理不断变化的 state 非常难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起除对应 model 外另一个 model 的变化,依次地,可能会引起另一个 view 的变化... 直至你搞不清楚到底发生了什么。**state 在什么时候,为什么,如何变化已然不受控制。** 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。 6 | 7 | 如果这还不够糟糕,考虑一些**来自前端开发领域的新需求**,如更新调优、服务端渲染、路由跳转前请求数据等等。我们前端开发者正在经受前所未有的复杂性,[我们是时候该放弃了吗?](http://www.quirksmode.org/blog/archives/2015/07/stop_pushing_th.html) 8 | 9 | 这里的复杂性很大程度上来自于:**困扰着我们人类的两大难以理解的概念:变化和异步**。 我称它们为[Mentos and Coke](https://en.wikipedia.org/wiki/Diet_Coke_and_Mentos_eruption)。如果把二者都分开,能做的很好,但混到一起,就变得一团糟。一些库如 [React](http://facebook.github.io/react) 试图在视图层禁止异步和直接 DOM 操作来解决这个问题。美中不足的是,React 把处理 state 里数据的问题又留给了你自己。 10 | 11 | 跟随 [Flux](http://facebook.github.io/flux)、[CQRS](http://martinfowler.com/bliki/CQRS.html) 和 [Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html) 的脚步,通过限制更新发生的时间和方式,**Redux 试图让 state 的变化变得可预测**,而这些限制条件反映在 Redux 的 [三大原则](ThreePrinciples.md)中。 12 | -------------------------------------------------------------------------------- /docs/introduction/PriorArt.md: -------------------------------------------------------------------------------- 1 | # 先前技术 2 | 3 | Redux 是一个混合的产物。它和一些设计模式和技术相似,但它也有与众不同之处。我们现在来探索它的一些相似与不同。 4 | 5 | ### Flux 6 | 7 | Redux 可以被考虑是一种 [Flux](https://facebook.github.io/flux/) 实现吗? 8 | [是](https://twitter.com/fisherwebdev/status/616278911886884864),或者说 [不是](https://twitter.com/andrestaltz/status/616270755605708800)。 9 | 10 | (不用担心,[Flux 作者](https://twitter.com/jingc/status/616608251463909376) [赞成它](https://twitter.com/fisherwebdev/status/616286955693682688),如果所有人都想知道结果。) 11 | 12 | Redux 是从很多有质量的 Flux 实现中产生的灵感。像 Flux 一样,Redux 规定集中你的 model 去更新应用的核心层里的逻辑(Flux 里的 store,Redux 里的 reducers)。替代应用代码直接改变数据,同时告诉你,描述每一个改变对象的动作叫 "action"。 13 | 14 | 不像 Flux 的是,**Redux 没有分发器的概念**。这是因为它依赖于纯函数来替代事件处理器。纯函数可以很方便的构建,而且不需要额外的实体来管理它们。取决于怎么样看 Flux,你可能看得到实现差异或实现细节。Flux 经常[被描述成 `(state, action) => state`](https://speakerdeck.com/jmorrell/jsconf-uy-flux-those-who-forget-the-past-dot-dot-dot)。从这个意义上说,Redux 是 Flux 架构的正确实现,这得感谢一下纯函数让它更简单。 15 | 16 | 和 Flux 另一个重要的区别是 **Redux 假设你永远不会改变你的数据**。你可以用纯对象和数组来管理好,在 reducers 里改变会深感挫折。你可以返回一个新对象, 它可以很容易的使用 [ES7 所提议的对象 spread 语法](https://github.com/sebmarkbage/ecmascript-rest-spread) 和 [Babel](http://babeljs.io),或使用一个库像 [Immutable](https://facebook.github.io/immutable-js) 来实现。 17 | 18 | 虽然技术上*可能* [写不纯的 reducers](https://github.com/gaearon/redux/issues/328#issuecomment-125035516) 来改变数据为了性能方面,但我们不鼓励你这么做。像开发时间旅行、记录/回放或热加载特性将会被不可实现。此外,在大部分应用,不像不可变特性会影响性能问题,因为,像 [Om](https://github.com/omcljs/om) 里的示范,即时是对象分配失败,仍然可以防止昂贵的重渲染和重计算,因为你知道什么改变了,这得感谢 reducer 的纯度。 19 | 20 | ### Elm 21 | 22 | [Elm](http://elm-lang.org/) 是一个函数式编程语言,创造于 [Evan Czaplicki](https://twitter.com/czaplic)。它受到了 [能描述成`(state, action) => state`的架构](http://elm-lang.org/guide/architecture) 激励。 从技术上说,Elm 的 "updaters" 等同于 Redux 里的 reducer。 23 | 24 | 不像 Redux,Elm 是一门语言,所以对于 actions 和模式匹配,它有着来自静态类型的优势。即使你没有计划使用 Elm,你也可以读关于 Elm 的架构,然后尝试一下。这将是十分有趣的,可以 [从 JavaScript 库产生的类似想法](https://github.com/paldepind/noname-functional-frontend-framework)。我们将看到 Redux 从中取得的灵感! 其中我们 [使用一个平缓的类型解决方案,像 Flow](https://github.com/gaearon/redux/issues/290) 来接近 Elm 的静态类型。 25 | 26 | ### Immutable 27 | 28 | [Immutable](https://facebook.github.io/immutable-js) 是一个 JavaScript 库来实现不可变数据结构。它表现得十分高性能,并有着惯用的 JavaScript API。 29 | 30 | Immutable 和相似的库对于 Redux 是相交的。我们一起使用它们非常容易! 31 | 32 | **Redux 不关心你*怎么样*存储 state,它可能是纯对象,是不可变对象,或是其它类型。** 你将来可能想要序列化或反序列化技巧,为了在 server 端写同构应用或融合它们的 state,但此外,你也可以使用数据存储库 *尽可能让它支持不可变特性*。举个例子,对于 Redux state 来说,Backbone 没有任何意义,因为 Backbone models 是可变的。 33 | 34 | 记录到这,即使不可变库支持 cursors,你也不应该在 Redux 应用里使用。整个 state 树应该考虑只读,你应该使用 Redux 来更新 state 和订阅更新。因此通过 cursor 来写在 Redux 里没有任何意义。**如果只是 cursor 的用例是去耦的,对于从 UI 树和逐渐改善的 cursor 和产生的 state 树,你应该使用 selector 来替代它。** Selectors 是组合 getter 的方法组。参考 [reselect](http://github.com/faassen/reselect),它是一个真正优秀和简洁的组合 selector 的现实。 35 | 36 | ### Baobab 37 | 38 | [Baobab](https://github.com/Yomguithereal/baobab) 是另一个流行的实现了不可变 API 并使用纯 JavaScript 对象的库。当你在 Redux 里使用它,它们在一起会有着一定的优势。 39 | 40 | 大部分函数式 Baobab provides 是通过 cursors 来更新数据的,但是 Redux 施行只能通过分发一个 action 来更新数据。因此,它们通过不同的实现,解决了同样的问题。 41 | 42 | 不像 Immtable,Baobab 在引擎下还不能现实任何特别有效的数据结构,所以你在和 Redux 一起的时候没有真正的益处。它只是简单的使用了纯对象在案例里。 43 | 44 | ### Rx 45 | 46 | [Reactive 扩展](https://github.com/Reactive-Extensions/RxJS) (和它们接受的 [现代化的重写](https://github.com/ReactiveX/RxJS)) 是管理复杂异步应用非常优秀的方案。事实上 [他们是构建人类计算机可观察到的,并有效的处理相互依赖的库](http://cycle.js.org). 47 | 48 | 它和 Redux 在一起使用有意义么? 当然! 他们可以极好的在一起。举个例子,这是一个简单地通过可观察变量来展示 Redx store 的例子: 49 | 50 | ```js 51 | function toObservable(store) { 52 | return { 53 | subscribe({ onNext }) { 54 | let dispose = store.subscribe(() => onNext(store.getState())); 55 | onNext(store.getState()); 56 | return { dispose }; 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | 相似地,你可以组成不同的异步流,在提供给他们 `store.dispatch()` 之前,转换他们到 action 中。 63 | 64 | 问题是: 你真的会在已经使用了 Rx 的同时需要 Redux 吗? 可能不会。[在 Rx 里重新现实 Redux](https://github.com/jas-chen/rx-redux) 并不难。有些人说这是使用 `.scan()` 方法的一两句实现。这可能不错! 65 | 66 | 如果你还在怀疑,拉下 Redux 的源代码 (这里不会多说什么),它是也生态系统 (举个例子, [开发者工具](https://github.com/gaearon/redux-devtools)). 如果你不是很不关心这些,一定想要交互数据流,你可能是要探索像 [Cycle](http://cycle.js.org),或者说把它合并到 Redux 中。让我们知道怎么才能做到它吧! 67 | -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | * [动机](Motivation.md) 4 | * [三大原则](ThreePrinciples.md) 5 | * [先前技术](PriorArt.md) 6 | * [生态](Ecosystem.md) 7 | * [示例](Examples.md) 8 | -------------------------------------------------------------------------------- /docs/introduction/ThreePrinciples.md: -------------------------------------------------------------------------------- 1 | # 三大原则 2 | 3 | Redux 可以被描述成三大基础原则: 4 | 5 | ### 单一数据源 6 | 7 | **整个应用的 [state](../Glossary.md#state) 被储存在一棵对象树中,它只有一个单一的 [store](../Glossary.md#store) 。** 8 | 9 | 这让同构应用开发变得非常容易。来自服务端的 state 可以轻而易举地被序列化并融合到没有额外代码影响的客户端上。由于是单一的 state 树,调试也变得非常容易。你也可以把应用的 state 保存下来加快开发速度。此外,受益于单一的 state 树,以前难以实现的像“撤销/重做”这类的功能也变得轻而易举。 10 | 11 | ```js 12 | console.log(store.getState()); 13 | 14 | { 15 | visibilityFilter: 'SHOW_ALL', 16 | todos: [{ 17 | text: 'Consider using Redux', 18 | completed: true, 19 | }, { 20 | text: 'Keep all state in a single tree', 21 | completed: false 22 | }] 23 | } 24 | ``` 25 | 26 | ### State 是只读的 27 | 28 | **惟一改变 state 的办法就是触发 [action](../Glossary.md#action),action 是一个描述要发生什么的对象。** 29 | 30 | 这让视图和网络请求不能直接修改 state,相反只能表达出需要修改的意图。因为所有的修改都被集中化处理,且严格按照顺序一个接一个执行,因此没有模棱两可的情况需要提防。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。 31 | 32 | ```js 33 | store.dispatch({ 34 | type: 'COMPLETE_TODO', 35 | index: 1 36 | }); 37 | 38 | store.dispatch({ 39 | type: 'SET_VISIBILITY_FILTER', 40 | filter: 'SHOW_COMPLETED' 41 | }); 42 | ``` 43 | 44 | ### 纯函数的形式来执行修改 45 | 46 | **为了描述 action 如何改变 state 树,你需要编写 [reducers](../Glossary.md#reducer)。** 47 | 48 | Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state 树的不同部分,因为 reducer 只是普通函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来做一些通用任务,如分页器。 49 | 50 | ```js 51 | function visibilityFilter(state = 'SHOW_ALL', action) { 52 | switch (action.type) { 53 | case 'SET_VISIBILITY_FILTER': 54 | return action.filter; 55 | default: 56 | return state; 57 | } 58 | } 59 | 60 | function todos(state = [], action) { 61 | switch (action.type) { 62 | case 'ADD_TODO': 63 | return [...state, { 64 | text: action.text, 65 | completed: false 66 | }]; 67 | case 'COMPLETE_TODO': 68 | return [ 69 | ...state.slice(0, action.index), 70 | Object.assign({}, state[action.index], { 71 | completed: true 72 | }), 73 | ...state.slice(action.index + 1) 74 | ] 75 | default: 76 | return state; 77 | } 78 | } 79 | 80 | import { combineReducers, createStore } from 'redux'; 81 | let reducer = combineReducers({ visibilityFilter, todos }); 82 | let store = createStore(reducer); 83 | ``` 84 | 85 | 就是这样,现在你已经明白 Redux 是怎么回事了。 86 | -------------------------------------------------------------------------------- /docs/recipes/ComputingDerivedData.md: -------------------------------------------------------------------------------- 1 | # 计算衍生数据 2 | 3 | [Reselect](https://github.com/faassen/reselect.git) 是用来创建可记忆的(Memoized)、可组合的 **selector** 函数。Reselect selectors 可以用来高效地计算 Redux store 里的衍生数据。 4 | 5 | ### 可记忆的 Selectors 初衷 6 | 7 | 首先访问 [Todos 列表示例](../basics/UsageWithReact.md): 8 | 9 | #### `containers/App.js` 10 | 11 | ```js 12 | import React, { Component, PropTypes } from 'react'; 13 | import { connect } from 'react-redux'; 14 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'; 15 | import AddTodo from '../components/AddTodo'; 16 | import TodoList from '../components/TodoList'; 17 | import Footer from '../components/Footer'; 18 | 19 | class App extends Component { 20 | render() { 21 | // 通过 connect() 注入: 22 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 23 | return ( 24 |
    25 | 27 | dispatch(addTodo(text)) 28 | } /> 29 | 32 | dispatch(completeTodo(index)) 33 | } /> 34 |
    37 | dispatch(setVisibilityFilter(nextFilter)) 38 | } /> 39 |
    40 | ); 41 | } 42 | } 43 | 44 | App.propTypes = { 45 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 46 | text: PropTypes.string.isRequired, 47 | completed: PropTypes.bool.isRequired 48 | })), 49 | visibilityFilter: PropTypes.oneOf([ 50 | 'SHOW_ALL', 51 | 'SHOW_COMPLETED', 52 | 'SHOW_ACTIVE' 53 | ]).isRequired 54 | }; 55 | 56 | function selectTodos(todos, filter) { 57 | switch (filter) { 58 | case VisibilityFilters.SHOW_ALL: 59 | return todos; 60 | case VisibilityFilters.SHOW_COMPLETED: 61 | return todos.filter(todo => todo.completed); 62 | case VisibilityFilters.SHOW_ACTIVE: 63 | return todos.filter(todo => !todo.completed); 64 | } 65 | } 66 | 67 | function select(state) { 68 | return { 69 | visibleTodos: selectTodos(state.todos, state.visibilityFilter), 70 | visibilityFilter: state.visibilityFilter 71 | }; 72 | } 73 | 74 | // 把组件包起来,以此来注入 dispatch 和 state 75 | export default connect(select)(App); 76 | ``` 77 | 78 | 上面的示例中,`select` 调用了 `selectTodos` 来计算 `visibleTodos`。运行没问题,但有一个缺点:每当组件更新时都会计算 `visibleTodos`。如果 state tree 非常大,或者计算量非常大,每次更新都重新计算可能会带来性能问题。Reselect 能帮你省去这些没必要的重新计算。 79 | 80 | ### 创建可记忆的 Selector 81 | 82 | 我们需要一个可记忆的 selector 来替代这个 `select`,只在 `state.todos` or `state.visibilityFilter` 变化时重新计算 `visibleTodos`,而在其它部分(非相关)变化时不做计算。 83 | 84 | Reselect 提供 `createSelector` 函数来创建可记忆的 selector。`createSelector` 接收一个 input-selectors 数组和一个转换函数作为参数。如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。如果 input-selectors 的值的前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。 85 | 86 | 让我们定义一个可记忆的 selector `visibleTodosSelector` 来替代 `select`: 87 | 88 | #### `selectors/TodoSelectors.js` 89 | 90 | ```js 91 | import { createSelector } from 'reselect'; 92 | import { VisibilityFilters } from './actions'; 93 | 94 | function selectTodos(todos, filter) { 95 | switch (filter) { 96 | case VisibilityFilters.SHOW_ALL: 97 | return todos; 98 | case VisibilityFilters.SHOW_COMPLETED: 99 | return todos.filter(todo => todo.completed); 100 | case VisibilityFilters.SHOW_ACTIVE: 101 | return todos.filter(todo => !todo.completed); 102 | } 103 | } 104 | 105 | const visibilityFilterSelector = (state) => state.visibilityFilter; 106 | const todosSelector = (state) => state.todos; 107 | 108 | export const visibleTodosSelector = createSelector( 109 | [visibilityFilterSelector, todosSelector], 110 | (visibilityFilter, todos) => { 111 | return { 112 | visibleTodos: selectTodos(todos, visibilityFilter), 113 | visibilityFilter 114 | }; 115 | } 116 | ); 117 | ``` 118 | 119 | 在上例中,`visibilityFilterSelector` 和 `todosSelector` 是 input-selector。因为他们并不转换数据,所以被创建成普通的非记忆的 selector 函数。但是,`visibleTodosSelector` 是一个可记忆的 selector。他接收 `visibilityFilterSelector` 和 `todosSelector` 为 input-selector,还有一个转换函数来计算过滤的 todos 列表。 120 | 121 | ### 组合 Selector 122 | 123 | 可记忆的 selector 自身可以作为其它可记忆的 selector 的 input-selector。下面的 `visibleTodosSelector` 被当作另一个 selector 的 input-selector,来进一步通过关键字(keyword)过滤 todos。 124 | 125 | ```js 126 | const keywordSelector = (state) => state.keyword; 127 | 128 | const keywordFilterSelector = createSelector( 129 | [visibleTodosSelector, keywordSelector], 130 | (visibleTodos, keyword) => visibleTodos.filter( 131 | todo => todo.indexOf(keyword) > -1 132 | ) 133 | ); 134 | ``` 135 | 136 | ### 连接 Selector 和 Redux Store 137 | 138 | 如果你在使用 react-redux,你可以使用 connect 来连接可忘记的 selector 和 Redux store。 139 | 140 | #### `containers/App.js` 141 | 142 | ```js 143 | import React, { Component, PropTypes } from 'react'; 144 | import { connect } from 'react-redux'; 145 | import { addTodo, completeTodo, setVisibilityFilter } from '../actions'; 146 | import AddTodo from '../components/AddTodo'; 147 | import TodoList from '../components/TodoList'; 148 | import Footer from '../components/Footer'; 149 | import { visibleTodosSelector } from '../selectors/todoSelectors.js'; 150 | 151 | class App extends Component { 152 | render() { 153 | // 通过 connect() 注入: 154 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 155 | return ( 156 |
    157 | 159 | dispatch(addTodo(text)) 160 | } /> 161 | 164 | dispatch(completeTodo(index)) 165 | } /> 166 |
    169 | dispatch(setVisibilityFilter(nextFilter)) 170 | } /> 171 |
    172 | ); 173 | } 174 | } 175 | 176 | App.propTypes = { 177 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 178 | text: PropTypes.string.isRequired, 179 | completed: PropTypes.bool.isRequired 180 | })), 181 | visibilityFilter: PropTypes.oneOf([ 182 | 'SHOW_ALL', 183 | 'SHOW_COMPLETED', 184 | 'SHOW_ACTIVE' 185 | ]).isRequired 186 | }; 187 | 188 | // 把 selector 传递给连接的组件 189 | export default connect(visibleTodosSelector)(App); 190 | ``` 191 | -------------------------------------------------------------------------------- /docs/recipes/MigratingToRedux.md: -------------------------------------------------------------------------------- 1 | # 迁移到 Redux 2 | 3 | Redux 不是一个整体框架,但是也设置了一些约定并[提供了一些函数](../api/README.md)来让这些工作。你的 Redux 项目中的主要代码都不是在使用 Redux 的 API,大多数时间你会在写一些函数。 4 | 5 | 这让把旧的项目迁移到 Redux 上来变得非常容易。我们不想限制你! 6 | 7 | ## 迁移 Flux 项目 8 | 9 | [Reducers](../Glossary.md#reducer) 提取了 Flux Stores 的本质,所以这让逐步迁移一个 Flux 项目到 Redux 上面来变成了可能,无论你使用了 [Flummox](http://github.com/acdlite/flummox)、[Alt](http://github.com/goatslacker/alt)、[traditional Flux](https://github.com/facebook/flux) 还是其他 Flux 库。 10 | 11 | 同样你也可以将 Redux 的项目通过同样的操作改回上述的这些 Flux 框架。 12 | 13 | 你需要做的包含下面几个步骤: 14 | 15 | * 建立一个叫做 `createFluxStore(reducer)` 的函数,来通过 reducer 函数适配你当前项目的 Flux Store。从代码来看,这个函数很像 Redux 中的 [`createStore`](../api/createStore.md) 的实现。它的 dispatch 处理器 应该根据不同的 action 来调用不同的 `reducer`,从该改变应用的状态。 16 | 17 | 18 | * 通过使用 `createFluxStore(reducer)`,可以让你逐步将你的 Flux Store 重写为 Reducer,逐步回归改动,避免对应用产生影响。 19 | 20 | * 当你重写了你的 Stores 后,你会发现你应该避免一些明显不对的 Flux 使用方法,例如在 Store 中请求 API、在 Store 中触发 actions。你的 Flux 代码会更容易掌握一旦你基于 Reducers 来构建它。 21 | 22 | * 当你所有的 Flux Stores 全部基于 reducers 来实现时,这时你可以利用 [`combineReducers(reducers)`](../api/combineReducers.md) 将多个 reducers 合并成一个,然后在应用里只使用一个 Store。 23 | 24 | * 现在只剩下[使用 react-redux](../basics/UsageWithReact.md) 或者类似的库来处理你的UI部分。 25 | 26 | * 最后,你或许想使用一些 Redux 的特性,例如利用 middleware 来进一步简化异步的代码。 27 | 28 | 29 | ## 迁移 Backbone 项目 30 | 31 | 对不起,你需要重写你的 Model 层。 32 | 它们区别太大了。 33 | -------------------------------------------------------------------------------- /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # 技巧 2 | 3 | 这一章是关于实现应用开发中会遇到的一些典型场景和代码片段。本章内容建立在你已经学会[基础章节]和[高级章节]的基础上。 4 | 5 | * [迁移到 Redux](MigratingToRedux.md) 6 | * [减少样板代码](ReducingBoilerplate.md) 7 | * [服务端渲染](ServerRendering.md) 8 | * [编写测试](WritingTests.md) 9 | * [Computing Derived Data](ComputingDerivedData.md) 10 | -------------------------------------------------------------------------------- /docs/recipes/ReducingBoilerplate.md: -------------------------------------------------------------------------------- 1 | # 减少样板代码 2 | 3 | Redux 很大部分 [受到 Flux 的启发](../introduction/PriorArt.md),并且最常见的关于 Flux 抱怨是它如何使得你写了一大堆的模板。在这个技巧中,我们将考虑 Redux 如何使得我们选择我们的代码会变得怎样繁复,取决于个人样式,团队选项,长期可维护等等。 4 | 5 | ## Actions 6 | 7 | Actions 是描述了在 app 中所发生的,以单独方式描述对象变异意图的服务的一个普通对象。很重要的一点是 **你必须分发的 action 对象并不是一个模板,而是 Redux 的一个[基本设计选项](../introduction/ThreePrinciples.md)**. 8 | 9 | 有些框架生成自己和 Flux 很像,不过缺少了 action 对象的概念。为了变得可预测,这是一个从 Flux or Redux 的倒退。如果没有可串行的普通对象 action,便无法记录或重放用户会话,或者无法实现 [带有时间旅行的热重载](https://www.youtube.com/watch?v=xsSnOQynTHs)。如果你更喜欢直接修改数据,那么你并不需要 Redux 。 10 | 11 | Action 一般长这样: 12 | 13 | ```js 14 | { type: 'ADD_TODO', text: 'Use Redux' } 15 | { type: 'REMOVE_TODO', id: 42 } 16 | { type: 'LOAD_ARTICLE', response: { ... } } 17 | ``` 18 | 19 | 一个约定俗成的是 actions 拥有一个定值 type 帮助 reducer (或 Flux 中的 Stores ) 识别它们。我们建议的你使用 string 而不是 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作为 action type ,因为 string 是可串行的,而使用 Symbols 的话你会把记录和重演变得比所需要的更难。 20 | 21 | 在 Flux 中,传统上认为你将每个 action type 定义为string定值: 22 | 23 | ```js 24 | const ADD_TODO = 'ADD_TODO'; 25 | const REMOVE_TODO = 'REMOVE_TODO'; 26 | const LOAD_ARTICLE = 'LOAD_ARTICLE'; 27 | ``` 28 | 29 | 这么做的优势?**人们通常声称定值不是必要的,对于小的项目可能是正确的。** 对于大的项目,将action types定义为定值有如下好处: 30 | 31 | * 帮助维护命名一致性,因为所有的 action type 汇总在同一位置。 32 | * 有的时候,在开发一个新功能之前你想看到所有现存的 actions 。可能的情况是你的团队里已经有人添加了你所需要的action,而你并不知道。 33 | * Action types 列表在Pull Request中能查到所有添加,删除,修改的记录。这能帮助团队中的所有人及时追踪新功能的范围与实现。 34 | * 如果你在导入一个 Action 定值的时候拼写错误,你会得到 `undefined` 。当你纳闷 action 被分发出去而什么也没发生的时候,一个拼写错误更容易被发现。 35 | 36 | 你的项目的约定取决与你自己。你开始的时候可能用的是inline string,之后转为定值,也许之后将他们归为一个独立文件。Redux 不会给予任何建议,选择你自己最喜欢的。 37 | 38 | ## Action Creators 39 | 40 | 另一个约定是,你创建生成 action 对象的函数,而不是在你分发的时候内联生成它们。 41 | 42 | 例如,用文字对象取代调用 `dispatch` : 43 | 44 | ```js 45 | // somewhere in an event handler 46 | dispatch({ 47 | type: 'ADD_TODO', 48 | text: 'Use Redux' 49 | }); 50 | ``` 51 | 52 | 你可以在单独的文件中写一个 action creator ,然后从 component 里导入: 53 | 54 | #### `actionCreators.js` 55 | 56 | ```js 57 | export function addTodo(text) { 58 | return { 59 | type: 'ADD_TODO', 60 | text 61 | }; 62 | } 63 | ``` 64 | 65 | #### `AddTodo.js` 66 | 67 | ```js 68 | import { addTodo } from './actionCreators'; 69 | 70 | // event handler 里的某处 71 | dispatch(addTodo('Use Redux')) 72 | ``` 73 | 74 | Action creators 总被当作模板受到批评。好吧,其实你并不用把他们写出来!**如果你觉得更适合你的项目你可以选用对象文字** 然而,你应该知道写 action creators 是存在某种优势的。 75 | 76 | 假设有个设计师看完我们的原型之后回来说,我们需要允许三个 todo 不能再多了。我们可以使用 [redux-thunk](https://github.com/gaearon/redux-thunk) 中间件添加一个提前退出,把我们的 action creator 重写成回调形式: 77 | 78 | ```js 79 | function addTodoWithoutCheck(text) { 80 | return { 81 | type: 'ADD_TODO', 82 | text 83 | }; 84 | } 85 | 86 | export function addTodo(text) { 87 | // Redux Thunk 中间件允许这种形式 88 | // 在下面的 “异步 Action Creators” 段落中有写 89 | return function (dispatch, getState) { 90 | if (getState().todos.length === 3) { 91 | // 提前退出 92 | return; 93 | } 94 | 95 | dispatch(addTodoWithoutCheck(text)); 96 | } 97 | } 98 | ``` 99 | 100 | 我们刚修改了 `addTodo` action creator 的行为,对调用它的代码完全不可见。**我们不用担心去看每个添加 todo 的地方保证他们有了这个检查** Action creator 让你可以解耦额外的分发 action 逻辑与实际的 components 发送这些 actions,而且当你在重开发经常要改变需求的时候也会非常有用。 101 | 102 | ### 生成 Action Creators 103 | 104 | 某些框架如 [Flummox](https://github.com/acdlite/flummox) 自动从 action creator 函数定义生成 action type 定值。这个想法是说你不需要 `ADD_TODO` 定值和 `addTodo()` action creator两个都自己定义。这样的方法在底层也生成 action type 定值,但他们是隐式生成的,也就是间接级。 105 | 106 | 我们不建议用这样的方法。如果你写像这样简单的 action creator 写烦了: 107 | 108 | ```js 109 | export function addTodo(text) { 110 | return { 111 | type: 'ADD_TODO', 112 | text 113 | }; 114 | } 115 | 116 | export function removeTodo(id) { 117 | return { 118 | type: 'REMOVE_TODO', 119 | id 120 | }; 121 | } 122 | ``` 123 | 124 | 你可以写一个生成 action creator 的函数: 125 | 126 | ```js 127 | function makeActionCreator(type, ...argNames) { 128 | return function(...args) { 129 | let action = { type }; 130 | argNames.forEach((arg, index) => { 131 | action[argNames[index]] = args[index]; 132 | }); 133 | return action; 134 | } 135 | } 136 | 137 | export const addTodo = makeActionCreator('ADD_TODO', 'todo'); 138 | export const removeTodo = makeActionCreator('REMOVE_TODO', 'id'); 139 | ``` 140 | 141 | 参见 [redux-action-utils](https://github.com/insin/redux-action-utils) 和 [redux-actions](https://github.com/acdlite/redux-actions) 获得更多介绍这样的常用工具。 142 | 143 | 注意这样的工具给你的代码添加了魔法。 144 | 魔法和间接声明真的值得多写一两行代码么? 145 | 146 | ## 异步 Action Creators 147 | 148 | [中间件](../Glossary.html#middleware) 让你注入一个定制逻辑,可以在每个 action 对象分发出去之前解释。异步 actions 是中间件的最常见用例。 149 | 150 | 没有中间件的话,[`dispatch`](../api/Store.md#dispatch) 只能接收一个普通对象。所以我们在 components 里面进行 AJAX 调用: 151 | 152 | #### `actionCreators.js` 153 | 154 | ```js 155 | export function loadPostsSuccess(userId, response) { 156 | return { 157 | type: 'LOAD_POSTS_SUCCESS', 158 | userId, 159 | response 160 | }; 161 | } 162 | 163 | export function loadPostsFailure(userId, error) { 164 | return { 165 | type: 'LOAD_POSTS_FAILURE', 166 | userId, 167 | error 168 | }; 169 | } 170 | 171 | export function loadPostsRequest(userId) { 172 | return { 173 | type: 'LOAD_POSTS_REQUEST', 174 | userId 175 | }; 176 | } 177 | ``` 178 | 179 | #### `UserInfo.js` 180 | 181 | ```js 182 | import { Component } from 'react'; 183 | import { connect } from 'react-redux'; 184 | import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators'; 185 | 186 | class Posts extends Component { 187 | loadData(userId) { 188 | // 调用 React Redux `connect()` 注入 props : 189 | let { dispatch, posts } = this.props; 190 | 191 | if (posts[userId]) { 192 | // 这里是被缓存的数据!啥也不做。 193 | return; 194 | } 195 | 196 | // Reducer 可以通过设置 `isFetching` 反应这个 action 197 | // 因此让我们显示一个 Spinner 控件。 198 | dispatch(loadPostsRequest(userId)); 199 | 200 | // Reducer 可以通过填写 `users` 反应这些 actions 201 | fetch(`http://myapi.com/users/${userId}/posts`).then( 202 | response => dispatch(loadPostsSuccess(userId, response)), 203 | error => dispatch(loadPostsFailure(userId, error)) 204 | ); 205 | } 206 | 207 | componentDidMount() { 208 | this.loadData(this.props.userId); 209 | } 210 | 211 | componentWillReceiveProps(nextProps) { 212 | if (nextProps.userId !== this.props.userId) { 213 | this.loadData(nextProps.userId); 214 | } 215 | } 216 | 217 | render() { 218 | if (this.props.isLoading) { 219 | return

    Loading...

    ; 220 | } 221 | 222 | let posts = this.props.posts.map(post => 223 | 224 | ); 225 | 226 | return
    {posts}
    ; 227 | } 228 | } 229 | 230 | export default connect(state => ({ 231 | posts: state.posts 232 | }))(Posts); 233 | ``` 234 | 235 | 然而,不久就需要再来一遍,因为不同的 components 从同样的 API 端点请求数据。而且,我们想要在多个components 中重用一些逻辑(比如,当缓存数据有效的时候提前退出)。 236 | 237 | **中间件让我们写的更清楚M的潜在的异步 action creators.** 它使得我们分发普通对象之外的东西,并且解释它们的值。比如,中间件能 “捕捉” 到已经分发的 Promises 并把他们变为一对请求和成功/失败 actions. 238 | 239 | 最简单的中间件例子是 [redux-thunk](https://github.com/gaearon/redux-thunk). **“Thunk” 中间件让你把 action creators 写成 “thunks”,也就是返回函数的函数。** 这使得控制被反转了: 你会像一个参数一样取得 `dispatch` ,所以你也能写一个多次分发的 action creator 。 240 | 241 | >##### 注意 242 | 243 | >Thunk 只是中间件的一个例子。中间件不是关于 “让你分发函数” 的:它是关于让你分发你用的特定中间件知道如何处理的任何东西的。Thunk 中间件添加了一个特定的行为用来分发函数,但这实际上取决于你用的中间件。 244 | 245 | 考虑上面的代码用 [redux-thunk](https://github.com/gaearon/redux-thunk) 重写: 246 | 247 | #### `actionCreators.js` 248 | 249 | ```js 250 | export function loadPosts(userId) { 251 | // 用 thunk 中间件解释: 252 | return function (dispatch, getState) { 253 | let { posts } = getState(); 254 | if (posts[userId]) { 255 | // 这里是数据缓存!啥也不做。 256 | return; 257 | } 258 | 259 | dispatch({ 260 | type: 'LOAD_POSTS_REQUEST', 261 | userId 262 | }); 263 | 264 | // 异步分发原味 actions 265 | fetch(`http://myapi.com/users/${userId}/posts`).then( 266 | response => dispatch({ 267 | type: 'LOAD_POSTS_SUCCESS', 268 | userId, 269 | respone 270 | }), 271 | error => dispatch({ 272 | type: 'LOAD_POSTS_FAILURE', 273 | userId, 274 | error 275 | }) 276 | ); 277 | } 278 | } 279 | ``` 280 | 281 | #### `UserInfo.js` 282 | 283 | ```js 284 | import { Component } from 'react'; 285 | import { connect } from 'react-redux'; 286 | import { loadPosts } from './actionCreators'; 287 | 288 | class Posts extends Component { 289 | componentDidMount() { 290 | this.props.dispatch(loadPosts(this.props.userId)); 291 | } 292 | 293 | componentWillReceiveProps(nextProps) { 294 | if (nextProps.userId !== this.props.userId) { 295 | this.props.dispatch(loadPosts(nextProps.userId)); 296 | } 297 | } 298 | 299 | render() { 300 | if (this.props.isLoading) { 301 | return

    Loading...

    ; 302 | } 303 | 304 | let posts = this.props.posts.map(post => 305 | 306 | ); 307 | 308 | return
    {posts}
    ; 309 | } 310 | } 311 | 312 | export default connect(state => ({ 313 | posts: state.posts 314 | }))(Posts); 315 | ``` 316 | 317 | 这样打得字少多了!如果你喜欢,你还是可以保留 “原味” action creators 比如从一个 “聪明的” `loadPosts` action creator 里用到的 `loadPostsSuccess` 。 318 | 319 | **最后,你可以重写中间件** 你可以把上面的模式泛化,然后代之以这样的异步 action creators : 320 | 321 | ```js 322 | export function loadPosts(userId) { 323 | return { 324 | // 要在之前和之后发送的 action types 325 | types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], 326 | // 检查缓存 (可选): 327 | shouldCallAPI: (state) => !state.users[userId], 328 | // 进行取: 329 | callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), 330 | // 在 actions 的开始和结束注入的参数 331 | payload: { userId } 332 | }; 333 | } 334 | ``` 335 | 336 | 解释这个 actions 的中间件可以像这样: 337 | 338 | ```js 339 | function callAPIMiddleware({ dispatch, getState }) { 340 | return function (next) { 341 | return function (action) { 342 | const { 343 | types, 344 | callAPI, 345 | shouldCallAPI = () => true, 346 | payload = {} 347 | } = action; 348 | 349 | if (!types) { 350 | // 普通 action:传走 351 | return next(action); 352 | } 353 | 354 | if ( 355 | !Array.isArray(types) || 356 | types.length !== 3 || 357 | !types.every(type => typeof type === 'string') 358 | ) { 359 | throw new Error('Expected an array of three string types.'); 360 | } 361 | 362 | if (typeof callAPI !== 'function') { 363 | throw new Error('Expected fetch to be a function.'); 364 | } 365 | 366 | if (!shouldCallAPI(getState())) { 367 | return; 368 | } 369 | 370 | const [requestType, successType, failureType] = types; 371 | 372 | dispatch(Object.assign({}, payload, { 373 | type: requestType 374 | })); 375 | 376 | return callAPI().then( 377 | response => dispatch(Object.assign({}, payload, { 378 | response: response, 379 | type: successType 380 | })), 381 | error => dispatch(Object.assign({}, payload, { 382 | error: error, 383 | type: failureType 384 | })) 385 | ); 386 | }; 387 | }; 388 | } 389 | ``` 390 | 391 | 在传给 [`applyMiddleware(...middlewares)`](../api/applyMiddleware.md) 一次以后,你能用相同方式写你的 API-调用 action creators : 392 | 393 | ```js 394 | export function loadPosts(userId) { 395 | return { 396 | types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], 397 | shouldCallAPI: (state) => !state.users[userId], 398 | callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), 399 | payload: { userId } 400 | }; 401 | } 402 | 403 | export function loadComments(postId) { 404 | return { 405 | types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'], 406 | shouldCallAPI: (state) => !state.posts[postId], 407 | callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`), 408 | payload: { postId } 409 | }; 410 | } 411 | 412 | export function addComment(postId, message) { 413 | return { 414 | types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'], 415 | callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, { 416 | method: 'post', 417 | headers: { 418 | 'Accept': 'application/json', 419 | 'Content-Type': 'application/json' 420 | }, 421 | body: JSON.stringify({ message }) 422 | }), 423 | payload: { postId, message } 424 | }; 425 | } 426 | ``` 427 | 428 | ## Reducers 429 | 430 | Redux 用函数描述逻辑更新减少了模版里大量的 Flux stores 。函数比对象简单,比类更简单得多。 431 | 432 | 考虑这个 Flux store: 433 | 434 | ```js 435 | let _todos = []; 436 | 437 | export default const TodoStore = assign({}, EventEmitter.prototype, { 438 | getAll() { 439 | return _todos; 440 | } 441 | }); 442 | 443 | AppDispatcher.register(function (action) { 444 | switch (action.type) { 445 | case ActionTypes.ADD_TODO: 446 | let text = action.text.trim(); 447 | _todos.push(text); 448 | TodoStore.emitChange(); 449 | } 450 | }); 451 | ``` 452 | 453 | 用了 Redux 之后,同样的逻辑更新可以被写成 reducing function: 454 | 455 | ```js 456 | export function todos(state = [], action) { 457 | switch (action.type) { 458 | case ActionTypes.ADD_TODO: 459 | let text = action.text.trim(); 460 | return [...state, text]; 461 | default: 462 | return state; 463 | } 464 | } 465 | ``` 466 | 467 | `switch` 陈述 *不是* 真正的模版。真正的 Flux 模版是概念性的:发送更新的需求,用 Dispatcher 注册 Store 的需求,Store 是对象的需求 (当你想要一个哪都能跑的 App 的时候复杂度会提升)。 468 | 469 | 不幸的是很多人仍然靠文档里用没用 `switch` 来选择 Flux 框架。如果你不爱用 `switch` 你可以用一个单独的函数来解决,下面会演示。 470 | 471 | ### 生成 Reducers 472 | 473 | 让我们写一个函数使得我们将 reducers 表达为 action types 到 handlers 的映射对象。例如,在我们的 `todos` reducer 里这样定义: 474 | 475 | ```js 476 | export const todos = createReducer([], { 477 | [ActionTypes.ADD_TODO](state, action) { 478 | let text = action.text.trim(); 479 | return [...state, text]; 480 | } 481 | } 482 | ``` 483 | 484 | 我们可以写下面的帮忙函数来完成: 485 | 486 | ```js 487 | function createReducer(initialState, handlers) { 488 | return function reducer(state = initialState, action) { 489 | if (handlers.hasOwnProperty(action.type)) { 490 | return handlers[action.type](state, action); 491 | } else { 492 | return state; 493 | } 494 | } 495 | } 496 | ``` 497 | 498 | 不难对吧?Redux 没有默认提供这样的帮忙函数,因为有好多种写的方法。可能你想要自动把普通 JS 对象变成不可变对象通过湿化服务器状态。可能你想合并返回状态和当前状态。有很多方法 “获取所有” handler。这些都取决于你为你的团队在特定项目中选择的约定。 499 | 500 | Redux reducer 的 API 是 `(state, action) => state`,但是怎么创建这些 reducers 由你来定。 501 | -------------------------------------------------------------------------------- /docs/recipes/WritingTests.md: -------------------------------------------------------------------------------- 1 | # 编写测试 2 | 3 | 因为你写的大部分 Redux 代码都是些函数,而且大部分是纯函数,所以很好测,不需要 mock。 4 | 5 | ### 设置 6 | 7 | 我们建议用 [Mocha](http://mochajs.org/) 作为测试引擎。 8 | 注意因为是在 node 环境下运行,所以你不能访问 DOM。 9 | 10 | ``` 11 | npm install --save-dev mocha 12 | ``` 13 | 14 | 想结合 [Babel](http://babeljs.io) 使用的话,在 `package.json` 的 `scripts` 里加入这一段: 15 | 16 | ```js 17 | { 18 | ... 19 | "scripts": { 20 | ... 21 | "test": "mocha --compilers js:babel/register --recursive", 22 | "test:watch": "npm test -- --watch", 23 | }, 24 | ... 25 | } 26 | ``` 27 | 28 | 然后运行 `npm test` 就能单次运行了,或者也可以使用 `npm run test:watch` 在每次有文件改变时自动执行测试。 29 | 30 | ### Action Creators 31 | 32 | Redux 里的 action creators 是会返回普通对象的函数。在测试 action creators 的时候我们想要测试不仅是调用了正确的 action creator,还有是否返回了正确的 action。 33 | 34 | #### 示例 35 | 36 | ```js 37 | export function addTodo(text) { 38 | return { 39 | type: 'ADD_TODO', 40 | text 41 | }; 42 | } 43 | ``` 44 | 可以这么测: 45 | 46 | ```js 47 | import expect from 'expect'; 48 | import * as actions from '../../actions/TodoActions'; 49 | import * as types from '../../constants/ActionTypes'; 50 | 51 | describe('actions', () => { 52 | it('should create an action to add a todo', () => { 53 | const text = 'Finish docs'; 54 | const expectedAction = { 55 | type: types.ADD_TODO, 56 | text 57 | }; 58 | expect(actions.addTodo(text)).toEqual(expectedAction); 59 | }); 60 | } 61 | ``` 62 | 63 | ### Reducers 64 | 65 | Reducer 应该是把 action 应用到之前的 state,并返回新的 state。测试起来是下面这样的。 66 | 67 | #### 示例 68 | 69 | ```js 70 | import { ADD_TODO } from '../constants/ActionTypes'; 71 | 72 | const initialState = [{ 73 | text: 'Use Redux', 74 | completed: false, 75 | id: 0 76 | }]; 77 | 78 | export default function todos(state = initialState, action) { 79 | switch (action.type) { 80 | case ADD_TODO: 81 | return [{ 82 | id: (state.length === 0) ? 0 : state[0].id + 1, 83 | completed: false, 84 | text: action.text 85 | }, ...state]; 86 | 87 | default: 88 | return state; 89 | } 90 | } 91 | ``` 92 | 可以这么测: 93 | 94 | ```js 95 | import expect from 'expect'; 96 | import reducer from '../../reducers/todos'; 97 | import * as types from '../../constants/ActionTypes'; 98 | 99 | describe('todos reducer', () => { 100 | it('should return the initial state', () => { 101 | expect( 102 | reducer(undefined, {}) 103 | ).toEqual([{ 104 | text: 'Use Redux', 105 | completed: false, 106 | id: 0 107 | }]); 108 | }); 109 | 110 | it('should handle ADD_TODO', () => { 111 | expect( 112 | reducer([], { 113 | type: types.ADD_TODO, 114 | text: 'Run the tests' 115 | }) 116 | ).toEqual([{ 117 | text: 'Run the tests', 118 | completed: false, 119 | id: 0 120 | }]); 121 | 122 | expect( 123 | reducer([{ 124 | text: 'Use Redux', 125 | completed: false, 126 | id: 0 127 | }], { 128 | type: types.ADD_TODO, 129 | text: 'Run the tests' 130 | }) 131 | ).toEqual([{ 132 | text: 'Run the tests', 133 | completed: false, 134 | id: 1 135 | }, { 136 | text: 'Use Redux', 137 | completed: false, 138 | id: 0 139 | }]); 140 | }); 141 | ``` 142 | 143 | ### Components 144 | 145 | React components 有一点好,就是他们一般都很小而且依赖于他们的 props。所以很好测。 146 | 147 | 要测 components 我们要建一个叫 `setup()` 的辅助方法,用来把模拟过的(stubbed)回调函数当作 props 来传入,然后使用 [React 浅渲染](https://facebook.github.io/react/docs/test-utils.html#shallow-rendering) 来渲染组件。这样就可以通过做 “是否调用了回调函数” 这样的断言来写独立的测试。 148 | 149 | #### 示例 150 | 151 | ```js 152 | import React, { PropTypes, Component } from 'react'; 153 | import TodoTextInput from './TodoTextInput'; 154 | 155 | class Header extends Component { 156 | handleSave(text) { 157 | if (text.length !== 0) { 158 | this.props.addTodo(text); 159 | } 160 | } 161 | 162 | render() { 163 | return ( 164 |
    165 |

    todos

    166 | 169 |
    170 | ); 171 | } 172 | } 173 | 174 | Header.propTypes = { 175 | addTodo: PropTypes.func.isRequired 176 | }; 177 | 178 | export default Header; 179 | ``` 180 | 181 | 可以这么测: 182 | 183 | ```js 184 | import expect from 'expect'; 185 | import jsdomReact from '../jsdomReact'; 186 | import React from 'react/addons'; 187 | import Header from '../../components/Header'; 188 | import TodoTextInput from '../../components/TodoTextInput'; 189 | 190 | const { TestUtils } = React.addons; 191 | 192 | function setup() { 193 | let props = { 194 | addTodo: expect.createSpy() 195 | }; 196 | 197 | let renderer = TestUtils.createRenderer(); 198 | renderer.render(
    ); 199 | let output = renderer.getRenderOutput(); 200 | 201 | return { 202 | props: props, 203 | output: output, 204 | renderer: renderer 205 | }; 206 | } 207 | 208 | describe('components', () => { 209 | jsdomReact(); 210 | 211 | describe('Header', () => { 212 | it('should render correctly', () => { 213 | const { output } = setup(); 214 | 215 | expect(output.type).toBe('header'); 216 | expect(output.props.className).toBe('header'); 217 | 218 | let [h1, input] = output.props.children; 219 | 220 | expect(h1.type).toBe('h1'); 221 | expect(h1.props.children).toBe('todos'); 222 | 223 | expect(input.type).toBe(TodoTextInput); 224 | expect(input.props.newTodo).toBe(true); 225 | expect(input.props.placeholder).toBe('What needs to be done?'); 226 | }); 227 | 228 | it('should call call addTodo if length of text is greater than 0', () => { 229 | const { output, props } = setup(); 230 | let input = output.props.children[1]; 231 | input.props.onSave(''); 232 | expect(props.addTodo.calls.length).toBe(0); 233 | input.props.onSave('Use Redux'); 234 | expect(props.addTodo.calls.length).toBe(1); 235 | }); 236 | }); 237 | }); 238 | ``` 239 | 240 | #### `setState()` 异常修复 241 | 242 | 浅渲染目前的问题是 [如果调用 `setState` 便抛异常](https://github.com/facebook/react/issues/4019). React 貌似想要的是,如果想要使用 `setState`,DOM 就一定要存在(但测试运行在 node 环境下,是没有 DOM 的)。要解决这个问题,我们用了 jsdom,为了在 DOM 无效的时候,React 也不抛异常。按下面方法设置它: 243 | 244 | ``` 245 | npm install --save-dev jsdom mocha-jsdom 246 | ``` 247 | 248 | 然后添加 `jsdomReact()` 帮助函数,是这样的: 249 | 250 | ```js 251 | import ExecutionEnvironment from 'react/lib/ExecutionEnvironment'; 252 | import jsdom from 'mocha-jsdom'; 253 | 254 | export default function jsdomReact() { 255 | jsdom(); 256 | ExecutionEnvironment.canUseDOM = true; 257 | } 258 | ``` 259 | 260 | 要在运行任何的 component 测试之前调用。注意这么做不优雅,等以后 [facebook/react#4019](https://github.com/facebook/react/issues/4019) 解决了之后,这段代码就可以删除了。 261 | 262 | ### 词汇表 263 | 264 | - [React Test Utils](http://facebook.github.io/react/docs/test-utils.html): 跟 React 一块来的测试小助手。 265 | 266 | - [jsdom](https://github.com/tmpvar/jsdom): 一个 JavaScript 的内建 DOM 。Jsdom 允许没浏览器的时候也能跑测试。 267 | 268 | - [浅渲染(shallow renderer)](http://facebook.github.io/react/docs/test-utils.html#shallow-rendering): 浅渲染的中心思想是,初始化一个 component 然后得到它的`渲染`方法作为结果,比起渲染成 DOM 那么深的只有一级那么深。浅渲染的结果是一个 [ReactElement](https://facebook.github.io/react/docs/glossary.html#react-elements) ,意味着可以访问它的 children, props 还能测试是否工作正常。 269 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-in-chinese", 3 | "description": "Redux 中文文档", 4 | "scripts": { 5 | "clean": "rimraf _book", 6 | "build": "gitbook build -g camsong/redux-in-chinese", 7 | "watch": "gitbook serve", 8 | "publish": "npm run clean && npm run build && cd _book && git init && git commit --allow-empty -m 'update book' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update book' && git push git@github.com:camsong/redux-in-chinese gh-pages --force" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/camsong/redux-in-chinese.git" 13 | }, 14 | "keywords": [ 15 | "flux", 16 | "redux", 17 | "reducer", 18 | "react", 19 | "reactjs", 20 | "hot", 21 | "reload", 22 | "hmr", 23 | "live", 24 | "edit", 25 | "webpack" 26 | ], 27 | "author": "Cam Song (http://github.com/camsong)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/camsong/redux-in-chinese/issues" 31 | }, 32 | "homepage": "https://github.com/camsong/redux-in-chinese", 33 | "devDependencies": { 34 | "gitbook-cli": "^0.3.4", 35 | "gitbook-plugin-ga": "^0.2.1", 36 | "rimraf": "^2.3.4" 37 | } 38 | } 39 | --------------------------------------------------------------------------------