├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── README_zh.md ├── docs ├── api.md ├── guide.md └── zh │ ├── api.md │ └── guide.md ├── examples ├── counter │ ├── .babelrc │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ └── index.js │ └── webpack.config.js ├── simple-router │ ├── .babelrc │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.js │ │ ├── components │ │ │ ├── About.js │ │ │ ├── AddTopic.js │ │ │ ├── Header.js │ │ │ ├── Home.js │ │ │ ├── Topic.js │ │ │ └── Topics.js │ │ ├── containers │ │ │ └── Topics.js │ │ └── index.js │ └── webpack.config.js └── todo │ ├── .babelrc │ ├── README.md │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── components │ │ ├── AddTodo.js │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ ├── containers │ │ ├── Link.js │ │ └── TodoList.js │ └── index.js │ └── webpack.config.js ├── package-lock.json ├── package.json ├── setupTests.js ├── src ├── actions.js ├── defaults.js ├── effects.js ├── hook.js ├── index.js ├── middleware.js ├── mirror.js ├── model.js ├── render.js ├── router.js ├── routerMiddleware.js ├── store.js └── toReducers.js ├── test ├── actions.spec.js ├── defaults.spec.js ├── hook.spec.js ├── middleware.spec.js ├── model.spec.js ├── provider.spec.js ├── render.spec.js ├── router.spec.js ├── store.spec.js └── toReducers.spec.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-transform-runtime", 8 | "@babel/plugin-proposal-class-properties" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true 11 | }, 12 | }, 13 | "env": { 14 | "browser": true, 15 | "commonjs": true, 16 | "node": true, 17 | "es6": true, 18 | "jest": true 19 | }, 20 | "rules": { 21 | "indent": [2, 2, { 22 | "SwitchCase": 1 23 | }], 24 | "linebreak-style": [2, "unix"], 25 | "no-multi-spaces": 2, 26 | "no-trailing-spaces": 2, 27 | "quotes": [2, "single", { 28 | "allowTemplateLiterals": true 29 | }], 30 | "semi": [2, "never"], 31 | "space-before-blocks": 2, 32 | "spaced-comment": [2, "always"], 33 | "object-curly-spacing": [2, "always"], 34 | }, 35 | "plugins": [ 36 | "react" 37 | ], 38 | settings: { 39 | react: { 40 | version: 'detect' 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | node_modules 3 | npm-debug.log.* 4 | 5 | dist 6 | lib 7 | coverage 8 | 9 | **/dist 10 | **/build 11 | 12 | .tmp 13 | *.swo 14 | *.swp 15 | *.diff 16 | *.log 17 | *.patch 18 | 19 | .DS_STORE 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: 5 | - npm run lint 6 | - npm run test:cov 7 | after_success: 8 | - npm run coveralls 9 | cache: yarn 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## [v1.1.0](https://github.com/mirrorjs/mirror/compare/v1.1.0...v1.1.1) 2 | 3 | > 2019-08-28 4 | 5 | * Update some dependencies. 6 | 7 | ## [v1.1.0](https://github.com/mirrorjs/mirror/compare/v0.2.13...v1.1.0) 8 | 9 | > 2019-05-06 10 | 11 | * Add [`toReducers`](docs/api.md#toreducers) and [`middleware`](docs/api.md#middleware). 12 | 13 | ## ~~v1.0.0~~ 14 | 15 | *Deprecated.* 16 | 17 | ## [v0.2.13](https://github.com/mirrorjs/mirror/compare/v0.2.12...v0.2.13) 18 | 19 | > 2019-03-21 20 | 21 | * Fix `history` cjs style importing warning ([#106](https://github.com/mirrorjs/mirror/pull/106) by @kvkens). 22 | 23 | ## [v0.2.12](https://github.com/mirrorjs/mirror/compare/v0.2.11...v0.2.12) 24 | 25 | > 2018-09-08 26 | 27 | * Allow `mirror.defaults` to *merge* new Redux reducers into the previous ones. 28 | 29 | ## [v0.2.11](https://github.com/mirrorjs/mirror/compare/v0.2.10...v0.2.11) 30 | 31 | > 2018-05-04 32 | 33 | * Update dependencies and package-lock.json to prevent potential security vulnerability 34 | 35 | ## [v0.2.10](https://github.com/mirrorjs/mirror/compare/v0.2.9...v0.2.10) 36 | 37 | > 2017-10-20 38 | 39 | * Update react-router-redux to v5.0.0.alpha-8 40 | 41 | ## [v0.2.9](https://github.com/mirrorjs/mirror/compare/v0.2.7...v0.2.9) 42 | 43 | > 2017-10-19 44 | 45 | * Tag effects with `isEffect` prop (by @shmck) 46 | * Update dependencies including React 16 and others 47 | * Add change log 48 | 49 | ## [v0.2.7](https://github.com/mirrorjs/mirror/compare/v0.2.6...v0.2.7) 50 | 51 | > 2017-09-16 52 | 53 | * Support extra custom `reducers` in `mirror.defaults` ([#47](https://github.com/mirrorjs/mirror/pull/47) by @madisvain) 54 | * Support `basename` and some other props for Router ([#22](https://github.com/mirrorjs/mirror/issues/22)) 55 | * Move `prop-types` to peerDependencies 56 | * Update code style to force spacing inside braces 57 | * Some minor code refactors 58 | 59 | ## [v0.2.6](https://github.com/mirrorjs/mirror/compare/v0.2.5...v0.2.6) 60 | 61 | > 2017-09-08 62 | 63 | * Optionally overwrite `addEffect` ([#37](https://github.com/mirrorjs/mirror/pull/37) by @shmck) 64 | * Add babel-runtime dep for Couter example if npm < 3 65 | * Change to ES6 modules for `index.js` 66 | * Docs updating 67 | 68 | ## [v0.2.5](https://github.com/mirrorjs/mirror/compare/v0.2.4...v0.2.5) 69 | 70 | > 2017-08-25 71 | 72 | * Export `Prompt` and `withRouter` for Router ([#30](https://github.com/mirrorjs/mirror/issues/30)) 73 | * Correct typos in docs 74 | 75 | ## [v0.2.4](https://github.com/mirrorjs/mirror/compare/v0.2.3...v0.2.4) 76 | 77 | > 2017-08-17 78 | 79 | * Correct some typos in docs 80 | * Add API links in `docs/api.md` 81 | 82 | ## [v0.2.3](https://github.com/mirrorjs/mirror/compare/v0.2.2...v0.2.3) 83 | 84 | > 2017-08-11 85 | 86 | * Update docs to make it better 87 | 88 | ## [v0.2.2](https://github.com/mirrorjs/mirror/compare/v0.2.1...v0.2.2) 89 | 90 | > 2017-08-10 91 | 92 | * Minor updating in docs 93 | 94 | ## [v0.2.1](https://github.com/mirrorjs/mirror/compare/v0.2.0...v0.2.1) 95 | 96 | > 2017-08-10 97 | 98 | * Change action type separator from `.` to `/` 99 | * Update test suits and docs 100 | 101 | ## [v0.2.0](https://github.com/mirrorjs/mirror/compare/v0.1.5...v0.2.0) 102 | 103 | > 2017-08-02 104 | 105 | * Update README to add `effects` demonstration of `mirror.model` API 106 | 107 | ## [v0.1.5](https://github.com/mirrorjs/mirror/compare/v0.1.4...v0.1.5) 108 | 109 | > 2017-08-01 110 | 111 | * Update dependencies and readme 112 | * Add travis and coveralls CI 113 | 114 | ## [v0.1.4](https://github.com/mirrorjs/mirror/commit/c188d79d0781394ece3f8d02f5eb16794baa6140) 115 | 116 | > 2017-07-31 117 | 118 | 🎉🎉🎉 119 | 120 | 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Linghao Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mirror 2 | 3 | [![npm version](https://img.shields.io/npm/v/mirrorx.svg?colorB=007ec6&style=flat-square)](https://www.npmjs.com/package/mirrorx) [![build status](https://img.shields.io/travis/mirrorjs/mirror.svg?style=flat-square)](https://travis-ci.org/mirrorjs/mirror) [![coverage status](https://img.shields.io/coveralls/mirrorjs/mirror.svg?style=flat-square)](https://coveralls.io/github/mirrorjs/mirror?branch=master) [![license](https://img.shields.io/github/license/mirrorjs/mirror.svg?style=flat-square)](https://github.com/mirrorjs/mirror/blob/master/LICENSE) 4 | 5 | [查看中文](https://github.com/mirrorjs/mirror/blob/master/README_zh.md) 6 | 7 | A simple and powerful React framework with minimal API and zero boilerplate. (Inspired by [dva](https://github.com/dvajs/dva) and [jumpstate](https://github.com/jumpsuit/jumpstate)) 8 | 9 | > Painless React and Redux. 10 | 11 | ## Why? 12 | 13 | We love React and Redux. 14 | 15 | A typical React/Redux app looks like the following: 16 | 17 | * An `actions/` directory to manually create all `action type`s (or `action creator`s) 18 | * A `reducers/` directory and tons of `switch` clause to capture all `action type`s 19 | * Apply middlewares to handle `async action`s 20 | * Explicitly invoke `dispatch` method to dispatch all actions 21 | * Manually create `history` to router and/or sync with store 22 | * Invoke methods in `history` or dispatch actions to programmatically changing routes 23 | 24 | The problem? [Too much boilerplates](https://github.com/reactjs/redux/blob/master/docs/recipes/ReducingBoilerplate.md) and a little bit tedious. 25 | 26 | In fact, most part of the above steps could be simplified. Like, create `action`s and `reducer`s in a single method, or dispatch both sync and async actions by simply invoking a function without extra middleware, or define routes without caring about `history`, etc. 27 | 28 | That's exactly what Mirror does, encapsulates the tedious or repetitive work in very few APIs to offer a high level abstraction with efficiency and simplicity, and without breaking the pattern. 29 | 30 | ## Features 31 | 32 | * Minimal API(only 4 newly introduced) 33 | * Easy to start 34 | * Actions done easy, sync or async 35 | * Support code splitting 36 | * Full-featured hook mechanism 37 | 38 | ## Getting Started 39 | 40 | ### Creating an App 41 | 42 | Use [create-react-app](https://github.com/facebookincubator/create-react-app) to create an app: 43 | 44 | ```sh 45 | $ npm i -g create-react-app 46 | $ create-react-app my-app 47 | ``` 48 | 49 | After creating, install Mirror from npm: 50 | 51 | ```sh 52 | $ cd my-app 53 | $ npm i --save mirrorx 54 | $ npm start 55 | ``` 56 | 57 | ### `index.js` 58 | 59 | ```js 60 | import React from 'react' 61 | import mirror, {actions, connect, render} from 'mirrorx' 62 | 63 | // declare Redux state, reducers and actions, 64 | // all actions will be added to `actions`. 65 | mirror.model({ 66 | name: 'app', 67 | initialState: 0, 68 | reducers: { 69 | increment(state) { return state + 1 }, 70 | decrement(state) { return state - 1 } 71 | }, 72 | effects: { 73 | async incrementAsync() { 74 | await new Promise((resolve, reject) => { 75 | setTimeout(() => { 76 | resolve() 77 | }, 1000) 78 | }) 79 | actions.app.increment() 80 | } 81 | } 82 | }) 83 | 84 | // connect state with component 85 | const App = connect(state => { 86 | return {count: state.app} 87 | })(props => ( 88 |
89 |

{props.count}

90 | {/* dispatch the actions */} 91 | 92 | 93 | {/* dispatch the async action */} 94 | 95 |
96 | ) 97 | ) 98 | 99 | // start the app,`render` is an enhanced `ReactDOM.render` 100 | render(, document.getElementById('root')) 101 | ``` 102 | 103 | ### [Demo](https://codesandbox.io/s/814mnvw1qj) 104 | 105 | ## Guide 106 | 107 | See [Guide](https://github.com/mirrorjs/mirror/blob/master/docs/guide.md). 108 | 109 | ## API 110 | 111 | See [API Reference](https://github.com/mirrorjs/mirror/blob/master/docs/api.md). 112 | 113 | ## Examples 114 | 115 | * [User-Dashboard](https://github.com/mirrorjs/user-dashboard-example) (An example similar to dva-user-dashboard) 116 | * [Counter](https://github.com/mirrorjs/mirror/blob/master/examples/counter) 117 | * [Simple-Router](https://github.com/mirrorjs/mirror/blob/master/examples/simple-router) 118 | * [Todo](https://github.com/mirrorjs/mirror/blob/master/examples/todo) 119 | 120 | ## Change log 121 | 122 | See [CHANGES.md](https://github.com/mirrorjs/mirror/blob/master/CHANGES.md). 123 | 124 | ## FAQ 125 | 126 | #### Does Mirror support TypeScript? 127 | 128 | Yes, it does. 129 | 130 | #### Does Mirror support [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension)? 131 | 132 | Yes, Mirror integrates Redux DevTools by default to make your debugging more easily. 133 | 134 | #### Can I use extra Redux middlewares? 135 | 136 | Yes, specify them in [`mirror.defaults`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#-optionsmiddlewares) is all you need to do, learn more from the Docs. 137 | 138 | #### I'm really into Redux-Saga, is there any way to use it in Mirror? 139 | 140 | Yes of course, take a look at the [`addEffect`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#-optionsaddeffect) option. 141 | 142 | #### Which version of react-router does Mirror use? 143 | 144 | react-router v4. 145 | 146 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Mirror 2 | 3 | [![npm version](https://img.shields.io/npm/v/mirrorx.svg?colorB=007ec6&style=flat-square)](https://www.npmjs.com/package/mirrorx) [![build status](https://img.shields.io/travis/mirrorjs/mirror.svg?style=flat-square)](https://travis-ci.org/mirrorjs/mirror) [![coverage status](https://img.shields.io/coveralls/mirrorjs/mirror.svg?style=flat-square)](https://coveralls.io/github/mirrorjs/mirror?branch=master) [![license](https://img.shields.io/github/license/mirrorjs/mirror.svg?style=flat-square)](https://github.com/mirrorjs/mirror/blob/master/LICENSE) 4 | 5 | 一款简洁、高效、易上手的 React 框架。(Inspired by [dva](https://github.com/dvajs/dva) and [jumpstate](https://github.com/jumpsuit/jumpstate)) 6 | 7 | > Painless React and Redux. 8 | 9 | ## 为什么? 10 | 11 | 我们热爱 React 和 Redux。 12 | 13 | 一个典型的 React/Redux 应用看起来像下面这样: 14 | 15 | * 一个 `actions/` 目录用来手动创建所有的 `action type`(或者 `action creator`); 16 | * 一个 `reducers/` 目录以及无数的 `switch` 来捕获所有的 `action type`; 17 | * 必须要依赖 middleware 才能处理 `异步 action`; 18 | * 明确调用 `dispatch` 方法来 dispatch 所有的 action; 19 | * 手动创建 `history` 对象关联路由组件,可能还需要与 store 同步; 20 | * 调用 `history` 上的方法或者 dispatch action 来手动更新路由; 21 | 22 | 存在的问题?太多的 [样板文件](https://github.com/reactjs/redux/blob/master/docs/recipes/ReducingBoilerplate.md) 以及繁琐甚至重复的劳动。 23 | 24 | 实际上,上述大部分操作都是可以简化的。比如,在单个 API 中创建所有的 `action` 和 `reducer`;比如,简单地调用一个函数来 dispatch 所有的同步和异步 action,且不需要额外引入 middleware;再比如,使用路由的时候只需要关心定义具体的路由,不用去关心 `history` 对象,等等。 25 | 26 | 这正是 Mirror 的使命,用极少数的 API 封装所有繁琐甚至重复的工作,提供一种简洁高效的更高级抽象,同时保持原有的开发模式。 27 | 28 | ## 特性 29 | 30 | * 极简 API(只有 4 个新 API) 31 | * 易于上手 32 | * Redux action 从未如此简单 33 | * 支持 code splitting 34 | * 强大的 hook 机制 35 | 36 | ## 快速开始 37 | 38 | ### 初始化项目 39 | 40 | 使用 [create-react-app](https://github.com/facebookincubator/create-react-app) 创建一个新的 app: 41 | 42 | ```sh 43 | $ npm i -g create-react-app 44 | $ create-react-app my-app 45 | ``` 46 | 47 | 创建之后,从 npm 安装 Mirror: 48 | 49 | ```sh 50 | $ cd my-app 51 | $ npm i --save mirrorx 52 | $ npm start 53 | ``` 54 | 55 | ### `index.js` 56 | 57 | ```js 58 | import React from 'react' 59 | import mirror, {actions, connect, render} from 'mirrorx' 60 | 61 | // 声明 Redux state, reducer 和 action, 62 | // 所有的 action 都会以相同名称赋值到全局的 actions 对象上 63 | mirror.model({ 64 | name: 'app', 65 | initialState: 0, 66 | reducers: { 67 | increment(state) { return state + 1 }, 68 | decrement(state) { return state - 1 } 69 | }, 70 | effects: { 71 | async incrementAsync() { 72 | await new Promise((resolve, reject) => { 73 | setTimeout(() => { 74 | resolve() 75 | }, 1000) 76 | }) 77 | actions.app.increment() 78 | } 79 | } 80 | }) 81 | 82 | // 使用 react-redux 的 connect 方法,连接 state 和组件 83 | const App = connect(state => { 84 | return {count: state.app} 85 | })(props => ( 86 |
87 |

{props.count}

88 | {/* 调用 actions 上的方法来 dispatch action */} 89 | 90 | 91 | {/* dispatch async action */} 92 | 93 |
94 | ) 95 | ) 96 | 97 | // 启动 app,render 方法是加强版的 ReactDOM.render 98 | render(, document.getElementById('root')) 99 | ``` 100 | 101 | ### [Demo](https://codesandbox.io/s/814mnvw1qj) 102 | 103 | ## 指南 104 | 105 | 查看 [指南](https://github.com/mirrorjs/mirror/blob/master/docs/zh/guide.md)。 106 | 107 | ## API 108 | 109 | 查看 [API 文档](https://github.com/mirrorjs/mirror/blob/master/docs/zh/api.md)。 110 | 111 | ## 示例项目 112 | 113 | * [User-Dashboard](https://github.com/mirrorjs/user-dashboard-example)(一个类似 dva-user-dashboard 的示例项目) 114 | * [Counter](https://github.com/mirrorjs/mirror/blob/master/examples/counter) 115 | * [Simple-Router](https://github.com/mirrorjs/mirror/blob/master/examples/simple-router) 116 | * [Todo](https://github.com/mirrorjs/mirror/blob/master/examples/todo) 117 | 118 | ## 更新日志 119 | 120 | 查看 [CHANGES.md](https://github.com/mirrorjs/mirror/blob/master/CHANGES.md). 121 | 122 | ## FAQ 123 | 124 | #### 是否支持 TypeScript? 125 | 126 | 支持。 127 | 128 | #### 是否支持 [Redux DevTools 扩展](https://github.com/zalmoxisus/redux-devtools-extension)? 129 | 130 | 支持。 131 | 132 | #### 是否可以使用额外的 Redux middleware? 133 | 134 | 可以,在 [`mirror.defaults`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#-optionsmiddlewares) 接口中指定即可,具体可查看文档。 135 | 136 | #### 有办法在 Mirror 中使用 Redux-Saga 么? 137 | 138 | 当然有,查看 [`addEffect`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#-optionsaddeffect) 选项,有详细的说明。 139 | 140 | #### Mirror 用的是什么版本的 react-router? 141 | 142 | react-router 4.x。 143 | 144 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | * [mirror.model](#mirrormodelname-initialstate-reducers-effects) 4 | * [`name`](#-name) 5 | * [`initialState`](#-initialState) 6 | * [`reducers`](#-reducers) 7 | * [`effects`](#-effects) 8 | * [actions](#actions) 9 | * [mirror.hook](#mirrorhookaction-getstate--) 10 | * [mirror.defaults](#mirrordefaultsoptions) 11 | * [`initialState`](#-optionsinitialstate) 12 | * [`historyMode`](#-optionshistorymode) 13 | * [`middlewares`](#-optionsmiddlewares) 14 | * [`reducers`](#-optionsreducers) 15 | * [`addEffect`](#-optionsaddeffect) 16 | * [connect](#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 17 | * [render](#rendercomponent-container-callback) 18 | * [Router](#router) 19 | * [toReducers](#toreducers) 20 | * [middleware](#middleware) 21 | 22 | ### mirror.model({name, initialState, reducers, effects}) 23 | 24 | This method is used to create and inject a model. A "model" is a combination of Redux's `state`, `action` and `reducer`. Calling `mirror.model` will automatically create actions and reducers, which will be used to create `Redux` store. 25 | 26 | Basically, **it's a simple and powerful way to organize you `Redux` stuff**. 27 | 28 | #### * `name` 29 | 30 | To create a model, **`name` must be provided and be a valid string**. It is the name of the model, which means it will be used as the namespace of the future-to-create `Redux` store. 31 | 32 | Suppose you create a model like this: 33 | 34 | ```js 35 | import mirror from 'mirrorx' 36 | 37 | mirror.model({ 38 | name: 'app', 39 | }) 40 | ``` 41 | 42 | Then you will get a `Redux` store like this: 43 | 44 | ```js 45 | // ... 46 | 47 | store.getState() 48 | // {app: null} 49 | ``` 50 | 51 | The model `name` is where your `Redux` state goes in you root store(of course, it's important to `actions` too, we'll cover that later). 52 | 53 | Also note that the value of the created store's `app` state is `null`, if you want a different, more meaningful value, then you need to pass an `initialState`. 54 | 55 | > Note: Mirror uses [react-router-redux](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux), so you **can not** use `routing` as model name. 56 | 57 | #### * `initialState` 58 | 59 | As its name indicated, `initialState` is the initial state of a model, nothing special. It is used as the `initialState` of a standard `Redux` reducer. 60 | 61 | **It is not required, and could be anything**. If `initialState` is not specified, then it will be `null`, as demonstrated above. 62 | 63 | Create model: 64 | 65 | ```js 66 | import mirror from 'mirrorx' 67 | 68 | mirror.model({ 69 | name: 'app', 70 | + initialState: 0, 71 | }) 72 | ``` 73 | 74 | After store is created: 75 | 76 | ```js 77 | store.getState() 78 | // {app: 0} 79 | ``` 80 | 81 | #### * `reducers` 82 | 83 | `reducers` is where you put your `Redux` reducers. The principle here is **one reducer, one action**, so you don't need to care about the action type you are dealing with. 84 | 85 | ```js 86 | -import mirror from 'mirrorx' 87 | +import mirror, {actions} from 'mirrorx' 88 | 89 | mirror.model({ 90 | name: 'app', 91 | initialState: 0, 92 | + reducers: { 93 | + add(state, data) { 94 | + return state + data 95 | + }, 96 | + }, 97 | }) 98 | ``` 99 | 100 | Execute the code above, Mirror will do 3 things behind the scenes: 101 | 102 | 1. Create a [reducer](http://redux.js.org/docs/basics/Reducers.html); 103 | 2. Create a [action type](http://redux.js.org/docs/basics/Actions.html) (`app/add` in this case), which will be captured by the created reducer; 104 | 3. Add a function whose name is the reducer's name under `actions.` object. This function, when called, will `dispatch` the very action created above. 105 | 106 | Here, we can see that model's `name` has another usage: 107 | 108 | ```js 109 | // ... 110 | typeof actions.app 111 | // 'object' 112 | 113 | typeof actions.app.add 114 | // 'function' 115 | 116 | actions.app.add(1) 117 | // Same as: 118 | // 119 | // dispatch({ 120 | // type: 'app/add', 121 | // data: 1 122 | // }) 123 | 124 | // ... 125 | store.getState() 126 | // {app: 1} 127 | ``` 128 | 129 | Yes, model `name` will be an attribute of the `actions` object, who it self is an object too. And **all functions you defined in `reducers` will be added as methods to that object under the same name.** 130 | 131 | Functions defined in `reducers` are most of the part a `Redux` reducer(**so it must be a [pure function](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#oh-to-be-pure-again) too**), except one tiny difference: 132 | 133 | ```js 134 | // Redux standard reducer 135 | function reduxReducer(state, {type, data}) { 136 | // do something, return some other state 137 | } 138 | 139 | // reducer defined in `reducers` 140 | function reducerInReducers(state, data) { 141 | // do something, return some other state 142 | } 143 | ``` 144 | 145 | For the standard `Redux` reducer, you pass an action object as the second parameter; while for the "reducer" defined in model's reducers, **you pass the action data as the second parameter**, because you don't have to care about the action type -- Mirror does that for you. 146 | 147 | What parameter should you pass when calling methods added in `actions.`? **Just the action data**. 148 | 149 | ```js 150 | // ... 151 | 152 | // You don't need to pass a `state` when dispath actions, right? 153 | actions.app.add(100) 154 | ``` 155 | 156 | Every reducer of every model you created will be combined together(using Redux's [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html)), and then used to create your `Redux` store. 157 | 158 | > Note: non-function entries in `reducers` is pointless, and will be ignored(same as `effects`): 159 | > 160 | > ```js 161 | > import mirror, {actions} from 'mirrorx' 162 | > 163 | > mirror.model({ 164 | > name: 'app', 165 | > reducers: { 166 | > a: 1 167 | > }, 168 | > }) 169 | > 170 | > actions.app // undefined 171 | > ``` 172 | 173 | 174 | #### * `effects` 175 | 176 | **`effects` are [async actions of Redux](http://redux.js.org/docs/advanced/AsyncActions.html)**. In functional programming, [`effect`](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#side-effects-may-include) is the interaction with the world outside of a function. Since async actions do interact with the outside world, so they surely are effects. 177 | 178 | An `effect` does not directly update your `Redux` state, but invokes other "sync actions" to update the state, usually after some asynchronous operations(like HTTP requests). 179 | 180 | Like `reducers`, **every function you defined in `effects` will be added to `actions.` as a method with the same name**, and calling this method will call the original function. 181 | 182 | ```js 183 | import mirror, {actions} from 'mirrorx' 184 | 185 | mirror.model({ 186 | name: 'app', 187 | initialState: 0, 188 | reducers: { 189 | add(state, data) { 190 | return state + data 191 | }, 192 | }, 193 | + effects: { 194 | + async myEffect(data, getState) { 195 | + const res = await Promise.resolve(data) 196 | + actions.app.add(res) 197 | + } 198 | + }, 199 | }) 200 | ``` 201 | 202 | Now, `actions.app` will have 2 methods: 203 | 204 | * `actions.app.add` 205 | * `actions.app.myEffect` 206 | 207 | There is no magic here, calling `actions.app.myEffect` will dispatch an action and run the exact code in `effects.myEffect`: 208 | 209 | ```js 210 | // ... 211 | 212 | // First, dispatch the action: 213 | // dispatch({ 214 | // type: 'app/myEffect', 215 | // data: 10 216 | // }) 217 | // 218 | // Second, invoke the method: 219 | // effects.myEffect(10) 220 | actions.app.myEffect(10) 221 | 222 | // ... 223 | store.getState() 224 | // {app: 10} 225 | ``` 226 | 227 | That's it, all you have to do is call the methods Mirror automatically added to `actions.`, and your async actions is dispatched! Maybe you have used some great middlewares to handle async actions, such as [redux-thunk](https://github.com/gaearon/redux-thunk) or [redux-saga](https://redux-saga.js.org). But none of them is as simple as Mirror is. 228 | 229 | Functions you defined in `effects` will get 2 arguments: 230 | 231 | * `data` - The data you pass when calling methods in `actions.`. 232 | * `getState` - It's actually `store.getState`, will return the root state of your store when called. 233 | 234 | But, when calling the corresponding methods Mirror added to `actions.` , you only need to pass the above `data` parameter to it -- if you want to. 235 | 236 | **`async/await` is the recommended way to define effects, but is not the only way.** 237 | 238 | You can go `Promise`: 239 | 240 | ```js 241 | // ... 242 | 243 | effects: { 244 | promisedEffect(data, getState) { 245 | return Promise.resolve(data).then(result => { 246 | // call your sync actions 247 | }) 248 | } 249 | } 250 | ``` 251 | 252 | Or, you can even go the old school `callback`(**discouraged**): 253 | 254 | ```js 255 | // ... 256 | 257 | effects: { 258 | callbackEffect(data, getState) { 259 | setTimeout(() => { 260 | // call your sync actions 261 | }, 1000) 262 | } 263 | } 264 | ``` 265 | 266 | **The point is, you can handle your async operations in whatever way you want, Mirror provides a consistent API to manage them.** 267 | 268 | > Note: action name in `effects` should not be duplicated with those in `reducers`: 269 | > 270 | > ```js 271 | > import mirror, {actions} from 'mirrorx' 272 | > 273 | > // Will throw an error 274 | > mirror.model({ 275 | > name: 'app', 276 | > reducers: { 277 | > add(state, data) { 278 | > } 279 | > }, 280 | > effects: { 281 | > add(data, getState) { 282 | > } 283 | > } 284 | > }) 285 | > ``` 286 | 287 | 288 | 289 | ### actions 290 | 291 | The `actions` object contains both your Redux `action`s and `reducer`s. Calling methods in it will `dispatch` some secret action, which will be captured by functions you defined in `reducers` and `effects` object. 292 | 293 | In Mirror, all actions and effects are generated automatically and "namespaced”, meaning, you can't manually create an `action`, and more importantly, **you don't have to**. 294 | 295 | You don't have to explicitly create and dispatch any action at all. If you want to create an `action` and a `reducer` to handle it, don't bother to add an `action type constant`(or an `action creator`), and then add a `reducer`, just throw a reducer in `reducers`, that's all. 296 | 297 | Thus, you don't have to jump through files or directories to determine which action type should be handled by which reducer. 298 | 299 | For example, run: 300 | 301 | ```js 302 | actions.app.add(1) 303 | ``` 304 | 305 | Is exactly the same as the following code: 306 | 307 | ```js 308 | dispatch({ 309 | type: 'app/add', 310 | data: 1 311 | }) 312 | ``` 313 | 314 | Plus, using this global `actions` to handle `Redux` actions, you can easily tell the "dependencies" between different modules: 315 | 316 | In a.js: 317 | 318 | ```js 319 | // a.js 320 | import mirror, {actions} from 'mirrorx' 321 | 322 | mirror.model({ 323 | name: 'a', 324 | initialState: 0, 325 | reducers: { 326 | add(state, data) { 327 | return state + data 328 | }, 329 | }, 330 | }) 331 | ``` 332 | 333 | In b.js: 334 | 335 | ```js 336 | // b.js 337 | import mirror, {actions} from 'mirrorx' 338 | 339 | mirror.model({ 340 | name: 'b', 341 | effects: { 342 | async foo(state, data) { 343 | const res = await Promise.resolve(data) 344 | // update state of model `a` 345 | actions.a.add(data) 346 | }, 347 | }, 348 | }) 349 | ``` 350 | 351 | #### * `actions.routing` 352 | 353 | If the enhanced [`Router`](#router) component provided by Mirror is used in your app, then you'll get `actions.routing` for free. 354 | 355 | There are 5 methods in `actions.routing`: 356 | 357 | * `push(location)` - Pushes a new location to history, becoming the current location. 358 | * `replace(location)` - Replaces the current location in history. 359 | * `go` - Moves backwards or forwards a relative number of locations in history. 360 | * `goForward` - Moves forward one location. Equivalent to go(1). 361 | * `goBack` - Moves backwards one location. Equivalent to go(-1). 362 | 363 | The usage of these methods are **exactly the same** with [history API](https://github.com/ReactTraining/history/blob/v3/docs/GettingStarted.md#navigation). And thanks to [react-router-redux](https://github.com/reactjs/react-router-redux), an action will be dispatched when called, so your history will be synced with your store. 364 | 365 | ```js 366 | import mirror, {actions} from 'mirrorx' 367 | 368 | // ... 369 | 370 | actions.routing.push('/foo/bar') 371 | // => http://example.com/foo/bar 372 | 373 | actions.routing.push({ 374 | pathname: '/foo/bar', 375 | search: '?search=123' 376 | }) 377 | // => http://example.com/foo/bar?search=123 378 | ``` 379 | 380 | You can [learn more from here](https://github.com/ReactTraining/history/blob/v3/docs/Location.md#location). 381 | 382 | > Note: if your app does not use [`Router`](#router), `actions.routing` would be `undefined`. 383 | 384 | ### mirror.hook((action, getState) => {}) 385 | 386 | Add a hook to monitor actions that have been dispatched. 387 | 388 | ```js 389 | import mirror, {actions} from 'mirrorx' 390 | 391 | // ... 392 | 393 | const locationChangeHook = mirror.hook((action, getState) => { 394 | if (action.type === '@@router/LOCATION_CHANGE') { 395 | console.log('Location has just changed') 396 | } 397 | }) 398 | 399 | const countHook = mirror.hook((action, getState) => { 400 | if (getState().app.count === 10) { 401 | console.log('You have just reached 10!') 402 | } 403 | }) 404 | 405 | // Remove hooks 406 | locationChangeHook() 407 | countHook() 408 | ``` 409 | 410 | ### mirror.defaults(options) 411 | 412 | `mirror.defaults` is a pretty intuitive API, you use it to configure your Mirror app. 413 | 414 | `mirror.defaults` can be called multiple times. 415 | 416 | #### * `options.initialState` 417 | 418 | * Default: `undefined` 419 | 420 | The [`preloadedState`](http://redux.js.org/docs/api/createStore.html) for your Mirror app's store. 421 | 422 | ```js 423 | mirror.defaults({ 424 | initialState: {app: 1} 425 | }) 426 | 427 | mirror.model({ 428 | name: 'app', 429 | // ... 430 | }) 431 | 432 | // ... 433 | 434 | store.getState() 435 | // {app: 1} 436 | ``` 437 | 438 | #### * `options.historyMode` 439 | 440 | * Default: `browser` 441 | 442 | The [history type](https://github.com/ReactTraining/history#usage) for your router, there are 3 optional values: 443 | 444 | * `browser` - A DOM-specific implementation, useful in web browsers that support the HTML5 history API. 445 | * `hash` - A DOM-specific implementation for legacy web browsers. 446 | * `memory` - An in-memory history implementation, useful in testing and non-DOM environments like React Native. 447 | 448 | For more information, check out the [history](https://github.com/ReactTraining/history) package. 449 | 450 | #### * `options.middlewares` 451 | 452 | * Default: `[]` 453 | 454 | Specifies a list of [Redux middleware](http://redux.js.org/docs/advanced/Middleware.html). 455 | 456 | This option is useful if you want to use some third party middlewares. In this case, you have to [`connect`](#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) without `mapDispatchToProps` specified to get `props.dispatch` method, so you can dispatch actions manually. 457 | 458 | #### * `options.reducers` 459 | 460 | * Default: `{}` 461 | 462 | Specifies some custom reducers. Reducers defined here must be standard Redux reducers, and this will be directly handled by [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html). 463 | 464 | For example, to use [redux-form](https://redux-form.com/), you can add redux-form's reducer as the following: 465 | 466 | ```js 467 | import mirror from 'mirrorx' 468 | import { reducer as formReducer } from 'redux-form' 469 | 470 | mirror.defaults({ 471 | reducers: { 472 | form: formReducer 473 | } 474 | }) 475 | ``` 476 | 477 | ##### Update, not replace 478 | 479 | There's something special about `options.reducers`, that is its `key-value`s will be **merged** into the previous ones instead of replacing them, since you can call `mirror.defaults` multiple times. That's the case when you call it after your app has been [started](#rendercomponent-container-callback), for example: 480 | 481 | ```js 482 | // after this call, your store will hava a standard reducer with namespace 483 | // of `a` 484 | mirror.defaults({ 485 | reducers: { 486 | // standard Redux reducer 487 | a: (state, data) => {} 488 | } 489 | }) 490 | 491 | // ... 492 | 493 | // then somewhere in your app, you can add other standard Redux reducers 494 | mirror.defaults({ 495 | reducers: { 496 | // standard Redux reducer 497 | b: (state, data) => {} 498 | } 499 | }) 500 | ``` 501 | 502 | After the second call, your store will have 2 reducers: `a` and `b`. 503 | 504 | #### * `options.addEffect` 505 | 506 | * Default: `(effects) => (name, handler) => { effects[name] = handler }` 507 | 508 | Presents an option to configure how effects are created. Checkout [mirror-saga](https://github.com/ShMcK/mirror-saga) for more information. 509 | 510 | ### connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) 511 | 512 | `connect` connects your `React` component to your `Redux` store. This is exactly the same [`connect`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) method from [react-redux](https://github.com/reactjs/react-redux). 513 | 514 | You must `connect` your component if it needs the data from your store; but it's not necessary to `connect` if your component only wants to dispatch `Redux` actions, because the `actions` object is accessible everywhere in your app, even your [presentational components](http://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-components). 515 | 516 | > Note: if you `connect` your component without `mapDispatchToProps` specified, then you'll get `props.dispatch`, which gives you the power to use some third party middlewares. This is the only case you should manually call `dispatch` in you component, in other cases, always use methods in `actions` to dispatch actions. 517 | 518 | ### render([component], [container], [callback]) 519 | 520 | `render` is an enhanced [`ReactDOM.render`](https://facebook.github.io/react/docs/react-dom.html#render), it starts your Mirror app. 521 | 522 | It first creates your `Redux` store, then renders your component to DOM using `ReactDOM.render`。**`render` takes exactly the same parameters as `ReactDOM.render` does.** 523 | 524 | You can call `render` multiple times in your app. The first time being called, `render` will create a `Redux` store using all the `reducers` and `effects` you defined through `mirror.model` method. After that, all later calls will [replace your store's reducer](http://redux.js.org/docs/api/Store.html#replaceReducer) and re-render your app. 525 | 526 | What's the point of that? It allows you to inject models dynamically, it's very convenient for code-splitting. 527 | 528 | #### Update models on the fly 529 | 530 | For example, suppose you have an `app.js`: 531 | 532 | ```js 533 | // app.js 534 | 535 | import React from 'react' 536 | import mirror, {actions, connect, render} from 'mirrorx' 537 | 538 | mirror.model({ 539 | name: 'foo', 540 | initialState: 0 541 | }) 542 | 543 | const App = connect(({foo, bar}) => { 544 | return {foo, bar} 545 | })(props => { 546 | return ( 547 |
548 |
{props.foo}
549 |
{props.bar}
550 |
551 | ) 552 | }) 553 | 554 | render(, document.getElementById('root')) 555 | ``` 556 | 557 | After `render`, your app will be rendered as: 558 | 559 | ```html 560 |
561 |
0
562 |
563 |
564 | ``` 565 | 566 | Then, suppose you have an async component/model which can be loaded by tools like [react-loadable](https://github.com/jamiebuilds/react-loadable): 567 | 568 | ```js 569 | // asyncComponent.js 570 | 571 | // inside this async component, you define an "async model" 572 | mirror.model({ 573 | name: 'bar', 574 | initialState: 'state of bar' 575 | }) 576 | ``` 577 | 578 | 579 | ```js 580 | // app.js 581 | 582 | // ... 583 | 584 | // some where in your app, after loading above component and model, 585 | // call `render()` will "register" the async model and re-render your app. 586 | // 587 | // NOTE: the `load` function is NOT a real implementation, it's just pseudo code. 588 | load('ayncComponent.js').then(() => { 589 | mirror.render() 590 | }) 591 | ``` 592 | 593 | **Calling `render` without arguments will re-render your app**. So above code will generate the following `html`: 594 | 595 | ```html 596 |
597 |
0
598 | -
599 | +
state of bar
600 |
601 | ``` 602 | 603 | #### Update standard reducers on the fly 604 | 605 | Plus, after the "async component/model" has been loaded, it's possible to call `mirorr.defaults` to add some standard Redux reducers on the fly: 606 | 607 | 608 | ```js 609 | // app.js 610 | 611 | // NOTE: the `load` function is NOT a real implementation, it's just pseudo code. 612 | load('ayncComponent.js').then(() => { 613 | 614 | // `MyAsyncReducer` will be **merged** into the existed ones, not replace them 615 | mirror.defaults({ 616 | reducers: { 617 | MyAsyncReducer: (state, data) => {}, 618 | // ... 619 | } 620 | }) 621 | 622 | // do the re-render 623 | mirror.render() 624 | }) 625 | ``` 626 | 627 | This is very useful for large apps. 628 | 629 | > Note: it's not recommended to pass `component` and `container` to re-render your app, because React may unmount/mount your app. If you just want to re-render, call `render` without any arguments. 630 | 631 | ### Router 632 | 633 | > Mirror uses [react-router@4.x](https://github.com/ReactTraining/react-router), so if you're from react-router 2.x/3.x, you should checkout the [Migrating from v2/v3 to v4 Guide](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/migrating.md). 634 | 635 | This is an enhanced `Router` component from [react-router](https://github.com/ReactTraining/react-router/tree/master/packages/react-router). The `history` and `store` is automatically passed to `Router`, all you have to do is declare your routes. But if you like, you can also create your own `history` object and pass it as a prop to `Router` component. 636 | 637 | What about props like [`basename`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md#basename-string) or [`getUserConfirmation`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md#getuserconfirmation-func)? Well, Mirror's `Router` handles them all! For a complete list of props `Router` takes, check out [`BrowserRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md), [`HashRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/HashRouter.md) and [`MemoryRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/MemoryRouter.md). 638 | 639 | The following components from `react-router` are also exported by Mirror: 640 | 641 | * [`Route`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md) 642 | * [`Switch`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Switch.md) 643 | * [`Redirect`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Redirect.md) 644 | * [`Link`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/Link.md) 645 | * [`NavLink`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/NavLink.md) 646 | * [`Prompt`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Prompt.md) 647 | * [`withRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md) 648 | 649 | A simple example: 650 | 651 | ```js 652 | import {render, Router, Route, Link} from 'mirrorx' 653 | 654 | // ... 655 | 656 | const App = () => ( 657 |
658 | 665 | 666 |
667 | 668 | 669 | 670 |
671 |
672 | ) 673 | 674 | 675 | render( 676 | 677 | 678 | 679 | , document.getElementById('root')) 680 | ``` 681 | 682 | For more details, checkout the [simple-router example](https://github.com/mirrorjs/mirror/blob/master/examples/simple-router), and [react-router Docs](https://github.com/ReactTraining/react-router/tree/master/packages/react-router). 683 | 684 | ### toReducers() 685 | > Since `1.1.0` 686 | 687 | A method transforms all 'models' defined by [`mirror.model`](#mirrormodelname-initialstate-reducers-effects) to an object of standard Redux reducers, which can be directly handled by [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html). In case you do not want the `render` part of mirrorx, you can use `toReducers` to get the reducer to create your own store by hand. 688 | 689 | For example: 690 | 691 | ```js 692 | import { createStore, combineReducers } from 'redux' 693 | import mirror, { actions } from 'mirrorx' 694 | 695 | mirror.model({ 696 | initialState: 0, 697 | name: 'count', 698 | reducers: { 699 | increment(state) { 700 | return state + 1 701 | }, 702 | decrement(state) { 703 | return state - 1 704 | }, 705 | add(state, data) { 706 | return state + data 707 | } 708 | } 709 | }) 710 | 711 | // `toReducers()` will generate an object whose keys are model names and values are corresponding reducers, 712 | // then combine them by `combineReducers` will create a single reducer to be used to create the store. 713 | const reducer = combineReducers(mirror.toReducers()) 714 | 715 | // create the store 716 | const store = createStore(reducer) 717 | 718 | store.getState() 719 | // 0 720 | 721 | store.dispatch({ type: 'count/increment' }) 722 | store.getState() 723 | // 1 724 | ``` 725 | 726 | But, if you try to dispatch actions by `actions.count.increment()`, an error will occur: 727 | 728 | ```js 729 | // ... 730 | 731 | actions.count.increment() 732 | // Error: You are calling "dispatch" or "getState" without applying mirrorMiddleware! Please create your store with mirrorMiddleware first! 733 | ``` 734 | 735 | In this case you'll have to apply the middleware provided by mirorrx to use [`actions`](#actions), see [below](#middleware) for more details. 736 | 737 | ### middleware 738 | > Since `1.1.0` 739 | 740 | A Redux middleware that makes [`actions`](#actions) and [`effects`](#-effects) possible, it MUST be applied if you want both manually created store and the handy `actions`: 741 | 742 | ```js 743 | import { createStore, applyMiddleware, combineReducers } from 'redux' 744 | import mirror, { actions, middleware } from 'mirrorx' 745 | 746 | mirror.model({ 747 | initialState: 0, 748 | name: 'count', 749 | reducers: { 750 | add(state, data) { 751 | return state + data 752 | } 753 | } 754 | }) 755 | 756 | const reducer = combineReducers(mirror.toReducers()) 757 | 758 | // create the store with middleware applied 759 | const store = createStore(reducer, applyMiddleware(middleware)) 760 | 761 | actions.count.add(10) 762 | store.getState() 763 | // 10 764 | ``` 765 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | #### State management 4 | 5 | The `Redux` store of your app is defined by [`mirror.model`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#mirrormodelname-initialstate-reducers-effects) API, and will be automatically created when the app is [started](#rendering). What `mirror.model` does is create the `state`, `reducers` and `actions`, so you don't have to create them by hand. 6 | 7 | * **Action dispatching** 8 | 9 | Dispatching `Redux` actions is very simple in Mirror, just call a function in the [`actions`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#actions) global object, and your action is dispatched. 10 | 11 | * **Async actions** 12 | 13 | There is no difference between sync actions and [async actions](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#-effects) in the way how they are dispatched, a function call is all you need to do. 14 | 15 | #### Routing 16 | 17 | Mirror follows the exact way how [react-router 4.x](https://github.com/ReactTraining/react-router) does routing, so no new learning cost. The [history](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Router.md#history-object) is automatically handled, so you can focus on routing itself. 18 | 19 | * **Programmatically change location** 20 | 21 | Calling methods in [`actions.routing`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#-actionsrouting) object will change the location. After changing, a `@@router/LOCATION_CHANGE` action will be dispatched. 22 | 23 | > If the routes are nested in the router , and you want to **connect** the component of the routes to listen some change in **store**. You may find that the routes render could not be triggered. The reason is that the nested routes can't find the router context from the parent component. The solution is: 24 | > 25 | > ``` 26 | > import React, { Component } from 'react' 27 | > import { withRouter, connect, render } from 'mirrorx' 28 | > 29 | > 30 | > render( 31 | > 32 | > ,document.getElementById('root')) 33 | > 34 | > // ... 35 | > class App extends Component { 36 | > render () { 37 | > return ( 38 | > ...
39 | > 40 | > 41 | > 42 | > 43 | > 44 | >
45 | > ... 46 | > ) 47 | > } 48 | > } 49 | > 50 | > const Root = withRouter(connect(state => { return {somestate: state.somestate}; })(App)) 51 | > ``` 52 | > 53 | > The same issue which you can also check in [React Router 4 (beta 8) won't render components if using redux connect #4671](https://github.com/ReactTraining/react-router/issues/4671). 54 | 55 | 56 | #### Rendering 57 | 58 | The [`render`](https://github.com/mirrorjs/mirror/blob/master/docs/api.md##rendercomponent-container) API will start your app: create the `Redux` store and render your component to the DOM. Calling `render` after app is started will replace reducer of your store and re-render your component. 59 | 60 | #### Hooks 61 | 62 | A [hook](https://github.com/mirrorjs/mirror/blob/master/docs/api.md#mirrorhookaction-getstate--) is a listener subscribes to every `action` you dispatched. For example, if you want to monitor location changes, you can hook the actions whose type is `@@router/LOCATION_CHANGE` by `mirror.hook`. 63 | -------------------------------------------------------------------------------- /docs/zh/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | * [mirror.model](#mirrormodelname-initialstate-reducers-effects) 4 | * [`name`](#-name) 5 | * [`initialState`](#-initialState) 6 | * [`reducers`](#-reducers) 7 | * [`effects`](#-effects) 8 | * [actions](#actions) 9 | * [mirror.hook](#mirrorhookaction-getstate--) 10 | * [mirror.defaults](#mirrordefaultsoptions) 11 | * [`initialState`](#-optionsinitialstate) 12 | * [`historyMode`](#-optionshistorymode) 13 | * [`middlewares`](#-optionsmiddlewares) 14 | * [`reducers`](#-optionsreducers) 15 | * [`addEffect`](#-optionsaddeffect) 16 | * [connect](#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 17 | * [render](#rendercomponent-container-callback) 18 | * [Router](#router) 19 | * [toReducers](#toreducers) 20 | * [middleware](#middleware) 21 | 22 | ### mirror.model({name, initialState, reducers, effects}) 23 | 24 | `mirror.model` 的作用是创建并注入一个 model,所谓的 model,就是 Redux 的 `state`、`action` 和 `reducer` 的组合。`mirror.model` 会自动创建 `reducer` 和 `action`,然后被用于创建 Redux store。 25 | 26 | 简而言之,`mirror.model` 就是一种组织、管理 Redux 的方式,简单而且强大。 27 | 28 | #### * `name` 29 | 30 | 要创建 model,必须要指定 `name`,且为一个合法字符串。`name` 很好理解,就是 model 的名称,这个名称会用于后面创建的 Redux store 里的命名空间。 31 | 32 | 假设定义了一个这样的 model: 33 | 34 | ```js 35 | import mirror from 'mirrorx' 36 | 37 | mirror.model({ 38 | name: 'app', 39 | }) 40 | ``` 41 | 42 | 那么最后创建的 Redux store 会是这样的结构: 43 | 44 | ```js 45 | // ... 46 | 47 | store.getState() 48 | // {app: null} 49 | ``` 50 | 51 | 可以看到,model 的 `name` 就是其 state 在根 store 下的命名空间(当然,`name` 对全局 `actions` 也非常重要,见下文)。 52 | 53 | 另外,需要注意的是,上面创建的 store,其 `app` 这个 state 的值是 `null`,假如你想要一个不同的、更有意义的值,那么你就需要指定一个 `initialState`。 54 | 55 | > 注意:Mirror 使用了 [react-router-redux](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux),因此你**不可以**使用 `routing` 作为 model 的 name。 56 | 57 | #### * `initialState` 58 | 59 | `initialState` 也很容易理解,表示 model 的初始 state。在创建标准的 `Redux reducer` 时,它就表示这个 reducer 的 `initialState`。 60 | 61 | 这个值不是必需的,而且可以为任意值。如果没有指定 `initialState`,那么它的值就是 `null`。 62 | 63 | 创建 model: 64 | 65 | ```js 66 | import mirror from 'mirrorx' 67 | 68 | mirror.model({ 69 | name: 'app', 70 | + initialState: 0, 71 | }) 72 | ``` 73 | 74 | 得到的 store: 75 | 76 | ```js 77 | store.getState() 78 | // {app: 0} 79 | ``` 80 | 81 | #### * `reducers` 82 | 83 | 84 | Mirror app 所有的 `Redux reducer` 都是在 `reducers` 中定义的,`reducers` 对象中的方法本身会用于创建 `reducer`,方法的名字会用于创建 `action type`。Mirror 的原则是,**一个 reducer 只负责一个 action**,所以你不需要关心你要处理的 action 具体的 type 是什么。 85 | 86 | ```js 87 | -import mirror from 'mirrorx' 88 | +import mirror, {actions} from 'mirrorx' 89 | 90 | mirror.model({ 91 | name: 'app', 92 | initialState: 0, 93 | + reducers: { 94 | + add(state, data) { 95 | + return state + data 96 | + }, 97 | + }, 98 | }) 99 | ``` 100 | 101 | 执行上述代码,Mirror 实际上做了以下 3 件事情: 102 | 103 | 1. 创建一个 [reducer](http://redux.js.org/docs/basics/Reducers.html); 104 | 2. 创建一个 [action type](http://redux.js.org/docs/basics/Actions.html)(本例中是 `app/add`),这个 action 会被上面的 reducer 处理; 105 | 3. 在 `actions.` 上添加一个方法,该方法的名称与 `reducers` 中的方法名完全一致,当调用 `actions.` 中的这个方法时,上面创建的 `action` 会被 dispatch。 106 | 107 | 同时我们也可以看到 model 的 `name` 的另一个用处: 108 | 109 | ```js 110 | // ... 111 | typeof actions.app 112 | // 'object' 113 | 114 | typeof actions.app.add 115 | // 'function' 116 | 117 | actions.app.add(1) 118 | // 等同于: 119 | // dispatch({ 120 | // type: 'app/add', 121 | // data: 1 122 | // }) 123 | 124 | 125 | // ... 126 | store.getState() 127 | // {app: 1} 128 | ``` 129 | 130 | 是的,`name` 的值会成为全局 `actions` 上的一个属性,该属性是一个对象,而且该对象会被添加与 `reducers` 中所有方法名相同的方法。调用这些方法会 dispatch 对应的 action。 131 | 132 | `reducers` 中定义的方法,基本上等同于一个 `Redux reducer`(所以也必须为[纯函数](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#oh-to-be-pure-again)),唯一的区别是参数不同: 133 | 134 | ```js 135 | // Redux reducer 136 | function reduxReducer(state, {type, data}) { 137 | // 返回一个新的 state 138 | } 139 | 140 | // `reducers` 中定义的 reducer 141 | function reducerInReducers(state, data) { 142 | // 返回一个新的 state 143 | } 144 | ``` 145 | 146 | 147 | 对于标准的 `Redux reducer`,函数的第二个参数是 `action` 对象;而 `reducers` 中定义的 reducer,函数的第二个参数是 action data。因为你根本不需要关心 action type。 148 | 149 | 那么调用 `actions.` 上的方法时,应该传什么参数呢?也是 action data。 150 | 151 | ```js 152 | // ... 153 | 154 | // 与手动调用 dispatch 一样,在调用 actions. 上的方法时,不需要传递 state 参数 155 | actions.app.add(100) 156 | ``` 157 | 158 | 所有 model 中的所有 reducer 最后都会合并起来形成一个 `Redux reducer`(使用 Redux 的[`combineReducers`](http://redux.js.org/docs/api/combineReducers.html)),然后用于创建 Redux store。 159 | 160 | > 注意:`reducers` 中的非函数属性会被忽略(`effects` 也一样): 161 | > 162 | > ```js 163 | > import mirror, {actions} from 'mirrorx' 164 | > 165 | > mirror.model({ 166 | > name: 'app', 167 | > reducers: { 168 | > a: 1 169 | > }, 170 | > }) 171 | > 172 | > actions.app // undefined 173 | > ``` 174 | 175 | 176 | #### * `effects` 177 | 178 | 所谓的 `effects` 就是 [Redux 的异步 action(async actions)](http://redux.js.org/docs/advanced/AsyncActions.html)。在函数式编程中,[`effect`](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#side-effects-may-include) 表示所有会与函数外部发生交互的操作。在 Redux 的世界里,异步 action 显然是 `effect`。 179 | 180 | `effect` 不会直接更新 Redux state,通常是在完成某些异步操作(比如 AJAX 请求)之后,再调用其他的“同步 action” 来更新 state。 181 | 182 | 和 `reducers` 对象类似,你在 `effects` 中定义的所有方法都会以相同名称添加到 `actions.` 上,调用这些方法便会调用 `effects` 你定义的那些方法。 183 | 184 | ```js 185 | import mirror, {actions} from 'mirrorx' 186 | 187 | mirror.model({ 188 | name: 'app', 189 | initialState: 0, 190 | reducers: { 191 | add(state, data) { 192 | return state + data 193 | }, 194 | }, 195 | + effects: { 196 | + async myEffect(data, getState) { 197 | + const res = await Promise.resolve(data) 198 | + actions.app.add(res) 199 | + } 200 | + }, 201 | }) 202 | ``` 203 | 204 | 执行上述代码,`actions.app` 就会拥有两个方法:`actions.app.add` 和 `actions.app.myEffect`。 205 | 206 | 调用 `actions.app.myEffect`,就会调用 `effects.myEffect`,简单得不能再简单。 207 | 208 | ```js 209 | // ... 210 | 211 | // 首先,dispatch action: 212 | // dispatch({ 213 | // type: 'app/myEffect', 214 | // data: 10 215 | // }) 216 | // 217 | // 然后,调用函数: 218 | // effects.myEffect(10) 219 | actions.app.myEffect(10) 220 | 221 | // ... 222 | store.getState() 223 | // {app: 10} 224 | ``` 225 | 226 | 就是这样,你只需要调用 Mirror 添加到 `actions.` 上的方法,然后你的异步 action 就被 dispatch 了!如果你具有一定的 Redux 经验,那么你一定使用过 [redux-thunk](https://github.com/gaearon/redux-thunk) 或者 [redux-saga](https://redux-saga.js.org) 之类的 middleware 来处理异步 action,当然,它们很优秀——但它们都没有 Mirror 简单! 227 | 228 | 在 `effects` 中定义的方法接收两个形参: 229 | 230 | * `data` - 调用 `actions.` 上的方法时所传递的 data,可选。 231 | * `getState` - 实际上就是 `store.getState`,返回当前 action 被 dispatch 前的 store 的数据,同样是可选的。 232 | 233 | 不过,当在调用 `actions.` 上的方法时,你只需要传递上面的 `data` 作为实参即可(如果需要的话)。 234 | 235 | **Mirror 强烈推荐使用 `async/await` 来定义 effect**。因为 async 函数会自动返回一个 promise。 236 | 237 | 当然了,你也可以使用直白的 `Promise`: 238 | 239 | ```js 240 | // ... 241 | 242 | effects: { 243 | promisedEffect(data, getState) { 244 | return Promise.resolve(data).then(result => { 245 | // 调用同步 action 246 | }) 247 | } 248 | } 249 | ``` 250 | 251 | 甚至,你还可以使用上古时代的 `callback`(**十分不推荐**): 252 | 253 | ```js 254 | // ... 255 | 256 | effects: { 257 | callbackEffect(data, getState) { 258 | setTimeout(() => { 259 | // 调用同步 action 260 | }, 1000) 261 | } 262 | } 263 | ``` 264 | 265 | 具体使用什么方式定义 effect 不是重点,重点是,你可以以任何你喜欢的方式来处理你的异步操作,而 Mirror 为你提供了简单一致的 API。 266 | 267 | > 注意: `effects` 中定义的 action 的名称,不可以与 `reducers` 中的重复: 268 | > 269 | > ```js 270 | > import mirror, {actions} from 'mirrorx' 271 | > 272 | > // 会抛错! 273 | > mirror.model({ 274 | > name: 'app', 275 | > reducers: { 276 | > add(state, data) { 277 | > } 278 | > }, 279 | > effects: { 280 | > add(data, getState) { 281 | > } 282 | > } 283 | > }) 284 | > ``` 285 | 286 | 287 | ### actions 288 | 289 | `actions` 全局对象包含了 Redux 中的 `action` 和 `reducer`。调用 `actions` 上的方法,将会 dispatch 一个 action,这个 action 会被你在 `mirror.model` 接口中的 `reducers` 和 `effects` 上定义的方法捕获、处理。 290 | 291 | 在 Mirror 中,所有的 action 和 effect 都是自动生成的,而且都有处于特定命名空间下。这就意味着,你无法手动创建 action,更重要的是,**没有必要**。 292 | 293 | 不但不需要手动创建 action,你也不需要手动 dispatch action。如果你想要一个 action 以及处理这个 action 的 reducer,你完全不需要先定义一个 `action type`(或者`action creator`),再定义一个处理它的 `reducer`。根本不用这么麻烦, 你只管往 `reducers` 对象里扔一个 reducer 就好了,剩下的交给 Mirror 处理。 294 | 295 | 这样的好处是,你不需要在不同的文件和目录间跳来跳去去决定到底哪个 action 该由哪个 reducer 来处理了。 296 | 297 | 例如,执行这段代码: 298 | 299 | ```js 300 | actions.app.add(1) 301 | ``` 302 | 303 | 完全等同于这段代码: 304 | 305 | ```js 306 | dispatch({ 307 | type: 'app/add', 308 | data: 1 309 | }) 310 | ``` 311 | 312 | 而且,使用全局的 `actions` 对象来处理 Redux 的 action,不同组件或者模块间的“依赖关系”也非常明显,而且更不易出错: 313 | 314 | 假设有一个 a.js: 315 | 316 | ```js 317 | // a.js 318 | import mirror, {actions} from 'mirrorx' 319 | 320 | mirror.model({ 321 | name: 'a', 322 | initialState: 0, 323 | reducers: { 324 | add(state, data) { 325 | return state + data 326 | }, 327 | }, 328 | }) 329 | ``` 330 | 331 | 还有一个 b.js: 332 | 333 | ```js 334 | // b.js 335 | import mirror, {actions} from 'mirrorx' 336 | 337 | mirror.model({ 338 | name: 'b', 339 | effects: { 340 | async foo(state, data) { 341 | const res = await Promise.resole(data) 342 | // 更新 `a` model 的 state 343 | actions.a.add(data) 344 | }, 345 | }, 346 | }) 347 | ``` 348 | 349 | 可以很清晰地看到,模块 `b` 会更新模块 `a` 中的 state。 350 | 351 | 352 | #### * `actions.routing` 353 | 354 | 如果你的 app 使用了 Mirror 提供的 [`Router`](#router) 组件,那么你会自动得到一个 `actions.routing` 对象。 355 | 356 | 这个对象上有 5 个方法,都是用来更新 location 的: 357 | 358 | * `push(location)` - 往 history 中添加一条记录,并跳转到目标 location。 359 | * `replace(location)` - 替换 hisotry 中当前 location。 360 | * `go` - 往前或者往后跳转 history 中的 location。 361 | * `goForward` - 往前跳转一条 location 记录,等价于 `go(1)`。 362 | * `goBack` - 往后跳转一条 location 记录,等价于 `go(-1)`。 363 | 364 | 事实上,这些方法来自于 [history API](https://github.com/ReactTraining/history/blob/v3/docs/GettingStarted.md#navigation),所以意义和用法完全一致。不过与原生方法不同的是,调用 `actions.routing` 上的这些方法,在更新 location 的同时,你的 routing 与 Redux store 将会保持同步,同时一个 type 为 `@@router/LOCATION_CHANGE` 的 action 会被 dispatch(感谢 [react-router-redux](https://github.com/reactjs/react-router-redux))。 365 | 366 | ```js 367 | import mirror, {actions} from 'mirrorx' 368 | 369 | // ... 370 | 371 | actions.routing.push('/foo/bar') 372 | // => http://example.com/foo/bar 373 | 374 | actions.routing.push({ 375 | pathname: '/foo/bar', 376 | search: '?search=123' 377 | }) 378 | // => http://example.com/foo/bar?search=123 379 | ``` 380 | 381 | 查看 [Location](https://github.com/ReactTraining/history/blob/v3/docs/Location.md#location) 了解更多。 382 | 383 | > 注意:如果你的 app 没有使用 [`Router`](#router),那么 `actions.routing` 将会是 `undefined`。 384 | 385 | ### mirror.hook((action, getState) => {}) 386 | 387 | 这是一个非常强大的接口,能够让你监控每一个 dispatch 出去的 action。 388 | 389 | ```js 390 | import mirror, {actions} from 'mirrorx' 391 | 392 | // ... 393 | 394 | const locationChangeHook = mirror.hook((action, getState) => { 395 | if (action.type === '@@router/LOCATION_CHANGE') { 396 | console.log('Location has just changed') 397 | } 398 | }) 399 | 400 | const countHook = mirror.hook((action, getState) => { 401 | if (getState().app.count === 10) { 402 | console.log('You have just reached 10!') 403 | } 404 | }) 405 | 406 | // 移除 hook 407 | locationChangeHook() 408 | countHook() 409 | ``` 410 | 411 | `mirror.hook` 会返回一个函数,调用该函数将会移除这个 hook。 412 | 413 | ### mirror.defaults(options) 414 | 415 | `mirror.defaults` 是一个相当直观的 API,你可以用它来设置你的 Mirror app 的一些选项。 416 | 417 | #### * `options.initialState` 418 | 419 | * 默认值:`undefined` 420 | 421 | 表示 Redux store 的 [`preloadedState`](http://redux.js.org/docs/api/createStore.html)。 422 | 423 | ```js 424 | mirror.defaults({ 425 | initialState: {app: 1} 426 | }) 427 | 428 | mirror.model({ 429 | name: 'app', 430 | // ... 431 | }) 432 | 433 | // ... 434 | 435 | store.getState() 436 | // {app: 1} 437 | ``` 438 | 439 | #### * `options.historyMode` 440 | 441 | * 默认值: `browser` 442 | 443 | 表示 Router 组件所需的 [history 对象的类型](https://github.com/ReactTraining/history#usage),共有 3 种可选的值: 444 | 445 | * `browser` - 标准的 HTML5 hisotry API。 446 | * `hash` - 针对不支持 HTML5 history API 的浏览器。 447 | * `memory` - history API 的内存实现版本,用于非 DOM 环境。 448 | 449 | 如果想了解更多,请查看 [history](https://github.com/ReactTraining/history)。 450 | 451 | #### * `options.middlewares` 452 | 453 | * 默认值: `[]` 454 | 455 | 用来指定一系列标准的 [Redux middleware](http://redux.js.org/docs/advanced/Middleware.html)。 456 | 457 | 假如你想使用一些第三方的 middleware,那么可以在这个选项中指定。同时,你需要调用 [`connect`](#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 且不传递 `mapDispatchToProps` 来获取 `props.dispatch` 方法,然后手动 dispatch action。 458 | 459 | #### * `options.reducers` 460 | 461 | * Default: `{}` 462 | 463 | 指定一些额外的 reducer。注意这里定义的 reducer 必须为标准的 Redux reducer,这些 reducer 会直接被 [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) 处理。 464 | 465 | 比如,要想在 Mirror app 中使用 [redux-form](https://redux-form.com/),那么你可以按照以下方式将 redux-form 的 reducer 集成进来: 466 | 467 | ```js 468 | import mirror from 'mirrorx' 469 | import { reducer as formReducer } from 'redux-form' 470 | 471 | mirror.defaults({ 472 | reducers: { 473 | form: formReducer 474 | } 475 | }) 476 | ``` 477 | 478 | ##### 更新,而不是替换 479 | 480 | `mirror.defautls` 可以调用多次,那么在后续的调用中,`options.reducers` 对象是被**更新**的,而不是被替换。也就是说,参数 `options.reducers` 中的 `key-value` 会被**合并**到之前的对象上去。例如: 481 | 482 | ```js 483 | // 首次调用,store 中会有一个标准的 reducer 其命名空间为 `a` 484 | mirror.defaults({ 485 | reducers: { 486 | // standard Redux reducer 487 | a: (state, data) => {} 488 | } 489 | }) 490 | 491 | // ... 492 | 493 | // 然后在 app 的某个地方,你可以动态地增加标准 reducer 494 | mirror.defaults({ 495 | reducers: { 496 | // standard Redux reducer 497 | b: (state, data) => {} 498 | } 499 | }) 500 | ``` 501 | 502 | 上述第二次的 `mirror.defaults` 调用,将会导致 store 中有 2 个标准 reducer:`a` 和 `b`。 503 | 504 | 505 | #### * `options.addEffect` 506 | 507 | * Default: `(effects) => (name, handler) => { effects[name] = handler }` 508 | 509 | 自定义指定 `effect` 如何处理,比如要使用 `saga`, 可在这个选项中 `runSaga`。更多信息,可查看 [mirror-saga](https://github.com/ShMcK/mirror-saga) 这个项目。 510 | 511 | ### connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) 512 | 513 | `connect` 接口会将 Redux store 与你的 React 组件绑定起来,这个 `connect` 其实就是 [react-redux](https://github.com/reactjs/react-redux) 的 [`connect`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options),所以意义和用法也都完全一致。 514 | 515 | 和普通的 React app 一样,如果你的组件需要用到 Redux store 的数据,那么你也需要 `connect` 来绑定数据。 516 | 517 | 和普通 React app 不一样的是,如果你的某个组件仅仅需要 dispatch 一些 action,那么你完全不要 `connect`。因为 `actions` 对象是全局的,你可以在任意一个组件(哪怕是[展示型组件](http://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-components))中引用它,并调用 `actions` 上的方法来 dispatch action。 518 | 519 | > 注意:`connect` 过的组件,如果没有指定 `mapDispatchToProps`,那么该组件的 `props` 会有一个 `dispatch` 方法,Mirror 保留了这个逻辑。这样,你就可以通过 [`mirror.defaults`](#mirrordefaultsoptions) 接口指定一些 middleware,然后拿到 dispatch 方法来使用它们。不过,这是唯一你需要手动 dispatch action 的情况,在其他所有情况下,你都应该使用全局 `actions` 上的方法来 dispatch action。 520 | 521 | ### render([component], [container], [callback]) 522 | 523 | Mirror 的 `render` 接口就是加强版的 [`ReactDOM.render`](https://facebook.github.io/react/docs/react-dom.html#render),它会启动并渲染你的 Mirror app。 524 | 525 | `render` 首先会创建 Redux store,然后使用 `ReactDOM.render` 将组件渲染到 DOM 上。`render` 方法的参数与 `ReactDOM.render` 完全一致。 526 | 527 | 你可以在 app 中多次调用 `render`。第一次调用会使用 `mirror.model` 方法中定义的 reducer 和 effect 来创建 store。后续的调用将会 [使用 `replaceReducer` 替换 store 的 reducer](http://redux.js.org/docs/api/Store.html#replaceReducer),并重新渲染整个 app。 528 | 529 | 这样处理的意义是什么呢?就是你可以动态载入 model 了,这对 code-splitting 非常有用。 530 | 531 | #### 动态加载 model 532 | 533 | 举例来说,假如你有一个 `app.js`: 534 | 535 | ```js 536 | // app.js 537 | import React from 'react' 538 | import mirror, {actions, connect, render} from 'mirrorx' 539 | 540 | mirror.model({ 541 | name: 'foo', 542 | initialState: 0 543 | }) 544 | 545 | const App = connect(({foo, bar}) => { 546 | return {foo, bar} 547 | })(props => { 548 | return ( 549 |
550 |
{props.foo}
551 |
{props.bar}
552 |
553 | ) 554 | }) 555 | 556 | render(, document.getElementById('root')) 557 | ``` 558 | 559 | `render` 之后,你的 app 会被渲染成下面这样: 560 | 561 | ```html 562 |
563 |
0
564 |
565 |
566 | ``` 567 | 568 | 然后,假设你又定义一个异步组件/model,可以通过类似 [react-loadable](https://github.com/jamiebuilds/react-loadable) 这样的库加载进来: 569 | 570 | ```js 571 | // asyncComponent.js 572 | 573 | // 在这个异步组件中,定义一个"异步 model" 574 | mirror.model({ 575 | name: 'bar', 576 | initialState: 'state of bar' 577 | }) 578 | ``` 579 | 580 | ```js 581 | // app.js 582 | 583 | // ... 584 | 585 | // 当加载完这个异步组件之后,调用 `render()` 将会“注册”其对应的异步 model, 586 | // 并重新渲染 app 587 | // 588 | // NOTE: 这里的 `load` 函数为伪代码 589 | load('ayncComponent.js').then(() => { 590 | mirror.render() 591 | }) 592 | ``` 593 | 594 | **不传递参数调用 `render` 将会重新渲染你的 app**。所以上述代码将会生成以下 DOM 结构: 595 | 596 | ```html 597 |
598 |
0
599 | -
600 | +
state of bar
601 |
602 | ``` 603 | 604 | #### 动态加载标准 reducer 605 | 606 | 另外,当加载完异步组件/model 之后,还可以通过调用 `mirror.defaults` 的方式更新标准的 Redux reducer: 607 | 608 | ```js 609 | // app.js 610 | 611 | // NOTE: 这里的 `load` 函数为伪代码 612 | load('ayncComponent.js').then(() => { 613 | 614 | // `MyAsyncReducer` 会被**合并**到之前指定的 reducer 中,而非替换它们 615 | mirror.defaults({ 616 | reducers: { 617 | MyAsyncReducer: (state, data) => {}, 618 | // ... 619 | } 620 | }) 621 | 622 | // 重新渲染 623 | mirror.render() 624 | }) 625 | ``` 626 | 627 | 这在大型 app 中非常有用。 628 | 629 | > 注意:Mirror 不建议传递 `component` 和 `container` 参数来重新渲染你的 app,因为这样做可能会导致 React mount/unmount 你的 app。如果你只希望重新渲染,永远不要传递任何参数给 `render`。 630 | 631 | ### Router 632 | 633 | > Mirror 使用的是 [react-router@4.x](https://github.com/ReactTraining/react-router),如果你有 react-router 2.x/3.x 的经验,那么你应该仔细阅读一下 react-router 官方的[迁移指南](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/migrating.md)。 634 | 635 | Mirror 的 `Router` 组件是加强版的 [react-router](https://github.com/ReactTraining/react-router/tree/master/packages/react-router) 的 `Router`。所加强的地方在于,`Redux store` 和 `history` 都自动处理好了,不需要你去做关联,也不需要你去创建 `history` 对象,你只需要关心自己的业务逻辑,定义路由即可。当然,如果你想自己创建一个 `history` 对象,然后通过 prop 传递给 `Router` 组件,也是没有任何问题的。 636 | 637 | 那 [`basename`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md#basename-string) 以及 [`getUserConfirmation`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md#getuserconfirmation-func) 等 props 呢?不用担心,Mirror 的 `Router` 全都能处理它们。你可以查看 [`BrowserRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md)、[`HashRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/HashRouter.md) 和 [`MemoryRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/MemoryRouter.md) 的文档获取更多信息。 638 | 639 | 因为 Mirror 没有将 `Router` 用到的 `history` 暴露出去,如果你需要手动更新 location,那么你可以使用 `actions.routing` 上的方法。 640 | 641 | 以下这些组件,都来自 `react-router`,Mirror 也都暴露出去了,你可以直接引入: 642 | 643 | * [`Route`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md) 644 | * [`Switch`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Switch.md) 645 | * [`Redirect`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Redirect.md) 646 | * [`Link`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/Link.md) 647 | * [`NavLink`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/NavLink.md) 648 | * [`Prompt`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Prompt.md) 649 | * [`withRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md) 650 | 651 | 一个简单的例子: 652 | 653 | ```js 654 | import {render, Router, Route, Link} from 'mirrorx' 655 | 656 | // ... 657 | 658 | const App = () => ( 659 |
660 | 667 | 668 |
669 | 670 | 671 | 672 |
673 |
674 | ) 675 | 676 | 677 | render( 678 | 679 | 680 | 681 | , document.getElementById('root')) 682 | ``` 683 | 684 | 想了解更多 `Router` 相关的信息,你可以查看 Mirror 提供的 [simple-router 示例项目](https://github.com/mirrorjs/mirror/blob/master/examples/simple-router),还有 [react-router 官方文档](https://github.com/ReactTraining/react-router/tree/master/packages/react-router)。 685 | 686 | ### toReducers() 687 | > Since `1.1.0` 688 | 689 | 一个方法,将当前所有通过 [`mirror.model`](#mirrormodelname-initialstate-reducers-effects) 定义的 "model" 转换为一个包含所有 reducer 的对象,该对象可直接用于 [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html)。如果你不想使用 mirrorx 提供的 `render` 方法,不想由 mirrorx 完全控制你的 store,那么 `toReducers` 就派上用场了。 690 | 691 | 例如: 692 | 693 | ```js 694 | import { createStore, combineReducers } from 'redux' 695 | import mirror, { actions } from 'mirrorx' 696 | 697 | mirror.model({ 698 | initialState: 0, 699 | name: 'count', 700 | reducers: { 701 | increment(state) { 702 | return state + 1 703 | }, 704 | decrement(state) { 705 | return state - 1 706 | }, 707 | add(state, data) { 708 | return state + data 709 | } 710 | } 711 | }) 712 | 713 | // `toReducers()` 会返回一个对象,对象的 key 是 model 的 name,value 是其对应的 reducer, 714 | // 再通过 `combineReducers` 将对象转换为一个标准 reducer 715 | const reducer = combineReducers(mirror.toReducers()) 716 | 717 | // 创建 store 718 | const store = createStore(reducer) 719 | 720 | store.getState() 721 | // 0 722 | 723 | store.dispatch({ type: 'count/increment' }) 724 | store.getState() 725 | // 1 726 | ``` 727 | 728 | 但是,此时如果你想通过 `actions.count.increment()` 的方式来 dispatch action,将会抛错: 729 | 730 | ```js 731 | // ... 732 | 733 | actions.count.increment() 734 | // Error: You are calling "dispatch" or "getState" without applying mirrorMiddleware! Please create your store with mirrorMiddleware first! 735 | ``` 736 | 抛错的原因是创建 store 时没有使用 mirrorx 提供的 middleware,也就是说,必须使用 middleware,[`actions`](#actions) 才会生效,参看 [下文的详细解释](#middleware)。 737 | 738 | ### middleware 739 | > Since `1.1.0` 740 | 741 | 一个 Redux middleware,它是 [`actions`](#actions) 和 [`effects`](#-effects) 能够工作的原因。如果你同时想自己创建 store 和使用方便的 `actions`,那么**必须**要应用此 `middleware`(通过 `applyMiddleware`): 742 | 743 | ```js 744 | import { createStore, applyMiddleware, combineReducers } from 'redux' 745 | import mirror, { actions, middleware } from 'mirrorx' 746 | 747 | mirror.model({ 748 | initialState: 0, 749 | name: 'count', 750 | reducers: { 751 | add(state, data) { 752 | return state + data 753 | } 754 | } 755 | }) 756 | 757 | const reducer = combineReducers(mirror.toReducers()) 758 | 759 | // 应用 middleware 760 | const store = createStore(reducer, applyMiddleware(middleware)) 761 | 762 | actions.count.add(10) 763 | store.getState() 764 | // 10 765 | -------------------------------------------------------------------------------- /docs/zh/guide.md: -------------------------------------------------------------------------------- 1 | # 指南 2 | 3 | #### 状态管理 4 | 5 | 在 Mirror 中,app 的 Redux store 是由 [`mirror.model`](https://github.com/mirrorjs/mirror/blob/master/docs/zh/api.md#mirrormodelname-initialstate-reducers-effects) 接口定义的,而且 store 将会在[启动 app](#启动和渲染) 的时候自动创建。`mirror.model` 所做的事情就是创建 `state`、`reducer` 和 `action`,所以你不需要手动创建它们。 6 | 7 | * **如何 dispatch action** 8 | 9 | dispatch action 异常简单,你完全不需要到处手动调用 Redux 的 `dispatch` 方法,只需要调用一个 [`actions`](https://github.com/mirrorjs/mirror/blob/master/docs/zh/api.md#actions) 全局对象上的方法就能 dispatch 一个 action 了。 10 | 11 | * **异步 action 的处理** 12 | 13 | 不管是同步 action,还是[异步 action](https://github.com/mirrorjs/mirror/blob/master/docs/zh/api.md#-effects),对开发者来说,这两者的处理方式是一样的,都是通过调用 `actions` 对象上的方法来 dispatch。 14 | 15 | 16 | #### 路由 17 | 18 | Mirror 完全按照 [react-router 4.x](https://github.com/ReactTraining/react-router) 的接口和方式定义路由,因此没有任何新的学习成本。更方便的是,Mirror 的 `Router` 组件,其 [history](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Router.md#history-object) 对象以及跟 `Redux store` 的联结是自动处理过的,所以你完全不用关心它们,只需关心你自己的各个路由即可。 19 | 20 | * **手动更新 location** 21 | 22 | 在全局的 `actions` 上,Mirror 为你提供了一个 [`actions.routing`](https://github.com/mirrorjs/mirror/blob/master/docs/zh/api.md#-actionsrouting) 对象,调用这个对象上的方法,即可手动更新 location,并且与 `store` 同步。location 更新后,一个 type 为 `@@router/LOCATION_CHANGE` 的 action 会被 dispatch。 23 | 24 | > 如果 **route** 组件是 与 **router** 存在嵌套关系,但你的 **route 层的组件** 又 **connect** 了 **store** 订阅了某些 **state**。当你去触发路由跳转的时候,你会发现并没有触发路由的**render**。原因是:一旦子路由被嵌套了,子路由无法获取 router 的 context 上的 props 了。解决方法如下: 25 | > 26 | > ``` 27 | > import React, { Component } from 'react' 28 | > import { withRouter, connect, render } from 'mirrorx' 29 | > 30 | > 31 | > render( 32 | > 33 | > ,document.getElementById('root')) 34 | > 35 | > // ... 36 | > class App extends Component { 37 | > render () { 38 | > return ( 39 | > ...
40 | > 41 | > 42 | > 43 | > 44 | > 45 | >
46 | > ... 47 | > ) 48 | > } 49 | > } 50 | > 51 | > const Root = withRouter(connect(state => { return {somestate: state.somestate}; })(App)) 52 | > ``` 53 | > 54 | > 对应的 issue 可以在参看这里:[React Router 4 (beta 8) won't render components if using redux connect #4671](https://github.com/ReactTraining/react-router/issues/4671)。 55 | 56 | #### 启动和渲染 57 | 58 | 启动一个 Mirror app 的方式也非常简单,调用 [`render`](https://github.com/mirrorjs/mirror/blob/master/docs/zh/api.md##rendercomponent-container) 接口即可。`render` 接口本质上是一个加强版的 `ReactDOM.render`,在渲染组件之前,`render` 会首先创建 Redux store。`render` 可以被多次调用,当 app 启动以后,再次调用 `render` 将会重新渲染你的 app。 59 | 60 | #### Hook 61 | 62 | 可以将 [hook](https://github.com/mirrorjs/mirror/blob/master/docs/zh/api.md#mirrorhookaction-getstate--) 理解为监听每一个被 dispatch 的 action 的 listener,并且这个 listener 是可以随时取消的。假设你希望监控每一次 location 的变化,那么你可以通过 `mirror.hook` 接口去检测 type 为 `@@router/LOCATION_CHANGE` 的 action。 63 | 64 | -------------------------------------------------------------------------------- /examples/counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2", 5 | "react" 6 | ], 7 | "plugins": [ 8 | ["transform-runtime", { 9 | "helpers": false, 10 | "polyfill": false, 11 | "regenerator": true 12 | }] 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | ## Counter 2 | 3 | ### Get started 4 | 5 | ```js 6 | npm i 7 | npm start 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirror-counter-example", 3 | "version": "0.1.0", 4 | "description": "Mirror counter example", 5 | "scripts": { 6 | "start": "webpack-dev-server --open", 7 | "build": "cross-env NODE_ENV=production webpack -p" 8 | }, 9 | "dependencies": { 10 | "babel-runtime": "^6.23.0", 11 | "mirrorx": "^1.0.0", 12 | "prop-types": "^15.5.10", 13 | "react": "^15.0.0", 14 | "react-dom": "^15.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-cli": "^6.14.1", 18 | "babel-core": "^6.14.1", 19 | "babel-loader": "^7.0.0", 20 | "babel-plugin-transform-runtime": "^6.23.0", 21 | "babel-preset-es2015": "^6.14.1", 22 | "babel-preset-react": "^6.14.1", 23 | "babel-preset-stage-2": "^6.14.1", 24 | "cross-env": "^5.0.1", 25 | "css-loader": "^0.28.4", 26 | "style-loader": "^0.18.2", 27 | "webpack": "^2.6.0", 28 | "webpack-dev-server": "^2.4.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mirror - Counter Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/counter/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100vh; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | flex-direction: column; 7 | margin: 0; 8 | } 9 | 10 | #root { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | flex-direction: column; 15 | } 16 | 17 | #counter-app { 18 | width: 300px; 19 | } 20 | 21 | h1 { 22 | font-size: 180px; 23 | margin: 0; 24 | text-align: center; 25 | } 26 | 27 | .btn-wrap { 28 | display: flex; 29 | align-items: center; 30 | justify-content: space-around; 31 | } 32 | 33 | button { 34 | background-color: #fff; 35 | border: 1px solid #999; 36 | border-radius: 2px; 37 | font-size: 120%; 38 | outline: none; 39 | width: 60px; 40 | cursor: pointer; 41 | } 42 | -------------------------------------------------------------------------------- /examples/counter/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import mirror, {actions, connect} from 'mirrorx' 3 | 4 | import './App.css' 5 | 6 | mirror.model({ 7 | name: 'app', 8 | initialState: 0, 9 | reducers: { 10 | increment(state) { 11 | return state + 1 12 | }, 13 | decrement(state) { 14 | return state - 1 15 | } 16 | }, 17 | effects: { 18 | async incrementAsync() { 19 | await new Promise((resolve, reject) => { 20 | setTimeout(() => { 21 | resolve() 22 | }, 1000) 23 | }) 24 | actions.app.increment() 25 | } 26 | } 27 | }) 28 | 29 | const App = props => { 30 | return ( 31 |
32 |

{props.count}

33 |
34 | 35 | 36 | 37 |
38 |
39 | ) 40 | } 41 | 42 | export default connect(state => { 43 | return {count: state.app} 44 | })(App) 45 | -------------------------------------------------------------------------------- /examples/counter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import mirror, {render, Router, Route} from 'mirrorx' 4 | import App from './App' 5 | 6 | render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /examples/counter/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const config = { 5 | // in development, 6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added 7 | entry: [ 8 | './src/index.js' 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'build'), 12 | filename: 'app.js', 13 | publicPath: '/' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?$/, 19 | include: [ 20 | path.resolve(__dirname, 'src') 21 | ], 22 | loader: 'babel-loader' 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | { 28 | loader: 'style-loader', 29 | }, 30 | { 31 | loader: 'css-loader', 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | 39 | if (process.env.NODE_ENV === 'production') { 40 | 41 | config.devtool = 'source-map' 42 | 43 | // Exclude react and react-dom in the production bundle 44 | config.externals = { 45 | 'react': 'React', 46 | 'react-dom': 'ReactDOM' 47 | } 48 | 49 | } else { 50 | 51 | config.devtool = 'cheap-module-source-map' 52 | 53 | config.devServer = { 54 | contentBase: path.resolve(__dirname, 'public'), 55 | clientLogLevel: 'none', 56 | quiet: true, 57 | port: 8000, 58 | compress: true, 59 | hot: true, 60 | historyApiFallback: { 61 | disableDotRule: true 62 | } 63 | } 64 | 65 | // HMR support 66 | config.plugins = [ 67 | new webpack.HotModuleReplacementPlugin(), 68 | new webpack.NamedModulesPlugin() 69 | ] 70 | } 71 | 72 | module.exports = config 73 | -------------------------------------------------------------------------------- /examples/simple-router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2", 5 | "react" 6 | ], 7 | "plugins": [ 8 | ["transform-runtime", { 9 | "helpers": false, 10 | "polyfill": false, 11 | "regenerator": true 12 | }] 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/simple-router/README.md: -------------------------------------------------------------------------------- 1 | ## Simple Router 2 | 3 | ### Get started 4 | 5 | ```js 6 | npm i 7 | npm start 8 | ``` 9 | 10 | ### Tips 11 | 12 | If the routes are nested in the router , and you want to **connect** the component of the routes to listen some change in **store**. You may find that the routes render could not be triggered. The reason is that the nested routes can't find the router context from the parent component. The solution is: 13 | 14 | ``` 15 | import React, { Component } from 'react' 16 | import { withRouter, connect, render } from 'mirrorx' 17 | 18 | 19 | render( 20 | 21 | ,document.getElementById('root')) 22 | 23 | // ... 24 | class App extends Component { 25 | render () { 26 | return ( 27 | ...
28 | 29 | 30 | 31 | 32 | 33 |
34 | ... 35 | ) 36 | } 37 | } 38 | 39 | const Root = withRouter(connect(state => { return {somestate: state.somestate}; })(App)) 40 | ``` 41 | 42 | The same issue which you can also check in [React Router 4 (beta 8) won't render components if using redux connect #4671](https://github.com/ReactTraining/react-router/issues/4671). 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/simple-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirror-router-example", 3 | "version": "0.1.0", 4 | "description": "Mirror router example", 5 | "scripts": { 6 | "start": "webpack-dev-server --open", 7 | "build": "cross-env NODE_ENV=production webpack -p" 8 | }, 9 | "dependencies": { 10 | "mirrorx": "^1.0.0", 11 | "prop-types": "^15.5.10", 12 | "react": "^15.0.0", 13 | "react-dom": "^15.0.0" 14 | }, 15 | "devDependencies": { 16 | "babel-cli": "^6.14.1", 17 | "babel-core": "^6.14.1", 18 | "babel-loader": "^7.0.0", 19 | "babel-plugin-transform-runtime": "^6.23.0", 20 | "babel-preset-es2015": "^6.14.1", 21 | "babel-preset-react": "^6.14.1", 22 | "babel-preset-stage-2": "^6.14.1", 23 | "cross-env": "^5.0.1", 24 | "css-loader": "^0.28.4", 25 | "style-loader": "^0.18.2", 26 | "webpack": "^2.6.0", 27 | "webpack-dev-server": "^2.4.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/simple-router/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mirror - Router Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/simple-router/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route} from 'mirrorx' 3 | 4 | import Header from './components/Header' 5 | import Home from './components/Home' 6 | import About from './components/About' 7 | 8 | import Topics from './containers/Topics' 9 | 10 | const App = () => ( 11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 | ) 21 | 22 | export default App 23 | -------------------------------------------------------------------------------- /examples/simple-router/src/components/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const About = () => ( 4 |
5 |

About

6 |
7 | ) 8 | 9 | export default About 10 | -------------------------------------------------------------------------------- /examples/simple-router/src/components/AddTopic.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {actions} from 'mirrorx' 3 | 4 | const AddTopic = () => { 5 | 6 | let input 7 | 8 | const submit = () => { 9 | if (input.value.trim()) { 10 | actions.topics.add(input.value) 11 | input.value = '' 12 | } 13 | } 14 | 15 | return ( 16 |
17 | input = el}/> 18 | 19 |
20 | ) 21 | } 22 | 23 | export default AddTopic 24 | -------------------------------------------------------------------------------- /examples/simple-router/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'mirrorx' 3 | 4 | const Header = () => ( 5 |
6 | 13 |
14 | ) 15 | 16 | export default Header 17 | -------------------------------------------------------------------------------- /examples/simple-router/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Home = () => ( 4 |
5 |

Home

6 |
7 | ) 8 | 9 | export default Home 10 | -------------------------------------------------------------------------------- /examples/simple-router/src/components/Topic.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {actions} from 'mirrorx' 3 | 4 | const Topic = ({topic}) => ( 5 |
6 |

{topic ? topic : 'Topic not found'}

7 | 8 |
9 | ) 10 | 11 | export default Topic 12 | -------------------------------------------------------------------------------- /examples/simple-router/src/components/Topics.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, Link} from 'mirrorx' 3 | 4 | import Topic from '../components/Topic' 5 | import AddTopic from '../components/AddTopic' 6 | 7 | const Topics = ({topics}) => ( 8 |
9 |

Topics

10 |
    11 | { 12 | topics.map((topic, idx) => ( 13 |
  • 14 | {topic} 15 |
  • 16 | )) 17 | } 18 |
19 | ( 20 | idx == match.params.topicId)}/> 21 | )}/> 22 | 23 |
24 | ) 25 | 26 | export default Topics 27 | -------------------------------------------------------------------------------- /examples/simple-router/src/containers/Topics.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import mirror, {connect} from 'mirrorx' 3 | 4 | import Topics from '../components/Topics' 5 | 6 | mirror.model({ 7 | name: 'topics', 8 | initialState: [ 9 | 'foo' 10 | ], 11 | reducers: { 12 | add(state, topic) { 13 | return [...state, topic] 14 | } 15 | } 16 | }) 17 | 18 | export default connect(({topics}) => ({topics}))(Topics) 19 | -------------------------------------------------------------------------------- /examples/simple-router/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import mirror, {render, Router} from 'mirrorx' 4 | import App from './App' 5 | 6 | mirror.defaults({ 7 | historyMode: 'hash' 8 | }) 9 | 10 | render( 11 | 12 | 13 | 14 | , document.getElementById('root')) 15 | -------------------------------------------------------------------------------- /examples/simple-router/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const config = { 5 | // in development, 6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added 7 | entry: [ 8 | './src/index.js' 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'build'), 12 | filename: 'app.js', 13 | publicPath: '/' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?$/, 19 | include: [ 20 | path.resolve(__dirname, 'src') 21 | ], 22 | loader: 'babel-loader' 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | { 28 | loader: 'style-loader', 29 | }, 30 | { 31 | loader: 'css-loader', 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | 39 | if (process.env.NODE_ENV === 'production') { 40 | 41 | config.devtool = 'source-map' 42 | 43 | // Exclude react and react-dom in the production bundle 44 | config.externals = { 45 | 'react': 'React', 46 | 'react-dom': 'ReactDOM' 47 | } 48 | 49 | } else { 50 | 51 | config.devtool = 'cheap-module-source-map' 52 | 53 | config.devServer = { 54 | contentBase: path.resolve(__dirname, 'public'), 55 | clientLogLevel: 'none', 56 | quiet: true, 57 | port: 8000, 58 | compress: true, 59 | hot: true, 60 | historyApiFallback: { 61 | disableDotRule: true 62 | } 63 | } 64 | 65 | // HMR support 66 | config.plugins = [ 67 | new webpack.HotModuleReplacementPlugin(), 68 | new webpack.NamedModulesPlugin() 69 | ] 70 | } 71 | 72 | module.exports = config 73 | -------------------------------------------------------------------------------- /examples/todo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2", 5 | "react" 6 | ], 7 | "plugins": [ 8 | ["transform-runtime", { 9 | "helpers": false, 10 | "polyfill": false, 11 | "regenerator": true 12 | }] 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/todo/README.md: -------------------------------------------------------------------------------- 1 | ## Todo 2 | 3 | ### Get started 4 | 5 | ```js 6 | npm i 7 | npm start 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirror-todo-example", 3 | "version": "0.1.0", 4 | "description": "Mirror todo example", 5 | "scripts": { 6 | "start": "webpack-dev-server --open", 7 | "build": "cross-env NODE_ENV=production webpack -p" 8 | }, 9 | "dependencies": { 10 | "mirrorx": "^1.0.0", 11 | "prop-types": "^15.5.10", 12 | "react": "^15.0.0", 13 | "react-dom": "^15.0.0" 14 | }, 15 | "devDependencies": { 16 | "babel-cli": "^6.14.1", 17 | "babel-core": "^6.14.1", 18 | "babel-loader": "^7.0.0", 19 | "babel-plugin-transform-runtime": "^6.23.0", 20 | "babel-preset-es2015": "^6.14.1", 21 | "babel-preset-react": "^6.14.1", 22 | "babel-preset-stage-2": "^6.14.1", 23 | "cross-env": "^5.0.1", 24 | "css-loader": "^0.28.4", 25 | "style-loader": "^0.18.2", 26 | "webpack": "^2.6.0", 27 | "webpack-dev-server": "^2.4.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/todo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mirror - Todo Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/todo/src/components/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {actions} from 'mirrorx' 3 | 4 | const AddTodo = () => { 5 | 6 | let input 7 | 8 | return ( 9 |
10 |
{ 12 | e.preventDefault() 13 | if (input.value.trim()) { 14 | actions.todos.add(input.value) 15 | input.value = '' 16 | } 17 | }} 18 | > 19 | input = el}/> 20 | {' '} 21 | 22 |
23 |
24 | ) 25 | } 26 | 27 | export default AddTodo 28 | -------------------------------------------------------------------------------- /examples/todo/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AddTodo from './AddTodo' 3 | import Footer from './Footer' 4 | import TodoList from '../containers/TodoList' 5 | 6 | const App = () => ( 7 |
8 | 9 | 10 |
11 |
12 | ) 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /examples/todo/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from '../containers/Link' 3 | 4 | const Footer = () => ( 5 |
6 | All 7 | {' '} 8 | Active 9 | {' '} 10 | Completed 11 |
12 | ) 13 | 14 | export default Footer 15 | -------------------------------------------------------------------------------- /examples/todo/src/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {actions} from 'mirrorx' 3 | 4 | const Link = ({active, children, filter}) => { 5 | 6 | if (active) { 7 | return {children} 8 | } 9 | 10 | return ( 11 | { 14 | e.preventDefault() 15 | actions.todos.setVisibility(filter) 16 | }} 17 | > 18 | {children} 19 | 20 | ) 21 | } 22 | 23 | export default Link 24 | -------------------------------------------------------------------------------- /examples/todo/src/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Todo = ({text, completed, ...rest}) => ( 4 |
  • 5 | {text} 6 |
  • 7 | ) 8 | 9 | export default Todo 10 | -------------------------------------------------------------------------------- /examples/todo/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {actions} from 'mirrorx' 3 | 4 | import Todo from './Todo' 5 | 6 | const TodoList = ({todos}) => ( 7 |
      8 | { 9 | todos.map(todo => ( 10 | actions.todos.toggle(todo.id)} 14 | /> 15 | )) 16 | } 17 |
    18 | ) 19 | 20 | export default TodoList 21 | -------------------------------------------------------------------------------- /examples/todo/src/containers/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'mirrorx' 3 | import Link from '../components/Link' 4 | 5 | export default connect((state, props) => { 6 | return { 7 | active: state.todos.visibility === props.filter 8 | } 9 | })(Link) 10 | -------------------------------------------------------------------------------- /examples/todo/src/containers/TodoList.js: -------------------------------------------------------------------------------- 1 | import mirror, {connect} from 'mirrorx' 2 | import TodoList from '../components/TodoList' 3 | 4 | let nextId = 0 5 | 6 | mirror.model({ 7 | name: 'todos', 8 | initialState: { 9 | list: [], 10 | visibility: 'all' 11 | }, 12 | reducers: { 13 | add(state, text) { 14 | return { 15 | ...state, 16 | list: [ 17 | ...state.list, 18 | {id: nextId++, text} 19 | ] 20 | } 21 | }, 22 | toggle(state, id) { 23 | return { 24 | ...state, 25 | list: state.list.map(d => { 26 | if (d.id === id) { 27 | d.completed = !d.completed 28 | } 29 | return d 30 | }) 31 | } 32 | }, 33 | setVisibility(state, filter) { 34 | return { 35 | ...state, 36 | visibility: filter 37 | } 38 | } 39 | } 40 | }) 41 | 42 | export default connect(({todos}) => { 43 | return { 44 | todos: getVisibleTodos(todos.list, todos.visibility) 45 | } 46 | })(TodoList) 47 | 48 | function getVisibleTodos(todos, filter) { 49 | switch (filter) { 50 | case 'all': 51 | return todos 52 | case 'completed': 53 | return todos.filter(t => t.completed) 54 | case 'active': 55 | return todos.filter(t => !t.completed) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/todo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from 'mirrorx' 3 | 4 | import App from './components/App' 5 | 6 | render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /examples/todo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const config = { 5 | // in development, 6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added 7 | entry: [ 8 | './src/index.js' 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'build'), 12 | filename: 'app.js', 13 | publicPath: '/' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?$/, 19 | include: [ 20 | path.resolve(__dirname, 'src') 21 | ], 22 | loader: 'babel-loader' 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | { 28 | loader: 'style-loader', 29 | }, 30 | { 31 | loader: 'css-loader', 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | 39 | if (process.env.NODE_ENV === 'production') { 40 | 41 | config.devtool = 'source-map' 42 | 43 | // Exclude react and react-dom in the production bundle 44 | config.externals = { 45 | 'react': 'React', 46 | 'react-dom': 'ReactDOM' 47 | } 48 | 49 | } else { 50 | 51 | config.devtool = 'cheap-module-source-map' 52 | 53 | config.devServer = { 54 | contentBase: path.resolve(__dirname, 'public'), 55 | clientLogLevel: 'none', 56 | quiet: true, 57 | port: 8000, 58 | compress: true, 59 | hot: true, 60 | historyApiFallback: { 61 | disableDotRule: true 62 | } 63 | } 64 | 65 | // HMR support 66 | config.plugins = [ 67 | new webpack.HotModuleReplacementPlugin(), 68 | new webpack.NamedModulesPlugin() 69 | ] 70 | } 71 | 72 | module.exports = config 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirrorx", 3 | "version": "1.1.1", 4 | "description": "A React framework with minimal API and zero boilerplate.", 5 | "scripts": { 6 | "prepublishOnly": "npm run build", 7 | "test": "jest", 8 | "test:watch": "jest --watch", 9 | "test:cov": "jest --coverage", 10 | "coveralls": "cat ./coverage/lcov.info | coveralls", 11 | "lint": "eslint src test", 12 | "build": "rm -rf lib && babel src -d lib --no-comments", 13 | "bundle": "webpack -p" 14 | }, 15 | "main": "./lib/index.js", 16 | "keywords": [ 17 | "framework", 18 | "react", 19 | "redux", 20 | "mirror", 21 | "mirrorx", 22 | "react-router", 23 | "front-end" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:mirrorjs/mirror.git" 28 | }, 29 | "license": "MIT", 30 | "dependencies": { 31 | "history": "^4.10.1", 32 | "react-redux": "^5.1.2", 33 | "react-router": "^4.3.1", 34 | "react-router-dom": "^4.3.1", 35 | "react-router-redux": "^5.0.0-alpha.9", 36 | "redux": "^4.0.5" 37 | }, 38 | "peerDependencies": { 39 | "prop-types": ">=15", 40 | "react": ">=15", 41 | "react-dom": ">=15" 42 | }, 43 | "devDependencies": { 44 | "@babel/cli": "^7.12.10", 45 | "@babel/core": "^7.12.10", 46 | "@babel/plugin-proposal-class-properties": "^7.12.1", 47 | "@babel/plugin-transform-runtime": "^7.12.10", 48 | "@babel/preset-env": "^7.12.11", 49 | "@babel/preset-react": "^7.12.10", 50 | "babel-eslint": "^10.1.0", 51 | "babel-jest": "^25.5.1", 52 | "babel-loader": "^8.2.2", 53 | "coveralls": "^3.1.0", 54 | "eslint": "^6.8.0", 55 | "eslint-plugin-react": "^7.22.0", 56 | "jest": "^26.0.0", 57 | "prop-types": "^15.6.2", 58 | "react": "^16.14.0", 59 | "react-dom": "^16.14.0", 60 | "webpack": "^4.46.0", 61 | "webpack-cli": "^3.3.12" 62 | }, 63 | "files": [ 64 | "lib/", 65 | "docs/", 66 | "README.md", 67 | "LICENSE" 68 | ], 69 | "jest": { 70 | "modulePaths": [ 71 | "/src" 72 | ], 73 | "setupFiles": [ 74 | "/setupTests.js" 75 | ], 76 | "testRegex": "(/test/.*\\.spec\\.js)$", 77 | "testURL": "http://localhost" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | // polyfill `requestAnimationFrame` for React 16 2 | // @see https://github.com/facebook/jest/issues/4545 3 | /* istanbul ignore next */ 4 | global.requestAnimationFrame = callback => { 5 | setTimeout(callback, 0) 6 | } 7 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { dispatch } from './middleware' 2 | import { options } from './defaults' 3 | 4 | const SEP = '/' 5 | 6 | export const actions = {} 7 | 8 | export function addActions(modelName, reducers = {}, effects = {}) { 9 | 10 | if (Object.keys(reducers).length || Object.keys(effects).length) { 11 | actions[modelName] = actions[modelName] || {} 12 | } 13 | 14 | each(reducers, actionName => { 15 | // A single-argument function, whose argument is the payload data of a normal redux action, 16 | // and also the `data` param of corresponding method defined in model.reducers. 17 | actions[modelName][actionName] = actionCreator(modelName, actionName) 18 | }) 19 | 20 | each(effects, effectName => { 21 | if (actions[modelName][effectName]) { 22 | throw new Error(`Action name "${effectName}" has been used! Please select another name as effect name!`) 23 | } 24 | 25 | options.addEffect(`${modelName}${SEP}${effectName}`, effects[effectName]) 26 | 27 | // Effect is like normal action, except it is handled by mirror middleware 28 | actions[modelName][effectName] = actionCreator(modelName, effectName) 29 | // Allow packages to differentiate effects from actions 30 | actions[modelName][effectName].isEffect = true 31 | }) 32 | } 33 | 34 | export function resolveReducers(modelName, reducers = {}) { 35 | return Object.keys(reducers).reduce((acc, cur) => { 36 | acc[`${modelName}${SEP}${cur}`] = reducers[cur] 37 | return acc 38 | }, {}) 39 | } 40 | 41 | function each(obj, callback) { 42 | Object.keys(obj).forEach(callback) 43 | } 44 | 45 | function actionCreator(modelName, actionName) { 46 | return data => ( 47 | dispatch({ 48 | type: `${modelName}${SEP}${actionName}`, 49 | data 50 | }) 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | import { effects, addEffect } from './effects' 2 | 3 | export const options = { 4 | // global initial state 5 | // initialState: undefined, 6 | 7 | // Should be one of ['browser', 'hash', 'memory'] 8 | // Learn more: https://github.com/ReactTraining/history/blob/master/README.md 9 | historyMode: 'browser', 10 | 11 | // A list of the standard Redux middleware 12 | middlewares: [], 13 | 14 | // `options.reducers` will be directly handled by `combineReducers`, 15 | // so reducers defined here must be standard Redux reducer: 16 | // 17 | // reducers: { 18 | // add: (state, action) => {} 19 | // } 20 | // 21 | reducers: {}, 22 | 23 | // An overwrite of the existing effect handler 24 | addEffect: addEffect(effects), 25 | 26 | } 27 | 28 | const historyModes = ['browser', 'hash', 'memory'] 29 | 30 | // Can be called multiple times, ie. after load an async component that has 31 | // exported standard Redux `reducer`s that need to be `replaceReducer` for the 32 | // store. 33 | // 34 | // After the first time called, all later calls will try to *merge* `opts.reducers` 35 | // into the previous `options.reducers`, other keys like `historyMode` will be *updated* 36 | // if it is provided, otherwise it will be ignored, which means the previous values will 37 | // be kept. 38 | export default function defaults(opts = {}) { 39 | 40 | const { 41 | historyMode, 42 | middlewares, 43 | addEffect 44 | } = opts 45 | 46 | if (historyMode && !historyModes.includes(historyMode)) { 47 | throw new Error(`historyMode "${historyMode}" is invalid, must be one of ${historyModes.join(', ')}!`) 48 | } 49 | 50 | if (middlewares && !Array.isArray(middlewares)) { 51 | throw new Error(`middlewares "${middlewares}" is invalid, must be an Array!`) 52 | } 53 | 54 | if (addEffect) { 55 | if (typeof addEffect !== 'function' || typeof addEffect({}) !== 'function') { 56 | throw new Error(`addEffect "${addEffect}" is invalid, must be a function that returns a function`) 57 | } else { 58 | // create effects handler with initial effects object 59 | opts.addEffect = opts.addEffect(effects) 60 | } 61 | } 62 | 63 | Object.keys(opts).forEach(key => { 64 | if (key === 'reducers') { 65 | options[key] = { 66 | ...options[key], 67 | ...opts[key] 68 | } 69 | } else { 70 | options[key] = opts[key] 71 | } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/effects.js: -------------------------------------------------------------------------------- 1 | // Registry of namespaced effects 2 | export const effects = {} 3 | 4 | export const addEffect = effects => (name, handler) => { 5 | effects[name] = handler 6 | } 7 | -------------------------------------------------------------------------------- /src/hook.js: -------------------------------------------------------------------------------- 1 | export const hooks = [] 2 | 3 | export default function hook(subscriber) { 4 | 5 | if (typeof subscriber !== 'function') { 6 | throw new Error('Invalid hook, must be a function!') 7 | } 8 | 9 | hooks.push(subscriber) 10 | 11 | return () => { 12 | hooks.splice(hooks.indexOf(subscriber), 1) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as mirror from './mirror' 2 | 3 | export default mirror 4 | 5 | export * from './mirror' 6 | 7 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | import { effects } from './effects' 2 | import { hooks } from './hook' 3 | 4 | function warning() { 5 | throw new Error( 6 | 'You are calling "dispatch" or "getState" without applying mirrorMiddleware! ' + 7 | 'Please create your store with mirrorMiddleware first!' 8 | ) 9 | } 10 | 11 | export let dispatch = warning 12 | 13 | export let getState = warning 14 | 15 | export default function createMiddleware() { 16 | return middlewareAPI => { 17 | dispatch = middlewareAPI.dispatch 18 | getState = middlewareAPI.getState 19 | 20 | return next => action => { 21 | 22 | let result = next(action) 23 | 24 | if (typeof effects[action.type] === 'function') { 25 | result = effects[action.type](action.data, getState) 26 | } 27 | 28 | hooks.forEach(hook => hook(action, getState)) 29 | 30 | return result 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/mirror.js: -------------------------------------------------------------------------------- 1 | import { Route, Redirect, Switch, Prompt, withRouter } from 'react-router' 2 | import { Link, NavLink } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | import model from './model' 5 | import { actions } from './actions' 6 | import render,{ RootProvider as Provider } from './render' 7 | import hook from './hook' 8 | import Router from './router' 9 | import defaults from './defaults' 10 | import createMiddleware from './middleware' 11 | import toReducers from './toReducers' 12 | 13 | const middleware = createMiddleware() 14 | 15 | export { 16 | model, 17 | actions, 18 | hook, 19 | defaults, 20 | connect, 21 | render, 22 | middleware, 23 | toReducers, 24 | 25 | Provider, 26 | Router, 27 | Route, 28 | Link, 29 | NavLink, 30 | Switch, 31 | Redirect, 32 | Prompt, 33 | withRouter 34 | } 35 | -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | import { resolveReducers, addActions } from './actions' 2 | 3 | const isObject = target => Object.prototype.toString.call(target) === '[object Object]' 4 | 5 | export const models = [] 6 | 7 | export default function model(m) { 8 | const { name, reducers, initialState, effects } = validateModel(m) 9 | 10 | const reducer = getReducer(resolveReducers(name, reducers), initialState) 11 | 12 | const toAdd = { name, reducer } 13 | 14 | models.push(toAdd) 15 | 16 | addActions(name, reducers, effects) 17 | 18 | return toAdd 19 | } 20 | 21 | function validateModel(m = {}) { 22 | const { name, reducers, effects } = m 23 | 24 | if (!name || typeof name !== 'string') { 25 | throw new Error(`Model name must be a valid string!`) 26 | } 27 | 28 | if (name === 'routing') { 29 | throw new Error(`Model name can not be "routing", it is used by react-router-redux!`) 30 | } 31 | 32 | if (models.find(item => item.name === name)) { 33 | throw new Error(`Model "${name}" has been created, please select another name!`) 34 | } 35 | 36 | if (reducers !== undefined && !isObject(reducers)) { 37 | throw new Error(`Model reducers must be a valid object!`) 38 | } 39 | 40 | if (effects !== undefined && !isObject(effects)) { 41 | throw new Error(`Model effects must be a valid object!`) 42 | } 43 | 44 | m.reducers = filterReducers(reducers) 45 | m.effects = filterReducers(effects) 46 | 47 | return m 48 | } 49 | 50 | 51 | // If initialState is not specified, then set it to null 52 | function getReducer(reducers, initialState = null) { 53 | 54 | return (state = initialState, action) => { 55 | if (typeof reducers[action.type] === 'function') { 56 | return reducers[action.type](state, action.data) 57 | } 58 | return state 59 | } 60 | } 61 | 62 | function filterReducers(reducers) { 63 | if (!reducers) { 64 | return reducers 65 | } 66 | 67 | return Object.keys(reducers) 68 | .reduce((acc, action) => { 69 | // Filter out non-function entries 70 | if (typeof reducers[action] === 'function') { 71 | acc[action] = reducers[action] 72 | } 73 | return acc 74 | }, {}) 75 | } 76 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | 5 | import { options } from './defaults' 6 | import { models } from './model' 7 | import { store, createStore, replaceReducer } from './store' 8 | 9 | let started = false 10 | let Root 11 | 12 | export default function render(component, container, callback) { 13 | 14 | const { initialState, middlewares, reducers } = options 15 | 16 | if (started) { 17 | 18 | // If app has rendered, do `store.replaceReducer` to update store. 19 | replaceReducer(store, models, reducers) 20 | 21 | // Call `render` without arguments means *re-render*. Since store has updated, 22 | // `component` will automatically be updated, so no need to `ReactDOM.render` again. 23 | if (arguments.length === 0) { 24 | return Root 25 | } 26 | 27 | } else { 28 | createStore(models, reducers, initialState, middlewares) 29 | } 30 | 31 | // Use named function get a proper displayName 32 | Root = function Root() { 33 | return ( 34 | 35 | {component} 36 | 37 | ) 38 | } 39 | 40 | started = true 41 | 42 | global.document && ReactDOM.render(, container, callback) 43 | 44 | return Root 45 | } 46 | 47 | 48 | export function RootProvider(props) { 49 | const { initialState, middlewares, reducers } = options 50 | if (started) { 51 | replaceReducer(store, models, reducers) 52 | if (arguments.length === 0) { 53 | return 54 | } 55 | } else { 56 | createStore(models, reducers, initialState, middlewares) 57 | } 58 | Root = function Root() { 59 | return ( 60 | 61 | {props.children} 62 | 63 | ) 64 | } 65 | started = true 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { createBrowserHistory, createHashHistory, createMemoryHistory } from 'history' 4 | import { ConnectedRouter, routerActions } from 'react-router-redux' 5 | 6 | import { options } from './defaults' 7 | import { dispatch } from './middleware' 8 | import { actions } from './actions' 9 | 10 | export let history = null 11 | 12 | export default function Router({ history: _history, children, ...others }) { 13 | 14 | // Add `push`, `replace`, `go`, `goForward` and `goBack` methods to actions.routing, 15 | // when called, will dispatch the crresponding action provided by react-router-redux. 16 | actions.routing = Object.keys(routerActions).reduce((memo, action) => { 17 | memo[action] = (...args) => { 18 | dispatch(routerActions[action](...args)) 19 | } 20 | return memo 21 | }, {}) 22 | 23 | // Support for `basename` etc props for Router, 24 | // see https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md 25 | if (!_history) { 26 | _history = createHistory(others) 27 | } 28 | 29 | history = _history 30 | 31 | // ConnectedRouter will use the store from Provider automatically 32 | return ( 33 | 34 | {children} 35 | 36 | ) 37 | } 38 | 39 | Router.propTypes = { 40 | children: PropTypes.element.isRequired, 41 | history: PropTypes.object 42 | } 43 | 44 | function createHistory(props) { 45 | 46 | const { historyMode } = options 47 | 48 | const historyModes = { 49 | browser: createBrowserHistory, 50 | hash: createHashHistory, 51 | memory: createMemoryHistory, 52 | } 53 | 54 | history = historyModes[historyMode](props) 55 | 56 | return history 57 | } 58 | -------------------------------------------------------------------------------- /src/routerMiddleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @credit react-router-redux 3 | * @see https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/middleware.js 4 | * 5 | * This is the routerMiddleware from react-router-redux, but to use 6 | * the global `history` object instead of the passed one. 7 | */ 8 | 9 | import { CALL_HISTORY_METHOD } from 'react-router-redux' 10 | import { history } from './router' 11 | 12 | export default function routerMiddleware() { 13 | return () => next => action => { 14 | if (action.type !== CALL_HISTORY_METHOD) { 15 | return next(action) 16 | } 17 | 18 | const { payload: { method, args } } = action 19 | history[method](...args) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore as _createStore, 3 | applyMiddleware, 4 | combineReducers, 5 | compose 6 | } from 'redux' 7 | import { routerReducer } from 'react-router-redux' 8 | 9 | import createMiddleware from './middleware' 10 | import routerMiddleware from './routerMiddleware' 11 | 12 | export let store 13 | 14 | export function createStore(models, reducers, initialState, middlewares = []) { 15 | 16 | const middleware = applyMiddleware( 17 | routerMiddleware(), 18 | ...middlewares, 19 | createMiddleware() 20 | ) 21 | 22 | const enhancers = [middleware] 23 | 24 | let composeEnhancers = compose 25 | 26 | // Following line to exclude from coverage report: 27 | /* istanbul ignore next */ 28 | if (process.env.NODE_ENV !== 'production') { 29 | // Redux devtools extension support. 30 | if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { 31 | composeEnhancers = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 32 | } 33 | } 34 | 35 | const reducer = createReducer(models, reducers) 36 | const enhancer = composeEnhancers(...enhancers) 37 | 38 | store = _createStore(reducer, initialState, enhancer) 39 | 40 | return store 41 | } 42 | 43 | export function replaceReducer(store, models, reducers) { 44 | const reducer = createReducer(models, reducers) 45 | store.replaceReducer(reducer) 46 | } 47 | 48 | function createReducer(models, reducers) { 49 | 50 | const modelReducers = models.reduce((acc, cur) => { 51 | acc[cur.name] = cur.reducer 52 | return acc 53 | }, {}) 54 | 55 | return combineReducers({ 56 | ...reducers, 57 | ...modelReducers, 58 | routing: routerReducer 59 | }) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/toReducers.js: -------------------------------------------------------------------------------- 1 | import { models } from './model' 2 | 3 | export default function toReducers() { 4 | return models.reduce((acc, cur) => { 5 | acc[cur.name] = cur.reducer 6 | return acc 7 | }, {}) 8 | } 9 | -------------------------------------------------------------------------------- /test/actions.spec.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | jest.resetModules() 3 | }) 4 | 5 | // use jest fake timers 6 | jest.useFakeTimers() 7 | 8 | describe('global actions', () => { 9 | 10 | it('actions should be an empty object', () => { 11 | const { actions } = require('index') 12 | expect(actions).toEqual({}) 13 | }) 14 | 15 | it('addActions should add action', () => { 16 | const { actions } = require('index') 17 | const { addActions } = require('actions') 18 | 19 | addActions('app', { 20 | add(state, data) { 21 | return { ...state, count: state.count + data } 22 | } 23 | }) 24 | 25 | expect(actions.app).toBeInstanceOf(Object) 26 | expect(actions.app.add).toBeInstanceOf(Function) 27 | expect(actions.app.add.length).toBe(1) 28 | }) 29 | 30 | it('mirror.model should add action', () => { 31 | const mirror = require('index') 32 | const { actions } = mirror 33 | 34 | mirror.model({ 35 | name: 'app', 36 | initialState: { 37 | count: 0 38 | }, 39 | reducers: { 40 | add(state, data) { 41 | return { ...state, count: state.count + data } 42 | } 43 | } 44 | }) 45 | 46 | expect(actions.app).toBeInstanceOf(Object) 47 | expect(actions.app.add).toBeInstanceOf(Function) 48 | expect(actions.app.add.length).toBe(1) 49 | }) 50 | 51 | it('throws if call actions without creating store', () => { 52 | const mirror = require('index') 53 | const { actions } = mirror 54 | 55 | mirror.model({ 56 | name: 'app', 57 | initialState: 0, 58 | reducers: { 59 | add(state, data) { 60 | return state + data 61 | } 62 | } 63 | }) 64 | 65 | expect(() => { 66 | actions.app.add(1) 67 | }).toThrow(/create your store with mirrorMiddleware first/) 68 | }) 69 | 70 | it('call actions should dispatch action', () => { 71 | const mirror = require('index') 72 | const { actions } = mirror 73 | const { createStore } = require('store') 74 | 75 | const model = mirror.model({ 76 | name: 'app', 77 | initialState: 0, 78 | reducers: { 79 | add(state, data) { 80 | return state + data 81 | }, 82 | minus(state, data) { 83 | return state - data 84 | } 85 | } 86 | }) 87 | 88 | const store = createStore([model]) 89 | 90 | actions.app.add(1) 91 | expect(store.getState().app).toEqual(1) 92 | 93 | actions.app.minus(1) 94 | expect(store.getState().app).toEqual(0) 95 | }) 96 | 97 | it('should register to global effects object', () => { 98 | const mirror = require('index') 99 | const { effects } = require('effects') 100 | 101 | mirror.model({ 102 | name: 'app', 103 | effects: { 104 | async myEffect() { 105 | } 106 | } 107 | }) 108 | 109 | expect(effects).toBeInstanceOf(Object) 110 | expect(Object.keys(effects)).toEqual(['app/myEffect']) 111 | expect(effects['app/myEffect']).toBeInstanceOf(Function) 112 | }) 113 | 114 | it('should add action by specifying effects', () => { 115 | const mirror = require('index') 116 | const { actions } = mirror 117 | 118 | mirror.model({ 119 | name: 'app', 120 | reducers: { 121 | add(state, data) { 122 | return state + data 123 | } 124 | }, 125 | effects: { 126 | async myEffect(data) { 127 | const res = await Promise.resolve(data) 128 | actions.app.add(res) 129 | } 130 | } 131 | }) 132 | 133 | expect(actions.app.myEffect).toBeInstanceOf(Function) 134 | expect(actions.app.myEffect.length).toBe(1) 135 | }) 136 | 137 | it('async/await style effect actions', async () => { 138 | const mirror = require('index') 139 | const { actions } = mirror 140 | const { createStore } = require('store') 141 | 142 | const model = mirror.model({ 143 | name: 'app', 144 | initialState: 1, 145 | reducers: { 146 | add(state, data) { 147 | return state + data 148 | } 149 | }, 150 | effects: { 151 | async myEffect(data, getState) { 152 | const { app } = getState() 153 | const res = await Promise.resolve(app + data) 154 | // calls the pure reducer 155 | actions.app.add(res) 156 | // could return something too 157 | // return res 158 | } 159 | } 160 | }) 161 | 162 | const store = createStore([model]) 163 | 164 | const res = await actions.app.myEffect(2) 165 | // myEffect returns nothing 166 | expect(res).toBe(undefined) 167 | 168 | expect(store.getState().app).toEqual(4) 169 | }) 170 | 171 | it('callback style effect actions', () => { 172 | const mirror = require('index') 173 | const { actions } = mirror 174 | const { createStore } = require('store') 175 | 176 | const fn = jest.fn() 177 | 178 | const model = mirror.model({ 179 | name: 'app', 180 | initialState: 0, 181 | reducers: { 182 | add(state, data) { 183 | return state + data 184 | } 185 | }, 186 | effects: { 187 | myEffect() { 188 | setTimeout(() => { 189 | actions.app.add(2) 190 | fn() 191 | }, 1000) 192 | } 193 | } 194 | }) 195 | 196 | const store = createStore([model]) 197 | 198 | actions.app.myEffect() 199 | 200 | expect(fn).not.toBeCalled() 201 | 202 | // run the timer 203 | jest.runAllTimers() 204 | 205 | expect(fn).toBeCalled() 206 | 207 | expect(store.getState().app).toEqual(2) 208 | }) 209 | 210 | }) 211 | -------------------------------------------------------------------------------- /test/defaults.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import defaults, { options } from 'defaults' 3 | import { render } from 'index' 4 | import { store } from 'store' 5 | 6 | beforeEach(() => { 7 | jest.resetModules() 8 | }) 9 | 10 | describe('mirror.defaults', () => { 11 | 12 | it('options should be exported', () => { 13 | expect(options).toBeDefined() 14 | }) 15 | 16 | it('should not throw without argument', () => { 17 | expect(() => { 18 | defaults() 19 | }).not.toThrow() 20 | }) 21 | 22 | it('throws if historyMode is invalid', () => { 23 | expect(() => { 24 | defaults({ 25 | historyMode: 'unknown' 26 | }) 27 | }).toThrow(/invalid/) 28 | 29 | expect(() => { 30 | defaults({ 31 | historyMode: 'hash' 32 | }) 33 | }).not.toThrow() 34 | }) 35 | 36 | it('throws if middlewares is not array', () => { 37 | expect(() => { 38 | defaults({ 39 | middlewares: () => {} 40 | }) 41 | }).toThrow(/invalid/) 42 | 43 | expect(() => { 44 | defaults({ 45 | middlewares: [] 46 | }) 47 | }).not.toThrow() 48 | }) 49 | 50 | it('throws if an addEffect is not a function that returns a function', () => { 51 | expect(() => { 52 | defaults({ 53 | addEffect: () => true 54 | }) 55 | }).toThrow(/invalid/) 56 | 57 | expect(() => { 58 | defaults({ 59 | addEffect: () => () => true 60 | }) 61 | }).not.toThrow() 62 | }) 63 | 64 | it('should update `options.reducers` if call defaults multiple times', () => { 65 | defaults({ 66 | reducers: { 67 | a: () => 'a' 68 | } 69 | }) 70 | expect(Object.keys(options.reducers)).toEqual(['a']) 71 | 72 | const container = document.createElement('div') 73 | render(
    , container) 74 | expect(store.getState().a).toBe('a') 75 | 76 | defaults({ 77 | reducers: { 78 | b: () => 'b' 79 | } 80 | }) 81 | expect(Object.keys(options.reducers)).toEqual(['a', 'b']) 82 | 83 | render() 84 | expect(store.getState().b).toBe('b') 85 | 86 | }) 87 | 88 | it('should ignore un-provided values for second and after calls', () => { 89 | defaults({ 90 | reducers: {}, 91 | historyMode: 'hash' 92 | }) 93 | expect(options.historyMode).toBe('hash') 94 | 95 | defaults({}) 96 | expect(options.historyMode).toBe('hash') 97 | }) 98 | 99 | }) 100 | -------------------------------------------------------------------------------- /test/hook.spec.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | jest.resetModules() 3 | }) 4 | 5 | describe('the hook method', () => { 6 | 7 | it('hooks should be an array', () => { 8 | const { hooks } = require('hook') 9 | 10 | expect(hooks).toEqual([]) 11 | }) 12 | 13 | it('throws if hook is not function', () => { 14 | const mirror = require('index') 15 | 16 | expect(() => { 17 | mirror.hook(1) 18 | }).toThrow(/must be a function/) 19 | 20 | expect(() => { 21 | mirror.hook(noop => noop) 22 | }).not.toThrow() 23 | }) 24 | 25 | it('mirror.hook should add hook', () => { 26 | const mirror = require('index') 27 | const { hooks } = require('hook') 28 | 29 | const fn = jest.fn() 30 | 31 | mirror.hook(fn) 32 | 33 | expect(hooks).toEqual([fn]) 34 | }) 35 | 36 | it('dispatch action should call hook', () => { 37 | const mirror = require('index') 38 | const { createStore } = require('store') 39 | const { actions } = mirror 40 | 41 | const fn = jest.fn() 42 | 43 | const model = mirror.model({ 44 | name: 'app', 45 | initialState: 0, 46 | reducers: { 47 | add(state, data) { 48 | return state + data 49 | } 50 | }, 51 | effects: { 52 | async myEffect() {} 53 | } 54 | }) 55 | 56 | createStore([model]) 57 | 58 | mirror.hook(fn) 59 | 60 | expect(fn).not.toBeCalled() 61 | 62 | actions.app.add(1) 63 | 64 | expect(fn).toBeCalled() 65 | }) 66 | 67 | it('call function returned by hook should remove hook', () => { 68 | const mirror = require('index') 69 | const { createStore } = require('store') 70 | const { actions } = mirror 71 | 72 | let log = [] 73 | let state 74 | 75 | const model = mirror.model({ 76 | name: 'app', 77 | initialState: 0, 78 | reducers: { 79 | add(state, data) { 80 | return state + data 81 | } 82 | }, 83 | effects: { 84 | async myEffect() {} 85 | } 86 | }) 87 | 88 | createStore([model]) 89 | 90 | const unhook = mirror.hook((action, getState) => { 91 | if (action.type === 'app/add') { 92 | log.push('add') 93 | } 94 | if (action.type === 'app/myEffect') { 95 | log.push('myEffect') 96 | } 97 | state = getState().app 98 | }) 99 | 100 | actions.app.add(2) 101 | actions.app.myEffect() 102 | 103 | expect(log).toEqual(['add', 'myEffect']) 104 | expect(state).toEqual(2) 105 | 106 | // remove hook 107 | unhook() 108 | 109 | actions.app.add(10) 110 | actions.app.myEffect() 111 | 112 | expect(log).toEqual(['add', 'myEffect']) 113 | expect(state).toEqual(2) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /test/middleware.spec.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import createMiddleware, { dispatch } from 'middleware' 3 | import { effects, addEffect } from 'effects' 4 | 5 | describe('the middleware', () => { 6 | 7 | it('should export dispatch and call effect function when dispatch effect', () => { 8 | 9 | const fn = jest.fn() 10 | 11 | const reducer = (state = 0, action) => { 12 | switch(action.type) { 13 | case 'add': 14 | return state + action.data 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | // register an effect 21 | addEffect(effects)('myEffect', fn) 22 | 23 | const store = createStore(reducer, applyMiddleware(createMiddleware())) 24 | 25 | expect(dispatch).toBeDefined() 26 | 27 | dispatch({ type: 'add', data: 1 }) 28 | 29 | expect(store.getState()).toEqual(1) 30 | 31 | expect(fn).not.toBeCalled() 32 | 33 | dispatch({ type: 'myEffect' }) 34 | 35 | expect(fn).toBeCalled() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/model.spec.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | jest.resetModules() 3 | }) 4 | 5 | 6 | describe('mirror.model', () => { 7 | 8 | it('throws if model name is invalid', () => { 9 | const mirror = require('index') 10 | const errorReg = /Model name must be a valid string/ 11 | 12 | expect(() => { 13 | mirror.model() 14 | }).toThrow(errorReg) 15 | 16 | expect(() => { 17 | mirror.model({ 18 | name: 1 19 | }) 20 | }).toThrow(errorReg) 21 | 22 | expect(() => { 23 | mirror.model({ 24 | name: 'routing' 25 | }) 26 | }).toThrow(/it is used by react-router-redux/) 27 | 28 | expect(() => { 29 | mirror.model({ 30 | name: 'app' 31 | }) 32 | }).not.toThrow() 33 | }) 34 | 35 | it('throws if model name is duplicated', () => { 36 | const mirror = require('index') 37 | 38 | mirror.model({ 39 | name: 'app' 40 | }) 41 | 42 | expect(() => { 43 | mirror.model({ 44 | name: 'app' 45 | }) 46 | }).toThrow(/please select another name/) 47 | }) 48 | 49 | it('models should be an array', () => { 50 | const mirror = require('index') 51 | const { models } = require('model') 52 | 53 | expect(models).toBeInstanceOf(Array) 54 | 55 | const model1 = mirror.model({ 56 | name: 'model1' 57 | }) 58 | 59 | expect(models).toEqual([model1]) 60 | 61 | const model2 = mirror.model({ 62 | name: 'model2' 63 | }) 64 | 65 | expect(models).toEqual([model1, model2]) 66 | }) 67 | 68 | it('throws if reducers is invalid', () => { 69 | const mirror = require('index') 70 | const errorReg = /Model reducers must be a valid object/ 71 | 72 | expect(() => { 73 | mirror.model({ 74 | name: 'app', 75 | reducers: false 76 | }) 77 | }).toThrow(errorReg) 78 | 79 | expect(() => { 80 | mirror.model({ 81 | name: 'app', 82 | reducers: [] 83 | }) 84 | }).toThrow(errorReg) 85 | 86 | expect(() => { 87 | mirror.model({ 88 | name: 'app', 89 | reducers: () => {} 90 | }) 91 | }).toThrow(errorReg) 92 | 93 | expect(() => { 94 | mirror.model({ 95 | name: 'app', 96 | reducers: {} 97 | }).not.toThrow() 98 | }) 99 | }) 100 | 101 | it('throws if effects is invalid', () => { 102 | const mirror = require('index') 103 | const errorReg = /Model effects must be a valid object/ 104 | 105 | expect(() => { 106 | mirror.model({ 107 | name: 'app', 108 | effects: false 109 | }) 110 | }).toThrow(errorReg) 111 | 112 | expect(() => { 113 | mirror.model({ 114 | name: 'app', 115 | effects: [] 116 | }) 117 | }).toThrow(errorReg) 118 | 119 | expect(() => { 120 | mirror.model({ 121 | name: 'app', 122 | effects: () => {} 123 | }) 124 | }).toThrow(errorReg) 125 | 126 | expect(() => { 127 | mirror.model({ 128 | name: 'app', 129 | reducers: {} 130 | }).not.toThrow() 131 | }) 132 | }) 133 | 134 | it('do not add actions if reducers and effects are empty', () => { 135 | const mirror = require('index') 136 | const { actions } = mirror 137 | 138 | mirror.model({ 139 | name: 'model1' 140 | }) 141 | 142 | expect(actions).toEqual({}) 143 | 144 | mirror.model({ 145 | name: 'model2', 146 | reducers: {} 147 | }) 148 | 149 | expect(actions).toEqual({}) 150 | 151 | mirror.model({ 152 | name: 'model3', 153 | effects: {} 154 | }) 155 | 156 | expect(actions).toEqual({}) 157 | 158 | mirror.model({ 159 | name: 'model4', 160 | effects: {}, 161 | reducers: {} 162 | }) 163 | 164 | expect(actions).toEqual({}) 165 | }) 166 | 167 | it('throws if effect name is duplicated with action name', () => { 168 | const mirror = require('index') 169 | 170 | expect(() => { 171 | mirror.model({ 172 | name: 'app', 173 | reducers: { 174 | add(state, data) { 175 | return state + data 176 | } 177 | }, 178 | effects: { 179 | async add() {} 180 | } 181 | }) 182 | }).toThrow(/Please select another name as effect name/) 183 | }) 184 | 185 | it('should ignore non-function entries in reducers and effects', () => { 186 | const mirror = require('index') 187 | const { actions } = mirror 188 | 189 | const fn = () => {} 190 | 191 | mirror.model({ 192 | name: 'model1', 193 | reducers: { 194 | a: 1 195 | } 196 | }) 197 | 198 | expect(actions).toEqual({}) 199 | 200 | mirror.model({ 201 | name: 'model2', 202 | effects: { 203 | b: 'b' 204 | } 205 | }) 206 | 207 | expect(actions).toEqual({}) 208 | 209 | mirror.model({ 210 | name: 'model3', 211 | reducers: { 212 | a: 1, 213 | add: fn 214 | }, 215 | effects: { 216 | b: 'b', 217 | plus: fn 218 | } 219 | }) 220 | 221 | expect(actions.model3).toBeInstanceOf(Object) 222 | expect(Object.keys(actions.model3)).toEqual(['add', 'plus']) 223 | }) 224 | }) 225 | -------------------------------------------------------------------------------- /test/provider.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import mirror, { actions, Provider, connect } from 'index' 4 | 5 | 6 | describe('the Provider Component Wrapper', () => { 7 | 8 | 9 | it('should connect and render', () => { 10 | 11 | const container = document.createElement('div') 12 | 13 | mirror.model({ 14 | name: 'app', 15 | initialState: { 16 | count: 1 17 | }, 18 | reducers: { 19 | increment(state) { 20 | return { ...state, count: state.count + 1 } 21 | } 22 | } 23 | }) 24 | 25 | /* eslint react/prop-types: 0 */ 26 | const Comp = props =>
    {props.count}
    27 | 28 | const App = connect(({ app }) => app)(Comp) 29 | 30 | ReactDOM.render(, container) 31 | 32 | const app = container.querySelector('#app') 33 | 34 | expect(app.textContent).toEqual('1') 35 | 36 | // call the action 37 | actions.app.increment() 38 | 39 | expect(app.textContent).toEqual('2') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/render.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import mirror, { actions, render, connect } from 'index' 3 | import { store } from 'store' 4 | 5 | 6 | describe('the render function', () => { 7 | 8 | it('should create the store', () => { 9 | 10 | const container = document.createElement('div') 11 | 12 | mirror.model({ 13 | name: 'foo', 14 | initialState: { 15 | count: 0 16 | } 17 | }) 18 | 19 | render(
    , container) 20 | 21 | expect(store).toBeDefined() 22 | expect(store.getState).toBeInstanceOf(Function) 23 | expect(store.getState().foo).toEqual({ count: 0 }) 24 | }) 25 | 26 | it('should connect and render', () => { 27 | 28 | const container = document.createElement('div') 29 | 30 | mirror.model({ 31 | name: 'app', 32 | initialState: { 33 | count: 1 34 | }, 35 | reducers: { 36 | increment(state) { 37 | return { ...state, count: state.count + 1 } 38 | } 39 | } 40 | }) 41 | 42 | /* eslint react/prop-types: 0 */ 43 | const Comp = props =>
    {props.count}
    44 | 45 | const App = connect(({ app }) => app)(Comp) 46 | 47 | render(, container) 48 | 49 | const app = container.querySelector('#app') 50 | 51 | expect(app.textContent).toEqual('1') 52 | 53 | // call the action 54 | actions.app.increment() 55 | 56 | expect(app.textContent).toEqual('2') 57 | }) 58 | 59 | it('should inject models dynamically', () => { 60 | 61 | const container = document.createElement('div') 62 | 63 | mirror.model({ 64 | name: 'model1', 65 | initialState: { 66 | count: 0 67 | } 68 | }) 69 | 70 | render(
    , container) 71 | 72 | expect(store.getState().model1).toEqual({ count: 0 }) 73 | 74 | // create another model 75 | mirror.model({ 76 | name: 'model2', 77 | initialState: { 78 | foo: 'foo' 79 | } 80 | }) 81 | 82 | // re-render 83 | render() 84 | 85 | expect(store.getState().model1).toEqual({ count: 0 }) 86 | expect(store.getState().model2).toEqual({ foo: 'foo' }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/router.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import mirror, { actions, render, Router } from 'index' 4 | import { history } from 'router' 5 | 6 | beforeEach(() => { 7 | jest.resetModules() 8 | }) 9 | 10 | describe('the enhanced Router', () => { 11 | 12 | let rootContext 13 | 14 | const ContextChecker = (props, context) => { 15 | rootContext = context 16 | return null 17 | } 18 | 19 | ContextChecker.contextTypes = { 20 | router: PropTypes.shape({ 21 | history: PropTypes.object, 22 | route: PropTypes.object 23 | }) 24 | } 25 | 26 | afterEach(() => { 27 | rootContext = undefined 28 | }) 29 | 30 | 31 | it('should pass history to Router', () => { 32 | 33 | const container = document.createElement('div') 34 | 35 | render( 36 | 37 | 38 | , 39 | container 40 | ) 41 | 42 | expect(rootContext.router.history).toBe(history) 43 | }) 44 | 45 | it('should add navigation methods of history object to actions.routing', () => { 46 | 47 | const container = document.createElement('div') 48 | 49 | mirror.defaults({ 50 | historyMode: 'hash' 51 | }) 52 | 53 | render( 54 | 55 | 56 | , 57 | container 58 | ) 59 | 60 | expect(actions.routing).toBeDefined() 61 | expect(actions.routing.push).toBeInstanceOf(Function) 62 | expect(actions.routing.replace).toBeInstanceOf(Function) 63 | expect(actions.routing.go).toBeInstanceOf(Function) 64 | expect(actions.routing.goForward).toBeInstanceOf(Function) 65 | expect(actions.routing.goBack).toBeInstanceOf(Function) 66 | }) 67 | 68 | it('should change history when call methods in actions.routing', () => { 69 | 70 | const container = document.createElement('div') 71 | 72 | mirror.defaults({ 73 | historyMode: 'memory' 74 | }) 75 | 76 | render( 77 | 78 | 79 | , 80 | container 81 | ) 82 | 83 | expect(rootContext.router.route.match.isExact).toBe(true) 84 | 85 | actions.routing.push('/new') 86 | 87 | expect(rootContext.router.route.match.isExact).toBe(false) 88 | }) 89 | 90 | it('should be ok if pass an history object', () => { 91 | 92 | const createHashHistory = require('history').createHashHistory 93 | const _history = createHashHistory() 94 | 95 | const container = document.createElement('div') 96 | 97 | render( 98 | 99 | 100 | , 101 | container 102 | ) 103 | 104 | expect(rootContext.router.history).toBe(_history) 105 | 106 | expect(rootContext.router.route.match.isExact).toBe(true) 107 | actions.routing.push('/new') 108 | expect(rootContext.router.route.match.isExact).toBe(false) 109 | }) 110 | 111 | }) 112 | -------------------------------------------------------------------------------- /test/store.spec.js: -------------------------------------------------------------------------------- 1 | // Test for global exported store object 2 | import { store as _store, createStore } from 'store' 3 | 4 | beforeEach(() => { 5 | jest.resetModules() 6 | }) 7 | 8 | describe('create store', () => { 9 | 10 | it('should create a redux store', () => { 11 | const mirror = require('index') 12 | 13 | const model = mirror.model({ 14 | name: 'app', 15 | initialState: { 16 | count: 0 17 | }, 18 | reducers: { 19 | add(state, data) { 20 | return { ...state, count: state.count + data } 21 | } 22 | } 23 | }) 24 | 25 | const store = createStore([model]) 26 | 27 | expect(store).toBeDefined() 28 | expect(store.getState).toBeInstanceOf(Function) 29 | expect(store.getState().app).toEqual({ count: 0 }) 30 | }) 31 | 32 | it('exported store should be the created store', () => { 33 | const mirror = require('index') 34 | 35 | const model = mirror.model({ 36 | name: 'app', 37 | reducers: { 38 | id(state) { 39 | return state 40 | } 41 | } 42 | }) 43 | 44 | const store = createStore([model]) 45 | 46 | expect(_store).toBe(store) 47 | }) 48 | 49 | it('initialState should be null if not specified', () => { 50 | const mirror = require('index') 51 | 52 | const model = mirror.model({ 53 | name: 'app', 54 | reducers: { 55 | id(state) { 56 | return state 57 | } 58 | } 59 | }) 60 | 61 | const store = createStore([model]) 62 | 63 | expect(store.getState().app).toEqual(null) 64 | }) 65 | 66 | it('should update redux store by raw dispatch', () => { 67 | const mirror = require('index') 68 | 69 | const model = mirror.model({ 70 | name: 'app', 71 | initialState: { 72 | count: 0 73 | }, 74 | reducers: { 75 | add(state, data) { 76 | return { ...state, count: state.count + data } 77 | } 78 | } 79 | }) 80 | 81 | const store = createStore([model]) 82 | 83 | store.dispatch({ 84 | type: 'app/add', 85 | data: 1 86 | }) 87 | 88 | expect(store.getState().app).toEqual({ count: 1 }) 89 | }) 90 | 91 | }) 92 | -------------------------------------------------------------------------------- /test/toReducers.spec.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux' 2 | 3 | beforeEach(() => { 4 | jest.resetModules() 5 | }) 6 | 7 | // use jest fake timers 8 | jest.useFakeTimers() 9 | 10 | describe('mirror.toReducers', () => { 11 | 12 | it('should return void reducer when no model reducers', () => { 13 | const mirror = require('index') 14 | 15 | expect(mirror.toReducers()).toEqual({}) 16 | 17 | mirror.model({ 18 | name: 'app' 19 | }) 20 | 21 | expect(mirror.toReducers().app).toBeInstanceOf(Function) 22 | }) 23 | 24 | it('should return a standard reducer', () => { 25 | const mirror = require('index') 26 | 27 | mirror.model({ 28 | initialState: 0, 29 | name: 'count', 30 | reducers: { 31 | increment(state) { 32 | return state + 1 33 | }, 34 | decrement(state) { 35 | return state - 1 36 | }, 37 | add(state, data) { 38 | return state + data 39 | } 40 | } 41 | }) 42 | 43 | const reducer = combineReducers(mirror.toReducers()) 44 | 45 | const store = createStore(reducer) 46 | 47 | expect(store).toBeInstanceOf(Object) 48 | expect(store.getState().count).toBe(0) 49 | 50 | store.dispatch({ type: 'count/increment' }) 51 | expect(store.getState().count).toBe(1) 52 | 53 | store.dispatch({ type: 'count/add', data: 10 }) 54 | expect(store.getState().count).toBe(11) 55 | }) 56 | 57 | it('must apply middleware to use actions', () => { 58 | const mirror = require('index') 59 | const { actions, middleware } = mirror 60 | 61 | mirror.model({ 62 | initialState: 0, 63 | name: 'count', 64 | reducers: { 65 | add(state, data) { 66 | return state + data 67 | } 68 | }, 69 | effects: { 70 | addAsync(data) { 71 | setTimeout(() => { 72 | actions.count.add(data) 73 | }, 1000) 74 | } 75 | } 76 | }) 77 | 78 | const reducer = combineReducers(mirror.toReducers()) 79 | 80 | const store = createStore(reducer, applyMiddleware(middleware)) 81 | 82 | expect(store.getState().count).toBe(0) 83 | 84 | actions.count.add(10) 85 | expect(store.getState().count).toBe(10) 86 | 87 | actions.count.addAsync(20) 88 | jest.runAllTimers() 89 | 90 | expect(store.getState().count).toBe(30) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: path.resolve(__dirname, 'dist'), 7 | filename: 'mirror.js', 8 | library: 'Mirror', 9 | libraryTarget: 'umd' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | include: [ 16 | path.resolve(__dirname, 'src') 17 | ], 18 | loader: 'babel-loader' 19 | } 20 | ] 21 | }, 22 | externals: { 23 | 'react': 'React', 24 | 'react-dom': 'ReactDOM', 25 | 'prop-types': 'PropTypes' 26 | } 27 | } 28 | --------------------------------------------------------------------------------