├── examples ├── counter │ ├── .npmrc │ ├── .babelrc │ ├── README.md │ ├── src │ │ ├── models │ │ │ └── counter.js │ │ ├── index.html │ │ └── index.js │ ├── .gitignore │ └── package.json ├── func-test │ ├── .roadhogrc.mock.js │ ├── src │ │ ├── index.css │ │ ├── assets │ │ │ └── yay.jpg │ │ ├── services │ │ │ └── example.js │ │ ├── components │ │ │ └── Example.js │ │ ├── index.js │ │ ├── index.ejs │ │ ├── router.js │ │ ├── routes │ │ │ ├── IndexPage.css │ │ │ └── IndexPage.js │ │ ├── models │ │ │ └── example.js │ │ └── utils │ │ │ └── request.js │ ├── .roadhogrc │ ├── package.json │ └── .eslintrc ├── dynamic │ ├── public │ │ ├── favicon.ico │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── common │ │ │ ├── app.js │ │ │ └── router.js │ │ ├── models │ │ │ ├── counter.js │ │ │ └── todo.js │ │ ├── routes │ │ │ ├── counter.js │ │ │ └── todo.js │ │ └── index.js │ ├── .gitignore │ └── package.json ├── dynamic-run │ ├── public │ │ ├── favicon.ico │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── common │ │ │ ├── app.js │ │ │ └── router.js │ │ ├── models │ │ │ ├── counter.js │ │ │ └── todo.js │ │ ├── routes │ │ │ ├── counter.js │ │ │ └── todo.js │ │ └── index.js │ ├── .gitignore │ └── package.json ├── todomvc │ ├── src │ │ ├── constants │ │ │ ├── TodoFilters.js │ │ │ └── ActionTypes.js │ │ ├── components │ │ │ ├── App.js │ │ │ ├── Link.js │ │ │ ├── TodoList.js │ │ │ ├── App.spec.js │ │ │ ├── MainSection.js │ │ │ ├── Link.spec.js │ │ │ ├── Footer.js │ │ │ ├── TodoTextInput.js │ │ │ ├── TodoList.spec.js │ │ │ ├── TodoItem.js │ │ │ ├── TodoTextInput.spec.js │ │ │ ├── Footer.spec.js │ │ │ ├── MainSection.spec.js │ │ │ └── TodoItem.spec.js │ │ ├── models │ │ │ ├── visibilityFilter.js │ │ │ └── todos.js │ │ ├── index.js │ │ ├── containers │ │ │ ├── FilterLink.js │ │ │ ├── Header.js │ │ │ ├── MainSection.js │ │ │ └── VisibleTodoList.js │ │ └── selectors │ │ │ └── index.js │ ├── .gitignore │ ├── package.json │ ├── public │ │ └── index.html │ └── README.md ├── async │ ├── .gitignore │ ├── src │ │ ├── models │ │ │ ├── selectedSubreddit.js │ │ │ └── postsBySubreddit.js │ │ ├── components │ │ │ ├── Posts.js │ │ │ └── Picker.js │ │ ├── index.js │ │ └── containers │ │ │ └── App.js │ ├── package.json │ ├── public │ │ └── index.html │ └── README.md └── async-loading │ ├── .gitignore │ ├── src │ ├── models │ │ ├── selectedSubreddit.js │ │ └── postsBySubreddit.js │ ├── components │ │ ├── Posts.js │ │ └── Picker.js │ ├── index.js │ └── containers │ │ └── App.js │ ├── package.json │ ├── public │ └── index.html │ └── README.md ├── packages ├── redva-core │ ├── index.js │ ├── src │ │ ├── constants.js │ │ ├── prefixType.js │ │ ├── utils.js │ │ ├── getMutation.js │ │ ├── prefixedDispatch.js │ │ ├── createAsyncMiddleware.js │ │ ├── prefixNamespace.js │ │ ├── subscription.js │ │ ├── createStore.js │ │ ├── getAction.js │ │ ├── checkModel.js │ │ ├── createImmerReducer.js │ │ ├── Plugin.js │ │ └── index.js │ ├── .babelrc │ ├── README.md │ ├── test │ │ ├── plugin.test.js │ │ ├── mutations.test.js │ │ ├── checkModel.test.js │ │ ├── subscriptions.test.js │ │ ├── optsAndHooks.test.js │ │ └── model.test.js │ └── package.json ├── redva │ ├── dynamic.js │ ├── fetch.js │ ├── .babelrc │ ├── fetch.d.ts │ ├── index.js │ ├── README.md │ ├── dynamic.d.ts │ ├── router.js │ ├── router.d.ts │ ├── package.json │ ├── src │ │ ├── dynamic.js │ │ └── index.js │ ├── index.d.ts │ └── test │ │ └── index.test.js └── redva-loading │ ├── .babelrc │ ├── .travis.yml │ ├── README.md │ ├── package.json │ ├── src │ └── index.js │ └── test │ └── index.test.js ├── jest.config.js ├── .travis.yml ├── .gitignore ├── .eslintignore ├── .editorconfig ├── package.json ├── scripts ├── test.js ├── build.js └── publish.js ├── lerna.json ├── rollup.config.js ├── LICENSE ├── .eslintrc ├── docs ├── Concepts_zh-CN.md ├── Concepts.md ├── API_zh-CN.md └── API.md ├── README_zh-CN.md └── README.md /examples/counter/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/func-test/.roadhogrc.mock.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/redva-core/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /packages/redva/dynamic.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/dynamic'); 2 | -------------------------------------------------------------------------------- /packages/redva/fetch.js: -------------------------------------------------------------------------------- 1 | module.exports = require('isomorphic-fetch'); 2 | -------------------------------------------------------------------------------- /packages/redva-core/src/constants.js: -------------------------------------------------------------------------------- 1 | export const NAMESPACE_SEP = '/'; 2 | -------------------------------------------------------------------------------- /examples/func-test/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, body, :global(#root) { 3 | height: 100%; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['packages/**/src/*.{ts,tsx,js,jsx}'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/redva/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env","react","stage-0"], 3 | "plugins":["transform-runtime"] 4 | } -------------------------------------------------------------------------------- /packages/redva-core/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env","react","stage-0"], 3 | "plugins":["transform-runtime"] 4 | } -------------------------------------------------------------------------------- /packages/redva-loading/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env","react","stage-0"], 3 | "plugins":["transform-runtime"] 4 | } -------------------------------------------------------------------------------- /packages/redva/fetch.d.ts: -------------------------------------------------------------------------------- 1 | import * as isomorphicFetch from 'isomorphic-fetch'; 2 | 3 | export = isomorphicFetch; 4 | -------------------------------------------------------------------------------- /examples/counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/dynamic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishedee/redva/HEAD/examples/dynamic/public/favicon.ico -------------------------------------------------------------------------------- /packages/redva/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | module.exports.connect = require('react-redux').connect; 3 | -------------------------------------------------------------------------------- /examples/dynamic-run/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishedee/redva/HEAD/examples/dynamic-run/public/favicon.ico -------------------------------------------------------------------------------- /examples/func-test/src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishedee/redva/HEAD/examples/func-test/src/assets/yay.jpg -------------------------------------------------------------------------------- /packages/redva/README.md: -------------------------------------------------------------------------------- 1 | # redva 2 | 3 | Official React bindings for redva, with react-router@4. 4 | 5 | ## LICENSE 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /packages/redva/dynamic.d.ts: -------------------------------------------------------------------------------- 1 | declare const dynamic: (resolve: (value?: PromiseLike) => void) => void; 2 | export default dynamic; 3 | -------------------------------------------------------------------------------- /packages/redva/router.js: -------------------------------------------------------------------------------- 1 | module.exports = require('react-router-dom'); 2 | module.exports.routerRedux = require('react-router-redux'); 3 | -------------------------------------------------------------------------------- /packages/redva-loading/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "5" 5 | - "6" 6 | 7 | after_success: 8 | - npm run coveralls 9 | -------------------------------------------------------------------------------- /packages/redva/router.d.ts: -------------------------------------------------------------------------------- 1 | import * as routerRedux from 'react-router-redux'; 2 | 3 | export * from 'react-router-dom'; 4 | export { routerRedux }; 5 | -------------------------------------------------------------------------------- /packages/redva-core/README.md: -------------------------------------------------------------------------------- 1 | # redva-core 2 | 3 | The core lightweight library for redva, based on redux and async/await. 4 | 5 | ## LICENSE 6 | 7 | MIT 8 | 9 | -------------------------------------------------------------------------------- /examples/func-test/src/services/example.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export async function query() { 4 | return request('/api/users'); 5 | } 6 | -------------------------------------------------------------------------------- /examples/todomvc/src/constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all'; 2 | export const SHOW_COMPLETED = 'show_completed'; 3 | export const SHOW_ACTIVE = 'show_active'; 4 | -------------------------------------------------------------------------------- /examples/dynamic/src/common/app.js: -------------------------------------------------------------------------------- 1 | import redva from 'redva'; 2 | import createHistory from 'history/createHashHistory'; 3 | 4 | const app = redva({ 5 | history: createHistory(), 6 | }); 7 | export default app; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "7" 5 | - "8" 6 | - "9" 7 | 8 | before_script: 9 | - npm run build 10 | - npm run bootstrap 11 | 12 | after_success: 13 | - npm run coveralls 14 | -------------------------------------------------------------------------------- /examples/dynamic-run/src/common/app.js: -------------------------------------------------------------------------------- 1 | import redva from 'redva'; 2 | import createHistory from 'history/createHashHistory'; 3 | 4 | const app = redva({ 5 | history: createHistory(), 6 | }); 7 | export default app; 8 | -------------------------------------------------------------------------------- /examples/func-test/src/components/Example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Example = () => { 4 | return
Example
; 5 | }; 6 | 7 | Example.propTypes = {}; 8 | 9 | export default Example; 10 | -------------------------------------------------------------------------------- /examples/async/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Redva Counter Sample 2 | 3 | ``` 4 | npm install 5 | npm start 6 | ``` 7 | 8 | or 9 | 10 | ``` 11 | yarn 12 | yarn start 13 | ``` 14 | 15 | Open http://localhost:1234 to view Counter App 16 | --- 17 | -------------------------------------------------------------------------------- /examples/async-loading/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tmp 3 | node_modules 4 | lib 5 | es 6 | dist 7 | coverage 8 | .nyc_output 9 | npm-debug.log* 10 | lerna-debug.log 11 | package-lock.json 12 | .changelog 13 | .next/ 14 | examples/**/.umi 15 | examples/**/.umi-production 16 | 17 | -------------------------------------------------------------------------------- /examples/counter/src/models/counter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'counter', 3 | state: 0, 4 | mutations: { 5 | add(state) { 6 | state.counter += 1; 7 | }, 8 | minus(state) { 9 | state.counter -= 1; 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/async/src/models/selectedSubreddit.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'selectedSubreddit', 3 | state: 'reactjs', 4 | mutations: { 5 | selectSubreddit(state, action) { 6 | state.selectedSubreddit = action.subreddit; 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /examples/async-loading/src/models/selectedSubreddit.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'selectedSubreddit', 3 | state: 'reactjs', 4 | mutations: { 5 | selectSubreddit(state, action) { 6 | state.selectedSubreddit = action.subreddit; 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /examples/dynamic/src/models/counter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'counter', 3 | state: 0, 4 | mutations: { 5 | inc(state, action) { 6 | state.counter += 1; 7 | }, 8 | dec(state, action) { 9 | state.counter -= 1; 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/dynamic-run/src/models/counter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'counter', 3 | state: 0, 4 | mutations: { 5 | inc(state, action) { 6 | state.counter += 1; 7 | }, 8 | dec(state, action) { 9 | state.counter -= 1; 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | es/ 4 | dist/ 5 | packages/dva-example/ 6 | packages/dva-example-react-router-3/ 7 | packages/dva-example-nextjs// 8 | packages/dva-example-user-dashboard/ 9 | packages/dva/*.js 10 | packages/dva-react-router-3/*.js 11 | packages/dva-no-router/*.js 12 | scripts/ 13 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../containers/Header'; 3 | import MainSection from '../containers/MainSection'; 4 | 5 | const App = () => ( 6 |
7 |
8 | 9 |
10 | ); 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /examples/todomvc/src/models/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | import { SHOW_ALL } from '../constants/TodoFilters'; 2 | export default { 3 | namespace: 'visibilityFilter', 4 | state: SHOW_ALL, 5 | mutations: { 6 | SET_VISIBILITY_FILTER(state, action) { 7 | state.visibilityFilter = action.filter; 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /examples/async/src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Posts = ({ posts }) => ( 5 | 6 | ); 7 | 8 | Posts.propTypes = { 9 | posts: PropTypes.array.isRequired, 10 | }; 11 | 12 | export default Posts; 13 | -------------------------------------------------------------------------------- /examples/async-loading/src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Posts = ({ posts }) => ( 5 | 6 | ); 7 | 8 | Posts.propTypes = { 9 | posts: PropTypes.array.isRequired, 10 | }; 11 | 12 | export default Posts; 13 | -------------------------------------------------------------------------------- /examples/func-test/.roadhogrc: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "src/index.js", 3 | "env": { 4 | "development": { 5 | "extraBabelPlugins": [ 6 | "dva-hmr", 7 | "transform-runtime" 8 | ] 9 | }, 10 | "production": { 11 | "extraBabelPlugins": [ 12 | "transform-runtime" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/counter/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | dva-example-redux-redu 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/func-test/src/index.js: -------------------------------------------------------------------------------- 1 | import redva from 'redva'; 2 | import './index.css'; 3 | 4 | // 1. Initialize 5 | const app = redva(); 6 | 7 | // 2. Plugins 8 | // app.use({}); 9 | 10 | // 3. Model 11 | app.model(require('./models/example')); 12 | 13 | // 4. Router 14 | app.router(require('./router')); 15 | 16 | // 5. Start 17 | app.start('#root'); 18 | -------------------------------------------------------------------------------- /examples/todomvc/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO'; 2 | export const DELETE_TODO = 'DELETE_TODO'; 3 | export const EDIT_TODO = 'EDIT_TODO'; 4 | export const COMPLETE_TODO = 'COMPLETE_TODO'; 5 | export const COMPLETE_ALL_TODOS = 'COMPLETE_ALL_TODOS'; 6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'; 7 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'; 8 | -------------------------------------------------------------------------------- /examples/func-test/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redva Demo 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redva-build", 3 | "private": true, 4 | "scripts": { 5 | "build": "node scripts/build.js", 6 | "test": "node scripts/test.js" 7 | }, 8 | "devDependencies": { 9 | "lerna": "^2.11.0" 10 | }, 11 | "lint-staged": { 12 | "*.js": [ 13 | "prettier --trailing-comma es5 --single-quote --write", 14 | "git add" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/dynamic/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/func-test/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, Switch } from 'redva/router'; 3 | import IndexPage from './routes/IndexPage'; 4 | 5 | function RouterConfig({ history }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default RouterConfig; 14 | -------------------------------------------------------------------------------- /examples/dynamic-run/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/dynamic/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/dynamic-run/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/todomvc/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import redva from 'redva'; 3 | import App from './components/App'; 4 | import todos from './models/todos'; 5 | import visibilityFilter from './models/visibilityFilter'; 6 | import 'todomvc-app-css/index.css'; 7 | 8 | const app = redva(); 9 | 10 | app.model(todos); 11 | app.model(visibilityFilter); 12 | 13 | app.router(() => ); 14 | 15 | app.start('#root'); 16 | -------------------------------------------------------------------------------- /examples/counter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # development 25 | .cache 26 | *.map -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const execSync = require("child_process").execSync; 3 | 4 | const exec = (cmd, env) => 5 | execSync(cmd, { 6 | stdio: "inherit", 7 | env: Object.assign({}, process.env, env) 8 | }); 9 | const cwd = process.cwd(); 10 | const packages = ["redva","redva-core","redva-loading"]; 11 | for( const key in packages ){ 12 | const package = packages[key]; 13 | exec("cd "+cwd+"/packages/"+package+" && npm test") 14 | } 15 | 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const execSync = require("child_process").execSync; 3 | 4 | const exec = (cmd, env) => 5 | execSync(cmd, { 6 | stdio: "inherit", 7 | env: Object.assign({}, process.env, env) 8 | }); 9 | const cwd = process.cwd(); 10 | const packages = ["redva","redva-core","redva-loading"]; 11 | for( const key in packages ){ 12 | const package = packages[key]; 13 | exec("cd "+cwd+"/packages/"+package+" && npm run build") 14 | } 15 | 16 | -------------------------------------------------------------------------------- /packages/redva-core/src/prefixType.js: -------------------------------------------------------------------------------- 1 | import { NAMESPACE_SEP } from './constants'; 2 | 3 | export default function prefixType(type, model) { 4 | const prefixedType = `${model.namespace}${NAMESPACE_SEP}${type}`; 5 | const typeWithoutAffix = prefixedType.replace(/\/@@[^/]+?$/, ''); 6 | if ( 7 | (model.mutations && model.mutations[typeWithoutAffix]) || 8 | (model.actions && model.actions[typeWithoutAffix]) 9 | ) { 10 | return prefixedType; 11 | } 12 | return type; 13 | } 14 | -------------------------------------------------------------------------------- /examples/async/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import redva from 'redva'; 3 | import { createLogger } from 'redux-logger'; 4 | import postsBySubreddit from './models/postsBySubreddit'; 5 | import selectedSubreddit from './models/selectedSubreddit'; 6 | import App from './containers/App'; 7 | 8 | const app = redva({ 9 | extraMiddlewares: createLogger(), 10 | }); 11 | 12 | app.model(postsBySubreddit); 13 | app.model(selectedSubreddit); 14 | 15 | app.router(() => ); 16 | app.start('#root'); 17 | -------------------------------------------------------------------------------- /examples/async-loading/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import redva from 'redva'; 3 | import redvaLoading from 'redva-loading'; 4 | import postsBySubreddit from './models/postsBySubreddit'; 5 | import selectedSubreddit from './models/selectedSubreddit'; 6 | import App from './containers/App'; 7 | 8 | const app = redva({ 9 | }); 10 | 11 | app.use(new redvaLoading()); 12 | 13 | app.model(postsBySubreddit); 14 | app.model(selectedSubreddit); 15 | 16 | app.router(() => ); 17 | app.start('#root'); 18 | -------------------------------------------------------------------------------- /packages/redva-core/src/utils.js: -------------------------------------------------------------------------------- 1 | export isPlainObject from 'is-plain-object'; 2 | export const isArray = Array.isArray.bind(Array); 3 | export const isFunction = o => typeof o === 'function'; 4 | export const returnSelf = m => m; 5 | export const noop = () => {}; 6 | export const resetType = (str)=>{ 7 | const strList = str.split("/"); 8 | const newStrList = []; 9 | for( let i in strList ){ 10 | if( strList[i] != ""){ 11 | newStrList.push(strList[i]); 12 | } 13 | } 14 | return newStrList.join("/"); 15 | } 16 | -------------------------------------------------------------------------------- /examples/async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^1.1.4" 7 | }, 8 | "dependencies": { 9 | "prop-types": "^15.6.1", 10 | "react": "^16.3.1", 11 | "react-dom": "^16.3.1", 12 | "redux-logger": "^3.0.6", 13 | "redva":"^1.1.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "eject": "react-scripts eject", 19 | "test": "react-scripts test" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/todomvc/src/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Link from '../components/Link'; 3 | 4 | const mapStateToProps = (state, ownProps) => ({ 5 | active: ownProps.filter === state.visibilityFilter, 6 | }); 7 | 8 | const mapDispatchToProps = (dispatch, ownProps) => ({ 9 | setFilter: () => { 10 | dispatch({ 11 | type: 'visibilityFilter/SET_VISIBILITY_FILTER', 12 | filter: ownProps.filter, 13 | }); 14 | }, 15 | }); 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(Link); 18 | -------------------------------------------------------------------------------- /examples/dynamic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamic", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-polyfill": "^6.26.0", 7 | "history": "^4.7.2", 8 | "react": "^16.4.0", 9 | "react-dom": "^16.4.0", 10 | "react-scripts": "1.1.4", 11 | "redva": "^1.1.1", 12 | "url-polyfill": "^1.0.13" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/dynamic-run/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamic", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-polyfill": "^6.26.0", 7 | "history": "^4.7.2", 8 | "react": "^16.4.0", 9 | "react-dom": "^16.4.0", 10 | "react-scripts": "1.1.4", 11 | "redva": "^1.1.1", 12 | "url-polyfill": "^1.0.13" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/redva-core/src/getMutation.js: -------------------------------------------------------------------------------- 1 | function applyOnMutation(fns, mutation, model, key) { 2 | for (const fn of fns) { 3 | mutation = fn(mutation, model, key); 4 | } 5 | return mutation; 6 | } 7 | 8 | export default function getMutation(reducers, onMutation, model) { 9 | let newHandlers = {}; 10 | reducers = reducers || {}; 11 | for (let key in reducers) { 12 | if (Object.prototype.hasOwnProperty.call(reducers, key)) { 13 | newHandlers[key] = applyOnMutation(onMutation, reducers[key], model, key); 14 | } 15 | } 16 | return newHandlers; 17 | } 18 | -------------------------------------------------------------------------------- /examples/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^1.1.4", 7 | "react-test-renderer": "^16.3.1" 8 | }, 9 | "dependencies": { 10 | "classnames": "^2.2.5", 11 | "prop-types": "^15.6.1", 12 | "react": "^16.3.1", 13 | "react-dom": "^16.3.1", 14 | "redva": "^1.1.1", 15 | "reselect": "^3.0.1", 16 | "todomvc-app-css": "^2.1.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | const Link = ({ active, children, setFilter }) => ( 6 | setFilter()} 10 | > 11 | {children} 12 | 13 | ); 14 | 15 | Link.propTypes = { 16 | active: PropTypes.bool.isRequired, 17 | children: PropTypes.node.isRequired, 18 | setFilter: PropTypes.func.isRequired, 19 | }; 20 | 21 | export default Link; 22 | -------------------------------------------------------------------------------- /examples/func-test/src/routes/IndexPage.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 3em; 5 | text-align: center; 6 | } 7 | 8 | .title { 9 | font-size: 2.5rem; 10 | font-weight: normal; 11 | letter-spacing: -1px; 12 | } 13 | 14 | .welcome { 15 | height: 328px; 16 | background: url(../assets/yay.jpg) no-repeat center 0; 17 | background-size: 388px 328px; 18 | } 19 | 20 | .list { 21 | font-size: 1.2em; 22 | margin-top: 1.8em; 23 | list-style: none; 24 | line-height: 1.5em; 25 | } 26 | 27 | .list code { 28 | background: #f7f7f7; 29 | } 30 | -------------------------------------------------------------------------------- /examples/async-loading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-loading", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^1.1.4" 7 | }, 8 | "dependencies": { 9 | "prop-types": "^15.6.1", 10 | "react": "^16.3.1", 11 | "react-dom": "^16.3.1", 12 | "redux-logger": "^3.0.6", 13 | "redva": "^1.1.1", 14 | "redva-loading": "^1.1.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "eject": "react-scripts eject", 20 | "test": "react-scripts test" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/dynamic/src/common/router.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | import dynamic from 'redva/dynamic'; 3 | 4 | export default [ 5 | { 6 | url: '/counter', 7 | name: '计数器', 8 | component: dynamic({ 9 | app: app, 10 | models: () => [import('../models/counter')], 11 | component: () => import('../routes/counter'), 12 | }), 13 | }, 14 | { 15 | url: '/todo', 16 | name: 'todo列表', 17 | component: dynamic({ 18 | app: app, 19 | models: () => [import('../models/todo')], 20 | component: () => import('../routes/todo'), 21 | }), 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-example-redux-redu", 3 | "private": true, 4 | "scripts": { 5 | "start": "parcel ./src/index.html " 6 | }, 7 | "author": "sjy ", 8 | "devDependencies": { 9 | "babel-preset-env": "^1.6.1", 10 | "babel-preset-react": "^6.24.1", 11 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 12 | "parcel-bundler": "^1.4.1" 13 | }, 14 | "dependencies": { 15 | "redva": "^1.1.1", 16 | "react": "^16.2.0", 17 | "react-dom": "^16.2.0", 18 | "redux-undo": "^0.6.1" 19 | }, 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0", 3 | "changelog": { 4 | "repo": "fishedee/redva", 5 | "labels": { 6 | "new feature": ":rocket: New Feature", 7 | "breaking change": ":boom: Breaking Change", 8 | "bug": ":bug: Bug Fix", 9 | "enhancement": ":nail_care: Enhancement", 10 | "documentation": ":memo: Documentation" 11 | }, 12 | "cacheDir": ".changelog" 13 | }, 14 | "commands": { 15 | "publish": { 16 | "ignore": [ 17 | "redva-example*" 18 | ] 19 | } 20 | }, 21 | "packages": [ 22 | "packages/*" 23 | ], 24 | "version": "independent" 25 | } 26 | -------------------------------------------------------------------------------- /examples/todomvc/src/containers/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'redva'; 3 | import TodoTextInput from '../components/TodoTextInput'; 4 | 5 | export const Header = props => ( 6 |
7 |

todos

8 | { 11 | if (text.length !== 0) { 12 | props.dispatch({ 13 | type: 'todos/ADD_TODO', 14 | text: text, 15 | }); 16 | } 17 | }} 18 | placeholder="What needs to be done?" 19 | /> 20 |
21 | ); 22 | 23 | export default connect()(Header); 24 | -------------------------------------------------------------------------------- /examples/func-test/src/models/example.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'example', 3 | 4 | state: {}, 5 | 6 | subscriptions: { 7 | setup({ dispatch, history }) { 8 | // eslint-disable-line 9 | history.listen(location => { 10 | console.log(1, location); 11 | }); 12 | }, 13 | }, 14 | 15 | actions: { 16 | async fetch({ payload }, { dispatch }) { 17 | // eslint-disable-line 18 | await dispatch({ type: 'save' }); 19 | }, 20 | }, 21 | 22 | mutations: { 23 | save(state, action) { 24 | state.example = { 25 | ...state.example, 26 | ...action.payload, 27 | }; 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /examples/async/src/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Picker = ({ value, onChange, options }) => ( 5 | 6 |

{value}

7 | 14 |
15 | ); 16 | 17 | Picker.propTypes = { 18 | options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, 19 | value: PropTypes.string.isRequired, 20 | onChange: PropTypes.func.isRequired, 21 | }; 22 | 23 | export default Picker; 24 | -------------------------------------------------------------------------------- /examples/async-loading/src/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Picker = ({ value, onChange, options }) => ( 5 | 6 |

{value}

7 | 14 |
15 | ); 16 | 17 | Picker.propTypes = { 18 | options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, 19 | value: PropTypes.string.isRequired, 20 | onChange: PropTypes.func.isRequired, 21 | }; 22 | 23 | export default Picker; 24 | -------------------------------------------------------------------------------- /examples/dynamic/src/models/todo.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'todo', 3 | state: [ 4 | { 5 | id: 1, 6 | text: 'Hello Redva', 7 | }, 8 | ], 9 | mutations: { 10 | add(state, action) { 11 | let maxId = state.todo.reduce( 12 | (maxId, todo) => Math.max(todo.id, maxId), 13 | -1 14 | ); 15 | state.todo.push({ 16 | id: maxId + 1, 17 | text: action.text, 18 | }); 19 | }, 20 | del(state, action) { 21 | let index = state.todo.findIndex(todo => { 22 | return todo.id == action.id; 23 | }); 24 | if (index != -1) { 25 | state.todo.splice(index, 1); 26 | } 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /examples/dynamic-run/src/models/todo.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'todo', 3 | state: [ 4 | { 5 | id: 1, 6 | text: 'Hello Redva', 7 | }, 8 | ], 9 | mutations: { 10 | add(state, action) { 11 | let maxId = state.todo.reduce( 12 | (maxId, todo) => Math.max(todo.id, maxId), 13 | -1 14 | ); 15 | state.todo.push({ 16 | id: maxId + 1, 17 | text: action.text, 18 | }); 19 | }, 20 | del(state, action) { 21 | let index = state.todo.findIndex(todo => { 22 | return todo.id == action.id; 23 | }); 24 | if (index != -1) { 25 | state.todo.splice(index, 1); 26 | } 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import TodoItem from './TodoItem'; 4 | 5 | const TodoList = ({ filteredTodos, actions }) => ( 6 |
    7 | {filteredTodos.map(todo => ( 8 | 9 | ))} 10 |
11 | ); 12 | 13 | TodoList.propTypes = { 14 | filteredTodos: PropTypes.arrayOf( 15 | PropTypes.shape({ 16 | id: PropTypes.number.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired, 19 | }).isRequired 20 | ).isRequired, 21 | actions: PropTypes.object.isRequired, 22 | }; 23 | 24 | export default TodoList; 25 | -------------------------------------------------------------------------------- /examples/async/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Async Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/func-test/src/routes/IndexPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'redva'; 3 | import styles from './IndexPage.css'; 4 | 5 | function IndexPage() { 6 | return ( 7 |
8 |

Yay! Welcome to redva!

9 |
10 |
    11 |
  • 12 | To get started, edit src/index.js and save to reload. 13 |
  • 14 |
  • 15 | Getting Started 16 |
  • 17 |
18 |
19 | ); 20 | } 21 | 22 | IndexPage.propTypes = {}; 23 | 24 | export default connect()(IndexPage); 25 | -------------------------------------------------------------------------------- /examples/async-loading/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Async Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/todomvc/src/containers/MainSection.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'redva'; 2 | import MainSection from '../components/MainSection'; 3 | import { getCompletedTodoCount } from '../selectors'; 4 | 5 | const mapStateToProps = state => ({ 6 | todosCount: state.todos.length, 7 | completedCount: getCompletedTodoCount(state), 8 | }); 9 | 10 | const mapDispatchToProps = dispatch => ({ 11 | actions: { 12 | completeAllTodos: () => { 13 | dispatch({ 14 | type: 'todos/COMPLETE_ALL_TODOS', 15 | }); 16 | }, 17 | clearCompleted: () => { 18 | dispatch({ 19 | type: 'todos/CLEAR_COMPLETED', 20 | }); 21 | }, 22 | }, 23 | }); 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(MainSection); 26 | -------------------------------------------------------------------------------- /examples/dynamic-run/src/routes/counter.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'redva'; 2 | import React from 'react'; 3 | 4 | class Counter extends React.PureComponent { 5 | inc() { 6 | this.props.dispatch({ 7 | type: 'counter/inc', 8 | }); 9 | } 10 | dec() { 11 | this.props.dispatch({ 12 | type: 'counter/dec', 13 | }); 14 | } 15 | render() { 16 | return ( 17 |
18 |
{this.props.counter}
19 | 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default connect(state => { 27 | console.log('ut',state); 28 | return { 29 | counter: state.counter 30 | } 31 | })(Counter); 32 | -------------------------------------------------------------------------------- /examples/dynamic/src/routes/counter.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'redva'; 2 | import React from 'react'; 3 | 4 | class Counter extends React.PureComponent { 5 | inc() { 6 | this.props.dispatch({ 7 | type: 'counter/inc', 8 | }); 9 | } 10 | dec() { 11 | this.props.dispatch({ 12 | type: 'counter/dec', 13 | }); 14 | } 15 | render() { 16 | return ( 17 |
18 |
{this.props.counter}
19 | 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default connect(state => { 27 | console.log('ut',state); 28 | return { 29 | counter: state.counter 30 | } 31 | })(Counter); 32 | 33 | -------------------------------------------------------------------------------- /examples/todomvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/redva-core/src/prefixedDispatch.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import warning from 'warning'; 3 | import { NAMESPACE_SEP } from './constants'; 4 | import prefixType from './prefixType'; 5 | 6 | export default function prefixedDispatch(dispatch, model) { 7 | return action => { 8 | const { type } = action; 9 | invariant(type, 'dispatch: action should be a plain Object with type'); 10 | warning( 11 | type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0, 12 | `dispatch: ${type} should not be prefixed with namespace ${ 13 | model.namespace 14 | }` 15 | ); 16 | return dispatch({ 17 | ...action, 18 | type: prefixType(type, model), 19 | __redva_no_catch: true, 20 | }); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /examples/dynamic-run/src/common/router.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | import dynamic from 'redva/dynamic'; 3 | 4 | const routers = [ 5 | { 6 | url: '/counter', 7 | name: '计数器', 8 | models: ['counter'], 9 | component: 'counter', 10 | }, 11 | { 12 | url: '/todo', 13 | name: 'todo列表', 14 | models: ['todo'], 15 | component: 'todo', 16 | }, 17 | ]; 18 | 19 | export default routers.map(route => { 20 | return { 21 | url: route.url, 22 | name: route.name, 23 | component: dynamic({ 24 | app: app, 25 | models: () => { 26 | return route.models.map(model => { 27 | return import(`../models/${model}`); 28 | }); 29 | }, 30 | component: () => { 31 | return import(`../routes/${route.component}`); 32 | }, 33 | }), 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import replace from 'rollup-plugin-replace'; 4 | import uglify from 'rollup-plugin-uglify'; 5 | 6 | const env = process.env.NODE_ENV; 7 | 8 | export default { 9 | output: { 10 | format: 'umd', 11 | }, 12 | plugins: [ 13 | nodeResolve({ 14 | jsnext: true, 15 | }), 16 | replace({ 17 | 'process.env.NODE_ENV': JSON.stringify(env), 18 | }), 19 | commonjs(), 20 | ...(env === 'production' 21 | ? [ 22 | uglify({ 23 | compress: { 24 | pure_getters: true, 25 | unsafe: true, 26 | unsafe_comps: true, 27 | warnings: false, 28 | }, 29 | }), 30 | ] 31 | : []), 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /examples/func-test/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import fetch from 'redva/fetch'; 2 | 3 | function parseJSON(response) { 4 | return response.json(); 5 | } 6 | 7 | function checkStatus(response) { 8 | if (response.status >= 200 && response.status < 300) { 9 | return response; 10 | } 11 | 12 | const error = new Error(response.statusText); 13 | error.response = response; 14 | throw error; 15 | } 16 | 17 | /** 18 | * Requests a URL, returning a promise. 19 | * 20 | * @param {string} url The URL we want to request 21 | * @param {object} [options] The options we want to pass to "fetch" 22 | * @return {object} An object containing either "data" or "err" 23 | */ 24 | export default function request(url, options) { 25 | return fetch(url, options) 26 | .then(checkStatus) 27 | .then(parseJSON) 28 | .then(data => ({ data })) 29 | .catch(err => ({ err })); 30 | } 31 | -------------------------------------------------------------------------------- /examples/counter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import redva, { connect } from 'redva'; 3 | 4 | import counter from './models/counter'; 5 | 6 | // 1. Initialize 7 | const app = redva({}); 8 | 9 | // 2. Model 10 | app.model(counter); 11 | 12 | // 3. View 13 | const App = connect(state => { 14 | return { counter: state.counter }; 15 | })(props => { 16 | return ( 17 |
18 |

Count: {props.counter}

19 | 26 | 33 |
34 | ); 35 | }); 36 | 37 | // 4. Router 38 | app.router(() => ); 39 | 40 | // 5. Start 41 | app.start('#root'); 42 | -------------------------------------------------------------------------------- /examples/func-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redva-example", 3 | "private": true, 4 | "scripts": { 5 | "start": "roadhog server", 6 | "build": "roadhog build", 7 | "lint": "eslint --ext .js src test" 8 | }, 9 | "engines": { 10 | "install-node": "6.9.2" 11 | }, 12 | "dependencies": { 13 | "babel-runtime": "^6.9.2", 14 | "redva": "^1.1.1", 15 | "react": "^16.0.0", 16 | "react-dom": "^16.0.0" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^7.1.1", 20 | "babel-plugin-dva-hmr": "^0.3.2", 21 | "babel-plugin-module-alias": "^1.6.0", 22 | "babel-plugin-transform-runtime": "^6.9.0", 23 | "eslint": "^3.12.2", 24 | "eslint-config-airbnb": "^13.0.0", 25 | "eslint-plugin-import": "^2.2.0", 26 | "eslint-plugin-jsx-a11y": "^2.2.3", 27 | "eslint-plugin-react": "^6.8.0", 28 | "expect": "^1.20.2", 29 | "redbox-react": "^1.3.2", 30 | "roadhog": "^1.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/todomvc/src/selectors/index.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { 3 | SHOW_ALL, 4 | SHOW_COMPLETED, 5 | SHOW_ACTIVE, 6 | } from '../constants/TodoFilters'; 7 | 8 | const getVisibilityFilter = state => state.visibilityFilter; 9 | const getTodos = state => state.todos; 10 | 11 | export const getVisibleTodos = createSelector( 12 | [getVisibilityFilter, getTodos], 13 | (visibilityFilter, todos) => { 14 | switch (visibilityFilter) { 15 | case SHOW_ALL: 16 | return todos; 17 | case SHOW_COMPLETED: 18 | return todos.filter(t => t.completed); 19 | case SHOW_ACTIVE: 20 | return todos.filter(t => !t.completed); 21 | default: 22 | throw new Error('Unknown filter: ' + visibilityFilter); 23 | } 24 | } 25 | ); 26 | 27 | export const getCompletedTodoCount = createSelector([getTodos], todos => 28 | todos.reduce((count, todo) => (todo.completed ? count + 1 : count), 0) 29 | ); 30 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import App from './App'; 4 | import Header from '../containers/Header'; 5 | import MainSection from '../containers/MainSection'; 6 | 7 | const setup = propOverrides => { 8 | const renderer = createRenderer(); 9 | renderer.render(); 10 | const output = renderer.getRenderOutput(); 11 | return output; 12 | }; 13 | 14 | describe('components', () => { 15 | describe('Header', () => { 16 | it('should render', () => { 17 | const output = setup(); 18 | const [header] = output.props.children; 19 | expect(header.type).toBe(Header); 20 | }); 21 | }); 22 | 23 | describe('Mainsection', () => { 24 | it('should render', () => { 25 | const output = setup(); 26 | const [, mainSection] = output.props.children; 27 | expect(mainSection.type).toBe(MainSection); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/redva-loading/README.md: -------------------------------------------------------------------------------- 1 | # redva-loading 2 | 3 | Auto loading plugin for redva. :clap: You don't need to write `showLoading` and `hideLoading` any more. 4 | 5 | --- 6 | 7 | ## Install 8 | 9 | ```bash 10 | $ npm install redva-loading --save 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```javascript 16 | import createLoading from 'redva-loading'; 17 | 18 | const app = redva(); 19 | app.use(createLoading(opts)); 20 | ``` 21 | 22 | Then we can access loading state from store. 23 | 24 | ### opts 25 | 26 | - `opts.namespace`: property key on global state, type String, Default `loading` 27 | 28 | [See real project usage on dva-hackernews](https://github.com/dvajs/dva-hackernews/blob/2c3330b1c8ae728c94ebe1399b72486ad5a1a7a0/src/index.js#L4-L7). 29 | 30 | ## State Structure 31 | 32 | ``` 33 | loading: { 34 | global: false, 35 | models: { 36 | users: false, 37 | todos: false, 38 | ... 39 | }, 40 | } 41 | ``` 42 | 43 | ## License 44 | 45 | [MIT](https://tldrlegal.com/license/mit-license) 46 | -------------------------------------------------------------------------------- /packages/redva-core/src/createAsyncMiddleware.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { isFunction } from './utils'; 3 | import {resetType} from './utils'; 4 | 5 | export default function createAsyncMiddleware() { 6 | let actions; 7 | function run(models) { 8 | actions = {}; 9 | for (const key in models) { 10 | const model = models[key]; 11 | for (const name in model) { 12 | let action = model[name]; 13 | invariant( 14 | isFunction(action), 15 | `[model.actions] action should be function, but got ${typeof action}` 16 | ); 17 | actions[name] = action; 18 | } 19 | } 20 | } 21 | let asyncMiddleware = argv => next => action => { 22 | let { type } = action; 23 | type = resetType(type); 24 | const handler = actions[type]; 25 | if (handler) { 26 | return handler(action, argv); 27 | } else { 28 | return next(action); 29 | } 30 | }; 31 | asyncMiddleware.run = run; 32 | return asyncMiddleware; 33 | } 34 | -------------------------------------------------------------------------------- /examples/func-test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "generator-star-spacing": [0], 6 | "consistent-return": [0], 7 | "react/forbid-prop-types": [0], 8 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 9 | "global-require": [1], 10 | "import/prefer-default-export": [0], 11 | "react/jsx-no-bind": [0], 12 | "react/prop-types": [0], 13 | "react/prefer-stateless-function": [0], 14 | "no-else-return": [0], 15 | "no-restricted-syntax": [0], 16 | "import/no-extraneous-dependencies": [0], 17 | "no-use-before-define": [0], 18 | "jsx-a11y/no-static-element-interactions": [0], 19 | "no-nested-ternary": [0], 20 | "arrow-body-style": [0], 21 | "import/extensions": [0], 22 | "no-bitwise": [0], 23 | "no-cond-assign": [0], 24 | "import/no-unresolved": [0], 25 | "require-yield": [1] 26 | }, 27 | "parserOptions": { 28 | "ecmaFeatures": { 29 | "experimentalObjectRestSpread": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/redva-core/src/prefixNamespace.js: -------------------------------------------------------------------------------- 1 | import warning from 'warning'; 2 | import { isArray } from './utils'; 3 | import { NAMESPACE_SEP } from './constants'; 4 | 5 | function prefix(obj, namespace, type) { 6 | return Object.keys(obj).reduce((memo, key) => { 7 | warning( 8 | key.indexOf(`${namespace}${NAMESPACE_SEP}`) !== 0, 9 | `[prefixNamespace]: ${type} ${key} should not be prefixed with namespace ${namespace}` 10 | ); 11 | const newKey = `${namespace}${NAMESPACE_SEP}${key}`; 12 | memo[newKey] = obj[key]; 13 | return memo; 14 | }, {}); 15 | } 16 | 17 | export default function prefixNamespace(model) { 18 | const { namespace, actions, mutations } = model; 19 | 20 | if (mutations) { 21 | if (isArray(mutations)) { 22 | model.mutations[0] = prefix(mutations[0], namespace, 'mutations'); 23 | } else { 24 | model.mutations = prefix(mutations, namespace, 'mutations'); 25 | } 26 | } 27 | if (actions) { 28 | model.actions = prefix(actions, namespace, 'actions'); 29 | } 30 | return model; 31 | } 32 | -------------------------------------------------------------------------------- /examples/todomvc/src/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'redva'; 2 | import TodoList from '../components/TodoList'; 3 | import { getVisibleTodos } from '../selectors'; 4 | 5 | const mapStateToProps = state => ({ 6 | filteredTodos: getVisibleTodos(state), 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | actions: { 11 | editTodo: (id, text) => { 12 | dispatch({ 13 | type: 'todos/EDIT_TODO', 14 | id: id, 15 | text: text, 16 | }); 17 | }, 18 | deleteTodo: id => { 19 | dispatch({ 20 | type: 'todos/DELETE_TODO', 21 | id: id, 22 | }); 23 | }, 24 | completeTodo: id => { 25 | dispatch({ 26 | type: 'todos/COMPLETE_TODO', 27 | id: id, 28 | }); 29 | }, 30 | newTodo: id => { 31 | dispatch({ 32 | type: 'todos/ADD_TODO', 33 | id: id, 34 | }); 35 | }, 36 | }, 37 | }); 38 | 39 | const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList); 40 | 41 | export default VisibleTodoList; 42 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Footer from './Footer'; 4 | import VisibleTodoList from '../containers/VisibleTodoList'; 5 | 6 | const MainSection = ({ todosCount, completedCount, actions }) => ( 7 |
8 | {!!todosCount && ( 9 | 10 | 15 | 17 | )} 18 | 19 | {!!todosCount && ( 20 |
25 | )} 26 |
27 | ); 28 | 29 | MainSection.propTypes = { 30 | todosCount: PropTypes.number.isRequired, 31 | completedCount: PropTypes.number.isRequired, 32 | actions: PropTypes.object.isRequired, 33 | }; 34 | 35 | export default MainSection; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 ChenCheng (sorrycc@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/dynamic/src/routes/todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'redva'; 3 | 4 | class Todo extends React.PureComponent { 5 | state = { 6 | text: '', 7 | }; 8 | onChange(e) { 9 | this.setState({ text: e.target.value }); 10 | } 11 | add() { 12 | this.props.dispatch({ 13 | type: 'todo/add', 14 | text: this.state.text, 15 | }); 16 | } 17 | del(id) { 18 | this.props.dispatch({ 19 | type: 'todo/del', 20 | id: id, 21 | }); 22 | } 23 | render() { 24 | return ( 25 |
26 |
Todo Data
27 | 33 | 34 | {this.props.todo.map(todo => ( 35 |
36 | {todo.text} 37 | 38 |
39 | ))} 40 |
41 | ); 42 | } 43 | } 44 | export default connect(state => ({ todo: state.todo }))(Todo); 45 | -------------------------------------------------------------------------------- /examples/dynamic-run/src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'url-polyfill'; 3 | import React from 'react'; 4 | import app from './common/app'; 5 | import routers from './common/router'; 6 | import { Router, Route, Switch, Redirect } from 'redva/router'; 7 | require.context('./models/', true, /\.js$/); 8 | require.context('./routes/', true, /\.js$/); 9 | 10 | app.router(({ history, app }) => { 11 | console.log(routers); 12 | return ( 13 | 14 |
15 |
16 | 导航栏: 17 | {routers.map(router => ( 18 |
19 | {router.name} 20 |
21 | ))} 22 |
23 | 24 | {routers.map(router => ( 25 | 31 | ))} 32 | 33 | 34 |
35 |
36 | ); 37 | }); 38 | 39 | app.start('#root'); 40 | -------------------------------------------------------------------------------- /packages/redva-core/test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import Plugin from '../src/Plugin'; 3 | 4 | describe('plugin', () => { 5 | it('basic', () => { 6 | let hmrCount = 0; 7 | let errorMessage = ''; 8 | 9 | function onError(err) { 10 | errorMessage = err.message; 11 | } 12 | 13 | const plugin = new Plugin(); 14 | 15 | plugin.use({ 16 | onHmr: x => { 17 | hmrCount += 1 * x; 18 | }, 19 | onStateChange: 2, 20 | onAction: 1, 21 | extraReducers: { form: 1 }, 22 | onMutation: 1, 23 | }); 24 | plugin.use({ 25 | onHmr: x => { 26 | hmrCount += 2 + x; 27 | }, 28 | extraReducers: { user: 2 }, 29 | onMutation: 2, 30 | }); 31 | 32 | plugin.apply('onHmr')(2); 33 | plugin.apply('onError', onError)({ message: 'hello dva' }); 34 | 35 | expect(hmrCount).toEqual(6); 36 | expect(errorMessage).toEqual('hello dva'); 37 | 38 | expect(plugin.get('extraReducers')).toEqual({ form: 1, user: 2 }); 39 | expect(plugin.get('onAction')).toEqual([1]); 40 | expect(plugin.get('onStateChange')).toEqual([2]); 41 | 42 | expect(plugin.get('onMutation')).toEqual([1, 2]); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/dynamic-run/src/routes/todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'redva'; 3 | 4 | class Todo extends React.PureComponent { 5 | state = { 6 | text: '', 7 | }; 8 | onChange(e) { 9 | this.setState({ text: e.target.value }); 10 | } 11 | add() { 12 | this.props.dispatch({ 13 | type: 'todo/add', 14 | text: this.state.text, 15 | }); 16 | } 17 | del(id) { 18 | this.props.dispatch({ 19 | type: 'todo/del', 20 | id: id, 21 | }); 22 | } 23 | render() { 24 | return ( 25 |
26 |
Todo Data
27 | 33 | 34 | {this.props.todo.map(todo => ( 35 |
36 | {todo.text} 37 | 38 |
39 | ))} 40 |
41 | ); 42 | } 43 | } 44 | export default connect(state => { 45 | console.log('ut',state); 46 | return { todo: state.todo } 47 | })(Todo); 48 | -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const shell = require('shelljs'); 3 | const { join } = require('path'); 4 | const {fork} = require('child_process'); 5 | const packages = ["redva","redva-core","redva-loading"]; 6 | const cwd = process.cwd(); 7 | 8 | if ( 9 | shell 10 | .exec('npm config get registry') 11 | .stdout.indexOf('https://registry.npmjs.org/') === -1 12 | ) { 13 | console.error( 14 | 'Failed: set npm registry to https://registry.npmjs.org/ first' 15 | ); 16 | process.exit(1); 17 | } 18 | 19 | shell.exec("npm run build"); 20 | 21 | const cp = fork( 22 | join(cwd, 'node_modules/.bin/lerna'), 23 | ['publish', '--skip-npm'].concat(process.argv.slice(2)), 24 | { 25 | stdio: 'inherit', 26 | cwd: cwd, 27 | } 28 | ); 29 | cp.on('error', err => { 30 | console.log(err); 31 | }); 32 | cp.on('close', code => { 33 | console.log('code', code); 34 | if (code === 1) { 35 | console.error('Failed: lerna publish'); 36 | process.exit(1); 37 | } 38 | 39 | publishToNpm(); 40 | }); 41 | 42 | function publishToNpm() { 43 | for( const key in packages){ 44 | const package = packages[key]; 45 | shell.cd(join(cwd, 'packages', package)); 46 | shell.exec(`npm publish`); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/redva-core/src/subscription.js: -------------------------------------------------------------------------------- 1 | import warning from 'warning'; 2 | import { isFunction } from './utils'; 3 | import prefixedDispatch from './prefixedDispatch'; 4 | 5 | export function run(subs, model, app, onError) { 6 | const funcs = []; 7 | const nonFuncs = []; 8 | for (const key in subs) { 9 | if (Object.prototype.hasOwnProperty.call(subs, key)) { 10 | const sub = subs[key]; 11 | const unlistener = sub( 12 | { 13 | dispatch: prefixedDispatch(app._store.dispatch, model), 14 | history: app._history, 15 | }, 16 | onError 17 | ); 18 | if (isFunction(unlistener)) { 19 | funcs.push(unlistener); 20 | } else { 21 | nonFuncs.push(key); 22 | } 23 | } 24 | } 25 | return { funcs, nonFuncs }; 26 | } 27 | 28 | export function unlisten(unlisteners, namespace) { 29 | if (!unlisteners[namespace]) return; 30 | 31 | const { funcs, nonFuncs } = unlisteners[namespace]; 32 | warning( 33 | nonFuncs.length === 0, 34 | `[app.unmodel] subscription should return unlistener function, check these subscriptions ${nonFuncs.join( 35 | ', ' 36 | )}` 37 | ); 38 | for (const unlistener of funcs) { 39 | unlistener(); 40 | } 41 | delete unlisteners[namespace]; 42 | } 43 | -------------------------------------------------------------------------------- /packages/redva-core/src/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import flatten from 'flatten'; 3 | import invariant from 'invariant'; 4 | import window from 'global/window'; 5 | import { returnSelf, isArray } from './utils'; 6 | 7 | export default function({ 8 | reducers, 9 | initialState, 10 | plugin, 11 | asyncMiddleware, 12 | createOpts: { setupMiddlewares = returnSelf }, 13 | }) { 14 | // extra enhancers 15 | const extraEnhancers = plugin.get('extraEnhancers'); 16 | invariant( 17 | isArray(extraEnhancers), 18 | `[app.start] extraEnhancers should be array, but got ${typeof extraEnhancers}` 19 | ); 20 | 21 | const extraMiddlewares = plugin.get('extraMiddlewares'); 22 | const middlewares = setupMiddlewares([ 23 | asyncMiddleware, 24 | ...flatten(extraMiddlewares), 25 | ]); 26 | 27 | let devtools = () => noop => noop; 28 | if ( 29 | process.env.NODE_ENV !== 'production' && 30 | window.__REDUX_DEVTOOLS_EXTENSION__ 31 | ) { 32 | devtools = window.__REDUX_DEVTOOLS_EXTENSION__; 33 | } 34 | const enhancers = [ 35 | applyMiddleware(...middlewares), 36 | ...extraEnhancers, 37 | devtools(window.__REDUX_DEVTOOLS_EXTENSION__OPTIONS), 38 | ]; 39 | 40 | return createStore(reducers, initialState, compose(...enhancers)); 41 | } 42 | -------------------------------------------------------------------------------- /examples/async/README.md: -------------------------------------------------------------------------------- 1 | # Redva Async Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Redva TodoMVC Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Link.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import Link from './Link'; 4 | 5 | const setup = propOverrides => { 6 | const props = Object.assign( 7 | { 8 | active: false, 9 | children: 'All', 10 | setFilter: jest.fn(), 11 | }, 12 | propOverrides 13 | ); 14 | 15 | const renderer = createRenderer(); 16 | renderer.render(); 17 | const output = renderer.getRenderOutput(); 18 | 19 | return { 20 | props: props, 21 | output: output, 22 | }; 23 | }; 24 | 25 | describe('component', () => { 26 | describe('Link', () => { 27 | it('should render correctly', () => { 28 | const { output } = setup(); 29 | expect(output.type).toBe('a'); 30 | expect(output.props.style.cursor).toBe('pointer'); 31 | expect(output.props.children).toBe('All'); 32 | }); 33 | 34 | it('should have class selected if active', () => { 35 | const { output } = setup({ active: true }); 36 | expect(output.props.className).toBe('selected'); 37 | }); 38 | 39 | it('should call setFilter on click', () => { 40 | const { output, props } = setup(); 41 | output.props.onClick(); 42 | expect(props.setFilter).toBeCalled(); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/redva-loading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redva-loading", 3 | "version": "1.1.3", 4 | "description": "Auto loading plugin for redva.", 5 | "scripts": { 6 | "build": "babel src -d lib", 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/fishedee/redva" 12 | }, 13 | "homepage": "https://github.com/fishedee/redva", 14 | "keywords": [ 15 | "redva", 16 | "redva-plugin", 17 | "dva", 18 | "dva-plugin", 19 | "loading" 20 | ], 21 | "author": "fishedee ", 22 | "license": "MIT", 23 | "main": "lib/index.js", 24 | "devDependencies": { 25 | "babel-cli": "^6.26.0", 26 | "babel-core": "^6.26.3", 27 | "babel-eslint": "^8.0.2", 28 | "babel-plugin-transform-runtime": "^6.23.0", 29 | "babel-preset-env": "^1.7.0", 30 | "babel-preset-react": "^6.24.1", 31 | "babel-preset-stage-0": "^6.24.1", 32 | "jest": "^23.0.0", 33 | "react": "^16.4.0", 34 | "react-dom": "^16.4.0", 35 | "redva": "^1.1.3" 36 | }, 37 | "peerDependencies": { 38 | "react": "15.x || ^16.0.0-0", 39 | "react-dom": "15.x || ^16.0.0-0", 40 | "redva": "^1.1.0" 41 | }, 42 | "files": [ 43 | "lib", 44 | "src" 45 | ], 46 | "dependencies": { 47 | "babel-runtime": "^6.26.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/async-loading/README.md: -------------------------------------------------------------------------------- 1 | # Redva Async Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import FilterLink from '../containers/FilterLink'; 4 | import { 5 | SHOW_ALL, 6 | SHOW_COMPLETED, 7 | SHOW_ACTIVE, 8 | } from '../constants/TodoFilters'; 9 | 10 | const FILTER_TITLES = { 11 | [SHOW_ALL]: 'All', 12 | [SHOW_ACTIVE]: 'Active', 13 | [SHOW_COMPLETED]: 'Completed', 14 | }; 15 | 16 | const Footer = props => { 17 | const { activeCount, completedCount, onClearCompleted } = props; 18 | const itemWord = activeCount === 1 ? 'item' : 'items'; 19 | return ( 20 |
21 | 22 | {activeCount || 'No'} {itemWord} left 23 | 24 |
    25 | {Object.keys(FILTER_TITLES).map(filter => ( 26 |
  • 27 | {FILTER_TITLES[filter]} 28 |
  • 29 | ))} 30 |
31 | {!!completedCount && ( 32 | 35 | )} 36 |
37 | ); 38 | }; 39 | 40 | Footer.propTypes = { 41 | completedCount: PropTypes.number.isRequired, 42 | activeCount: PropTypes.number.isRequired, 43 | onClearCompleted: PropTypes.func.isRequired, 44 | }; 45 | 46 | export default Footer; 47 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | export default class TodoTextInput extends Component { 6 | static propTypes = { 7 | onSave: PropTypes.func.isRequired, 8 | text: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | editing: PropTypes.bool, 11 | newTodo: PropTypes.bool, 12 | }; 13 | 14 | state = { 15 | text: this.props.text || '', 16 | }; 17 | 18 | handleSubmit = e => { 19 | const text = e.target.value.trim(); 20 | if (e.which === 13) { 21 | this.props.onSave(text); 22 | if (this.props.newTodo) { 23 | this.setState({ text: '' }); 24 | } 25 | } 26 | }; 27 | 28 | handleChange = e => { 29 | this.setState({ text: e.target.value }); 30 | }; 31 | 32 | handleBlur = e => { 33 | if (!this.props.newTodo) { 34 | this.props.onSave(e.target.value); 35 | } 36 | }; 37 | 38 | render() { 39 | return ( 40 | 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/redva-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redva-core", 3 | "version": "1.1.3", 4 | "description": "The core lightweight library for redva, based on redux and async/await.", 5 | "scripts": { 6 | "build": "babel src -d lib", 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/fishedee/redva" 12 | }, 13 | "homepage": "https://github.com/fishedee/redva", 14 | "keywords": [ 15 | "redva", 16 | "dva", 17 | "alibaba", 18 | "redux", 19 | "elm", 20 | "framework", 21 | "frontend" 22 | ], 23 | "authors": [ 24 | "fishedee (https://github.com/fishedee)" 25 | ], 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/fishedee/redva/issues" 29 | }, 30 | "dependencies": { 31 | "babel-runtime": "^6.26.0", 32 | "flatten": "^1.0.2", 33 | "global": "^4.3.2", 34 | "immer": "^1.2.1", 35 | "invariant": "^2.2.1", 36 | "is-plain-object": "^2.0.3", 37 | "redux": "^3.7.2", 38 | "warning": "^3.0.0" 39 | }, 40 | "devDependencies": { 41 | "babel-cli": "^6.26.0", 42 | "babel-core": "^6.26.3", 43 | "babel-eslint": "^8.0.2", 44 | "babel-plugin-transform-runtime": "^6.23.0", 45 | "babel-preset-env": "^1.7.0", 46 | "babel-preset-react": "^6.24.1", 47 | "babel-preset-stage-0": "^6.24.1", 48 | "jest": "^23.0.0" 49 | }, 50 | "files": [ 51 | "lib", 52 | "src", 53 | "index.js" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /packages/redva-core/test/mutations.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { create } from '../src/index'; 3 | 4 | describe('reducers', () => { 5 | it('extraReducers', () => { 6 | const reducers = { 7 | count: (state, { type }) => { 8 | if (type === 'add') { 9 | return state + 1; 10 | } 11 | // default state 12 | return 0; 13 | }, 14 | }; 15 | const app = create({ 16 | extraReducers: reducers, 17 | }); 18 | app.start(); 19 | 20 | expect(app._store.getState().count).toEqual(0); 21 | app._store.dispatch({ type: 'add' }); 22 | expect(app._store.getState().count).toEqual(1); 23 | }); 24 | 25 | it('onMutation ', () => { 26 | let mutationCount = 0; 27 | const onMutation = r => (state, action) => { 28 | r(state, action); 29 | mutationCount++; 30 | }; 31 | const app = create({ 32 | onMutation: onMutation, 33 | }); 34 | app.model({ 35 | namespace: 'count', 36 | state: 0, 37 | mutations: { 38 | add(state) { 39 | state.count += 1; 40 | }, 41 | }, 42 | }); 43 | app.start(); 44 | 45 | app._store.dispatch({ type: 'count/add' }); 46 | expect(app._store.getState().count).toEqual(1); 47 | app._store.dispatch({ type: 'count/add' }); 48 | app._store.dispatch({ type: 'count/add' }); 49 | expect(app._store.getState().count).toEqual(3); 50 | expect(mutationCount).toEqual(3); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/dynamic/src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'url-polyfill'; 3 | import React from 'react'; 4 | import app from './common/app'; 5 | import routers from './common/router'; 6 | import {connect} from 'redva'; 7 | import { routerRedux,Router, Route, Switch, Redirect } from 'redva/router'; 8 | 9 | class Naviagtion extends React.PureComponent{ 10 | constructor(props){ 11 | super(props); 12 | } 13 | next(url){ 14 | this.props.dispatch(routerRedux.replace(url)) 15 | } 16 | render(){ 17 | return( 18 |
19 | 导航栏: 20 | {routers.map(router => ( 21 |
22 | {router.name} 23 |
24 | ))} 25 |
26 | ); 27 | } 28 | } 29 | 30 | let NaviagtionConnect = connect()(Naviagtion) 31 | 32 | app.router(({ history, app }) => { 33 | return ( 34 | 35 |
36 | 37 | 38 | {routers.map(router => ( 39 | 45 | ))} 46 | 47 | 48 |
49 |
50 | ); 51 | }); 52 | 53 | app.start('#root'); 54 | -------------------------------------------------------------------------------- /examples/todomvc/src/models/todos.js: -------------------------------------------------------------------------------- 1 | function findTodo(todos, id) { 2 | return todos.findIndex(todo => { 3 | return todo.id == id; 4 | }); 5 | } 6 | export default { 7 | namespace: 'todos', 8 | state: [ 9 | { 10 | text: 'Use Redux', 11 | completed: false, 12 | id: 0, 13 | }, 14 | ], 15 | mutations: { 16 | ADD_TODO(state, action) { 17 | let maxId = state.todos.reduce( 18 | (maxId, todo) => Math.max(todo.id, maxId), 19 | -1 20 | ); 21 | state.todos.push({ 22 | id: maxId + 1, 23 | completed: false, 24 | text: action.text, 25 | }); 26 | }, 27 | DELETE_TODO(state, action) { 28 | let index = findTodo(state.todos, action.id); 29 | if (index != -1) { 30 | state.todos.splice(index, 1); 31 | } 32 | }, 33 | EDIT_TODO(state, action) { 34 | let index = findTodo(state.todos, action.id); 35 | if (index != -1) { 36 | state.todos[index].text = action.text; 37 | } 38 | }, 39 | COMPLETE_TODO(state, action) { 40 | let index = findTodo(state.todos, action.id); 41 | if (index != -1) { 42 | state.todos[index].completed = !state.todos[index].completed; 43 | } 44 | }, 45 | COMPLETE_ALL_TODOS(state, action) { 46 | let areAllMarked = state.todos.every(todo => todo.completed); 47 | for (let i in state.todos) { 48 | state.todos[i].completed = !areAllMarked; 49 | } 50 | }, 51 | CLEAR_COMPLETED(state, action) { 52 | state.todos = state.todos.filter(todo => todo.completed === false); 53 | }, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoList.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import TodoList from './TodoList'; 4 | import TodoItem from './TodoItem'; 5 | 6 | const setup = () => { 7 | const props = { 8 | filteredTodos: [ 9 | { 10 | text: 'Use Redux', 11 | completed: false, 12 | id: 0, 13 | }, 14 | { 15 | text: 'Run the tests', 16 | completed: true, 17 | id: 1, 18 | }, 19 | ], 20 | actions: { 21 | editTodo: jest.fn(), 22 | deleteTodo: jest.fn(), 23 | completeTodo: jest.fn(), 24 | completeAll: jest.fn(), 25 | clearCompleted: jest.fn(), 26 | }, 27 | }; 28 | 29 | const renderer = createRenderer(); 30 | renderer.render(); 31 | const output = renderer.getRenderOutput(); 32 | 33 | return { 34 | props: props, 35 | output: output, 36 | }; 37 | }; 38 | 39 | describe('components', () => { 40 | describe('TodoList', () => { 41 | it('should render container', () => { 42 | const { output } = setup(); 43 | expect(output.type).toBe('ul'); 44 | expect(output.props.className).toBe('todo-list'); 45 | }); 46 | 47 | it('should render todos', () => { 48 | const { output, props } = setup(); 49 | expect(output.props.children.length).toBe(2); 50 | output.props.children.forEach((todo, i) => { 51 | expect(todo.type).toBe(TodoItem); 52 | expect(Number(todo.key)).toBe(props.filteredTodos[i].id); 53 | expect(todo.props.todo).toBe(props.filteredTodos[i]); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/redva-core/src/getAction.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import prefixedDispatch from './prefixedDispatch'; 3 | import { isFunction } from './utils'; 4 | 5 | export default function getAction(effects, model, onError, onEffect) { 6 | let actions = {}; 7 | for (const key in effects) { 8 | if (Object.prototype.hasOwnProperty.call(effects, key)) { 9 | actions[key] = getSingleAction( 10 | key, 11 | effects[key], 12 | model, 13 | onError, 14 | onEffect 15 | ); 16 | } 17 | } 18 | return actions; 19 | } 20 | 21 | function getSingleAction(key, effect, model, onError, onEffect) { 22 | invariant(isFunction(effect), '[model.action]: action should be function'); 23 | let actionWithCatch = async function(action, { dispatch, ...rest }) { 24 | dispatch = prefixedDispatch(dispatch, model); 25 | if (action.__redva_no_catch) { 26 | return await effect(action, { dispatch, ...rest }); 27 | } else { 28 | try { 29 | return await effect(action, { dispatch, ...rest }); 30 | } catch (e) { 31 | onError(e, { 32 | key, 33 | actionArgs: arguments, 34 | }); 35 | if (!e._dontReject) { 36 | throw e; 37 | } else { 38 | return e; 39 | } 40 | } 41 | } 42 | }; 43 | const actionWithOnEffect = applyOnEffect( 44 | onEffect, 45 | actionWithCatch, 46 | model, 47 | key 48 | ); 49 | return actionWithOnEffect; 50 | } 51 | 52 | function applyOnEffect(fns, effect, model, key) { 53 | for (const fn of fns) { 54 | effect = fn(effect, model, key); 55 | } 56 | return effect; 57 | } 58 | -------------------------------------------------------------------------------- /packages/redva-core/src/checkModel.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { isArray, isFunction, isPlainObject } from './utils'; 3 | 4 | export default function checkModel(model, existModels) { 5 | const { namespace, mutations, actions, subscriptions, state } = model; 6 | 7 | // namespace 必须被定义 8 | invariant(namespace, `[app.model] namespace should be defined`); 9 | // 并且是字符串 10 | invariant( 11 | typeof namespace === 'string', 12 | `[app.model] namespace should be string, but got ${typeof namespace}` 13 | ); 14 | // 并且唯一 15 | invariant( 16 | !existModels.some(model => model.namespace === namespace), 17 | `[app.model] namespace should be unique` 18 | ); 19 | 20 | // state 任意 21 | 22 | // mutations 可以为空,PlainObject 或者数组 23 | if (mutations) { 24 | invariant( 25 | isPlainObject(mutations), 26 | `[app.model] mutations should be plain object, but got ${typeof mutations}` 27 | ); 28 | } 29 | 30 | // actions 可以为空,PlainObject 31 | if (actions) { 32 | invariant( 33 | isPlainObject(actions), 34 | `[app.model] actions should be plain object, but got ${typeof actions}` 35 | ); 36 | } 37 | 38 | if (subscriptions) { 39 | // subscriptions 可以为空,PlainObject 40 | invariant( 41 | isPlainObject(subscriptions), 42 | `[app.model] subscriptions should be plain object, but got ${typeof subscriptions}` 43 | ); 44 | 45 | // subscription 必须为函数 46 | invariant( 47 | isAllFunction(subscriptions), 48 | `[app.model] subscription should be function` 49 | ); 50 | } 51 | } 52 | 53 | function isAllFunction(obj) { 54 | return Object.keys(obj).every(key => isFunction(obj[key])); 55 | } 56 | -------------------------------------------------------------------------------- /examples/dynamic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/dynamic-run/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "jest": true 7 | }, 8 | "rules": { 9 | "jsx-a11y/href-no-hash": [0], 10 | "jsx-a11y/click-events-have-key-events": [0], 11 | "jsx-a11y/anchor-is-valid": [ "error", { 12 | "components": [ "Link" ], 13 | "specialLink": [ "to" ] 14 | }], 15 | "generator-star-spacing": [0], 16 | "consistent-return": [0], 17 | "react/react-in-jsx-scope": [0], 18 | "react/forbid-prop-types": [0], 19 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 20 | "global-require": [1], 21 | "import/prefer-default-export": [0], 22 | "react/jsx-no-bind": [0], 23 | "react/prop-types": [0], 24 | "react/prefer-stateless-function": [0], 25 | "no-else-return": [0], 26 | "no-restricted-syntax": [0], 27 | "import/no-extraneous-dependencies": [0], 28 | "no-use-before-define": [0], 29 | "jsx-a11y/no-static-element-interactions": [0], 30 | "no-nested-ternary": [0], 31 | "arrow-body-style": [0], 32 | "import/extensions": [0], 33 | "no-bitwise": [0], 34 | "no-cond-assign": [0], 35 | "import/no-unresolved": [0], 36 | "require-yield": [1], 37 | "no-param-reassign": [0], 38 | "no-shadow": [0], 39 | "no-underscore-dangle": [0], 40 | "spaced-comment": [0], 41 | "indent": [0], 42 | "quotes": [0], 43 | "func-names": [0], 44 | "arrow-parens": [0], 45 | "space-before-function-paren": [0], 46 | "no-useless-escape": [0], 47 | "object-curly-newline": [0], 48 | "function-paren-newline": [0], 49 | "class-methods-use-this": [0], 50 | "no-new": [0], 51 | "import/newline-after-import": [0], 52 | "no-console": [0] 53 | }, 54 | "parserOptions": { 55 | "ecmaFeatures": { 56 | "experimentalObjectRestSpread": true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/redva-core/src/createImmerReducer.js: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | import invariant from 'invariant'; 3 | import {resetType} from './utils'; 4 | 5 | export function combineReducers(reducers) { 6 | return (state = {}, action) => { 7 | let hasChanged = false; 8 | let nextState = {}; 9 | for (const key in reducers) { 10 | let prevStateKey = state[key]; 11 | let nextStateKey = reducers[key](prevStateKey, action); 12 | nextState[key] = nextStateKey; 13 | hasChanged = hasChanged || nextStateKey !== prevStateKey; 14 | } 15 | if (hasChanged == false) { 16 | return state; 17 | } else { 18 | return { 19 | ...state, 20 | ...nextState, 21 | }; 22 | } 23 | }; 24 | } 25 | export default function createImmerReducer(reducers) { 26 | let defaultState = {}; 27 | let mutations = {}; 28 | for (let key in reducers) { 29 | defaultState[key] = reducers[key].state; 30 | for (let action in reducers[key].mutations) { 31 | mutations[action] = reducers[key].mutations[action]; 32 | } 33 | } 34 | return (state = {}, action) => { 35 | let addState = {}; 36 | let hasAddState = false; 37 | for (let key in defaultState) { 38 | if (key in state == false) { 39 | addState[key] = defaultState[key]; 40 | hasAddState = true; 41 | } 42 | } 43 | if (hasAddState) { 44 | state = { 45 | ...state, 46 | ...addState, 47 | }; 48 | } 49 | let { type } = action; 50 | type = resetType(type); 51 | invariant(type, 'dispatch: action should be a plain Object with type'); 52 | const handler = mutations[type]; 53 | if (handler) { 54 | const ret = produce(state, draft => { 55 | handler(draft, action); 56 | }); 57 | return ret === undefined ? {} : ret; 58 | } else { 59 | return state; 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import TodoTextInput from './TodoTextInput'; 5 | 6 | export default class TodoItem extends Component { 7 | static propTypes = { 8 | todo: PropTypes.object.isRequired, 9 | editTodo: PropTypes.func.isRequired, 10 | deleteTodo: PropTypes.func.isRequired, 11 | completeTodo: PropTypes.func.isRequired, 12 | }; 13 | 14 | state = { 15 | editing: false, 16 | }; 17 | 18 | handleDoubleClick = () => { 19 | this.setState({ editing: true }); 20 | }; 21 | 22 | handleSave = (id, text) => { 23 | if (text.length === 0) { 24 | this.props.deleteTodo(id); 25 | } else { 26 | this.props.editTodo(id, text); 27 | } 28 | this.setState({ editing: false }); 29 | }; 30 | 31 | render() { 32 | const { todo, completeTodo, deleteTodo } = this.props; 33 | 34 | let element; 35 | if (this.state.editing) { 36 | element = ( 37 | this.handleSave(todo.id, text)} 41 | /> 42 | ); 43 | } else { 44 | element = ( 45 |
46 | completeTodo(todo.id)} 51 | /> 52 | 53 |
55 | ); 56 | } 57 | 58 | return ( 59 |
  • 65 | {element} 66 |
  • 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/redva/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redva", 3 | "version": "1.1.3", 4 | "description": "React and redux based, lightweight and elm-style framework.", 5 | "scripts": { 6 | "build": "babel src -d lib", 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/fishedee/redva" 12 | }, 13 | "homepage": "https://github.com/fishedee/redva", 14 | "keywords": [ 15 | "redva", 16 | "dva", 17 | "alibaba", 18 | "react", 19 | "react-native", 20 | "redux", 21 | "elm", 22 | "framework", 23 | "frontend" 24 | ], 25 | "authors": [ 26 | "fishedee (https://github.com/fishedee)" 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/fishedee/redva/issues" 31 | }, 32 | "dependencies": { 33 | "@types/isomorphic-fetch": "^0.0.34", 34 | "@types/react-router": "^4.0.23", 35 | "@types/react-router-redux": "^5.0.13", 36 | "babel-runtime": "^6.26.0", 37 | "global": "^4.3.2", 38 | "history": "^4.7.2", 39 | "invariant": "^2.2.2", 40 | "isomorphic-fetch": "^2.2.1", 41 | "react-redux": "^5.0.7", 42 | "react-router-dom": "^4.2.2", 43 | "react-router-redux": "4.0.8", 44 | "redva-core": "^1.1.3" 45 | }, 46 | "devDependencies": { 47 | "babel-cli": "^6.26.0", 48 | "babel-core": "^6.26.3", 49 | "babel-eslint": "^8.0.2", 50 | "babel-plugin-transform-runtime": "^6.23.0", 51 | "babel-preset-env": "^1.7.0", 52 | "babel-preset-react": "^6.24.1", 53 | "babel-preset-stage-0": "^6.24.1", 54 | "jest": "^23.0.0", 55 | "react": "^16.4.0", 56 | "react-dom": "^16.4.0" 57 | }, 58 | "peerDependencies": { 59 | "react": "15.x || ^16.0.0-0", 60 | "react-dom": "15.x || ^16.0.0-0" 61 | }, 62 | "files": [ 63 | "lib", 64 | "src", 65 | "dist", 66 | "dynamic.js", 67 | "fetch.js", 68 | "index.js", 69 | "router.js", 70 | "*.d.ts" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /packages/redva-core/src/Plugin.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { isPlainObject } from './utils'; 3 | 4 | const hooks = [ 5 | 'onError', 6 | 'onStateChange', 7 | 'onHmr', 8 | 'onMutation', 9 | 'onAction', 10 | 'extraMiddlewares', 11 | 'extraModels', 12 | 'extraEnhancers', 13 | 'extraReducers', 14 | ]; 15 | 16 | export function filterHooks(obj) { 17 | return Object.keys(obj).reduce((memo, key) => { 18 | if (hooks.indexOf(key) > -1) { 19 | memo[key] = obj[key]; 20 | } 21 | return memo; 22 | }, {}); 23 | } 24 | 25 | export default class Plugin { 26 | constructor() { 27 | this._handleActions = null; 28 | this.hooks = hooks.reduce((memo, key) => { 29 | memo[key] = []; 30 | return memo; 31 | }, {}); 32 | } 33 | 34 | use(plugin) { 35 | invariant( 36 | isPlainObject(plugin), 37 | 'plugin.use: plugin should be plain object' 38 | ); 39 | const hooks = this.hooks; 40 | for (const key in plugin) { 41 | if (Object.prototype.hasOwnProperty.call(plugin, key)) { 42 | invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`); 43 | if (key === 'extraEnhancers') { 44 | hooks[key] = plugin[key]; 45 | } else { 46 | hooks[key].push(plugin[key]); 47 | } 48 | } 49 | } 50 | } 51 | 52 | apply(key, defaultHandler) { 53 | const hooks = this.hooks; 54 | const validApplyHooks = ['onError', 'onHmr']; 55 | invariant( 56 | validApplyHooks.indexOf(key) > -1, 57 | `plugin.apply: hook ${key} cannot be applied` 58 | ); 59 | const fns = hooks[key]; 60 | 61 | return (...args) => { 62 | if (fns.length) { 63 | for (const fn of fns) { 64 | fn(...args); 65 | } 66 | } else if (defaultHandler) { 67 | defaultHandler(...args); 68 | } 69 | }; 70 | } 71 | 72 | get(key) { 73 | const hooks = this.hooks; 74 | invariant(key in hooks, `plugin.get: hook ${key} cannot be got`); 75 | if (key === 'extraReducers') { 76 | return getExtraReducers(hooks[key]); 77 | } else { 78 | return hooks[key]; 79 | } 80 | } 81 | } 82 | 83 | function getExtraReducers(hook) { 84 | let ret = {}; 85 | for (const reducerObj of hook) { 86 | ret = { ...ret, ...reducerObj }; 87 | } 88 | return ret; 89 | } 90 | -------------------------------------------------------------------------------- /examples/async/src/models/postsBySubreddit.js: -------------------------------------------------------------------------------- 1 | const shouldFetchPosts = (state, subreddit) => { 2 | const posts = state.postsBySubreddit[subreddit]; 3 | if (!posts) { 4 | return true; 5 | } 6 | if (posts.isFetching) { 7 | return false; 8 | } 9 | return posts.didInvalidate; 10 | }; 11 | 12 | const createOrGetSubreddit = (postsBySubreddit, subreddit) => { 13 | let reddit = postsBySubreddit[subreddit]; 14 | if (!reddit) { 15 | reddit = { 16 | isFetching: false, 17 | didInvalidate: false, 18 | items: [], 19 | }; 20 | postsBySubreddit[subreddit] = reddit; 21 | } 22 | return reddit; 23 | }; 24 | 25 | export default { 26 | namespace: 'postsBySubreddit', 27 | state: {}, 28 | mutations: { 29 | invalidateSubreddit(state, action) { 30 | const subreddit = createOrGetSubreddit( 31 | state.postsBySubreddit, 32 | action.subreddit 33 | ); 34 | subreddit.didInvalidate = true; 35 | }, 36 | requestPosts(state, action) { 37 | const subreddit = createOrGetSubreddit( 38 | state.postsBySubreddit, 39 | action.subreddit 40 | ); 41 | subreddit.isFetching = true; 42 | subreddit.didInvalidate = false; 43 | }, 44 | receivePosts(state, action) { 45 | const subreddit = createOrGetSubreddit( 46 | state.postsBySubreddit, 47 | action.subreddit 48 | ); 49 | subreddit.isFetching = false; 50 | subreddit.didInvalidate = false; 51 | subreddit.items = action.posts; 52 | subreddit.lastUpdated = action.receivedAt; 53 | }, 54 | }, 55 | actions: { 56 | async fetchPosts(action, { dispatch, getState }) { 57 | dispatch({ 58 | type: 'requestPosts', 59 | subreddit: action.subreddit, 60 | }); 61 | let response = await fetch( 62 | `https://www.reddit.com/r/${action.subreddit}.json` 63 | ); 64 | let json = await response.json(); 65 | dispatch({ 66 | type: 'receivePosts', 67 | subreddit: action.subreddit, 68 | posts: json.data.children.map(child => child.data), 69 | receivedAt: Date.now(), 70 | }); 71 | }, 72 | fetchPostsIfNeeded(action, { dispatch, getState }) { 73 | if (shouldFetchPosts(getState(), action.subreddit)) { 74 | dispatch({ 75 | type: 'fetchPosts', 76 | subreddit: action.subreddit, 77 | }); 78 | } 79 | }, 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /examples/async-loading/src/models/postsBySubreddit.js: -------------------------------------------------------------------------------- 1 | const shouldFetchPosts = (state, subreddit) => { 2 | const posts = state.postsBySubreddit[subreddit]; 3 | if (!posts) { 4 | return true; 5 | } 6 | if (posts.isFetching) { 7 | return false; 8 | } 9 | return posts.didInvalidate; 10 | }; 11 | 12 | const createOrGetSubreddit = (postsBySubreddit, subreddit) => { 13 | let reddit = postsBySubreddit[subreddit]; 14 | if (!reddit) { 15 | reddit = { 16 | isFetching: false, 17 | didInvalidate: false, 18 | items: [], 19 | }; 20 | postsBySubreddit[subreddit] = reddit; 21 | } 22 | return reddit; 23 | }; 24 | 25 | export default { 26 | namespace: 'postsBySubreddit', 27 | state: {}, 28 | mutations: { 29 | invalidateSubreddit(state, action) { 30 | const subreddit = createOrGetSubreddit( 31 | state.postsBySubreddit, 32 | action.subreddit 33 | ); 34 | subreddit.didInvalidate = true; 35 | }, 36 | requestPosts(state, action) { 37 | const subreddit = createOrGetSubreddit( 38 | state.postsBySubreddit, 39 | action.subreddit 40 | ); 41 | subreddit.isFetching = true; 42 | subreddit.didInvalidate = false; 43 | }, 44 | receivePosts(state, action) { 45 | const subreddit = createOrGetSubreddit( 46 | state.postsBySubreddit, 47 | action.subreddit 48 | ); 49 | subreddit.isFetching = false; 50 | subreddit.didInvalidate = false; 51 | subreddit.items = action.posts; 52 | subreddit.lastUpdated = action.receivedAt; 53 | }, 54 | }, 55 | actions: { 56 | async fetchPosts(action, { dispatch, getState }) { 57 | dispatch({ 58 | type: 'requestPosts', 59 | subreddit: action.subreddit, 60 | }); 61 | let response = await fetch( 62 | `https://www.reddit.com/r/${action.subreddit}.json` 63 | ); 64 | let json = await response.json(); 65 | dispatch({ 66 | type: 'receivePosts', 67 | subreddit: action.subreddit, 68 | posts: json.data.children.map(child => child.data), 69 | receivedAt: Date.now(), 70 | }); 71 | }, 72 | fetchPostsIfNeeded(action, { dispatch, getState }) { 73 | if (shouldFetchPosts(getState(), action.subreddit)) { 74 | dispatch({ 75 | type: 'fetchPosts', 76 | subreddit: action.subreddit, 77 | }); 78 | } 79 | }, 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /packages/redva/src/dynamic.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const cached = {}; 4 | function registerModel(app, model) { 5 | model = model.default || model; 6 | if (!cached[model.namespace]) { 7 | app.model(model); 8 | cached[model.namespace] = 1; 9 | } 10 | } 11 | 12 | let defaultLoadingComponent = () => null; 13 | 14 | function asyncComponent(config) { 15 | const { resolve } = config; 16 | 17 | return class DynamicComponent extends Component { 18 | constructor(...args) { 19 | super(...args); 20 | this.LoadingComponent = 21 | config.LoadingComponent || defaultLoadingComponent; 22 | this.state = { 23 | AsyncComponent: null, 24 | }; 25 | this.load(); 26 | } 27 | 28 | componentDidMount() { 29 | this.mounted = true; 30 | } 31 | 32 | componentWillUnmount() { 33 | this.mounted = false; 34 | } 35 | 36 | load() { 37 | resolve().then(m => { 38 | const AsyncComponent = m.default || m; 39 | if (this.mounted) { 40 | this.setState({ AsyncComponent }); 41 | } else { 42 | this.state.AsyncComponent = AsyncComponent; // eslint-disable-line 43 | } 44 | }); 45 | } 46 | 47 | render() { 48 | const { AsyncComponent } = this.state; 49 | const { LoadingComponent } = this; 50 | if (AsyncComponent) return ; 51 | 52 | return ; 53 | } 54 | }; 55 | } 56 | 57 | export default function dynamic(config) { 58 | const { app, models: resolveModels, component: resolveComponent } = config; 59 | return asyncComponent({ 60 | resolve: 61 | config.resolve || 62 | function() { 63 | const models = 64 | typeof resolveModels === 'function' ? resolveModels() : []; 65 | const component = resolveComponent(); 66 | return new Promise(resolve => { 67 | Promise.all([...models, component]).then(ret => { 68 | if (!models || !models.length) { 69 | return resolve(ret[0]); 70 | } else { 71 | const len = models.length; 72 | ret.slice(0, len).forEach(m => { 73 | m = m.default || m; 74 | if (!Array.isArray(m)) { 75 | m = [m]; 76 | } 77 | m.map(_ => registerModel(app, _)); 78 | }); 79 | resolve(ret[len]); 80 | } 81 | }); 82 | }); 83 | }, 84 | ...config, 85 | }); 86 | } 87 | 88 | dynamic.setDefaultLoadingComponent = LoadingComponent => { 89 | defaultLoadingComponent = LoadingComponent; 90 | }; 91 | -------------------------------------------------------------------------------- /packages/redva-loading/src/index.js: -------------------------------------------------------------------------------- 1 | const SHOW = '@@DVA_LOADING/SHOW'; 2 | const HIDE = '@@DVA_LOADING/HIDE'; 3 | const NAMESPACE = 'loading'; 4 | 5 | function createLoading(opts = {}) { 6 | const namespace = opts.namespace || NAMESPACE; 7 | 8 | const { only = [], except = [] } = opts; 9 | if (only.length > 0 && except.length > 0) { 10 | throw Error( 11 | 'It is ambiguous to configurate `only` and `except` items at the same time.' 12 | ); 13 | } 14 | 15 | const initialState = { 16 | global: false, 17 | models: {}, 18 | actions: {}, 19 | }; 20 | 21 | const extraReducers = { 22 | [namespace](state = initialState, { type, payload }) { 23 | const { namespace, actionType } = payload || {}; 24 | let ret; 25 | switch (type) { 26 | case SHOW: 27 | ret = { 28 | ...state, 29 | global: true, 30 | models: { ...state.models, [namespace]: true }, 31 | actions: { ...state.actions, [actionType]: true }, 32 | }; 33 | break; 34 | case HIDE: // eslint-disable-line 35 | const actions = { ...state.actions, [actionType]: false }; 36 | const models = { 37 | ...state.models, 38 | [namespace]: Object.keys(actions).some(actionType => { 39 | const _namespace = actionType.split('/')[0]; 40 | if (_namespace !== namespace) return false; 41 | return actions[actionType]; 42 | }), 43 | }; 44 | const global = Object.keys(models).some(namespace => { 45 | return models[namespace]; 46 | }); 47 | ret = { 48 | ...state, 49 | global, 50 | models, 51 | actions, 52 | }; 53 | break; 54 | default: 55 | ret = state; 56 | break; 57 | } 58 | return ret; 59 | }, 60 | }; 61 | 62 | function onAction(effect, model, actionType) { 63 | const { namespace } = model; 64 | if ( 65 | (only.length === 0 && except.length === 0) || 66 | (only.length > 0 && only.indexOf(actionType) !== -1) || 67 | (except.length > 0 && except.indexOf(actionType) === -1) 68 | ) { 69 | return async function(action, { dispatch }) { 70 | try{ 71 | await dispatch({ type: SHOW, payload: { namespace, actionType } }); 72 | const result = await effect(...arguments); 73 | await dispatch({ type: HIDE, payload: { namespace, actionType } }); 74 | return result; 75 | }catch(e){ 76 | await dispatch({ type: HIDE, payload: { namespace, actionType } }); 77 | throw e; 78 | } 79 | }; 80 | } else { 81 | return effect; 82 | } 83 | } 84 | 85 | return { 86 | extraReducers, 87 | onAction, 88 | }; 89 | } 90 | 91 | export default createLoading; 92 | -------------------------------------------------------------------------------- /packages/redva-core/test/checkModel.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { create } from '../src/index'; 3 | 4 | describe('checkModel', () => { 5 | it('namespace should be defined', () => { 6 | const app = create(); 7 | expect(() => { 8 | app.model({}); 9 | }).toThrow(/\[app\.model\] namespace should be defined/); 10 | }); 11 | 12 | it('namespace should be unique', () => { 13 | const app = create(); 14 | expect(() => { 15 | app.model({ 16 | namespace: 'repeat', 17 | }); 18 | app.model({ 19 | namespace: 'repeat', 20 | }); 21 | }).toThrow(/\[app\.model\] namespace should be unique/); 22 | }); 23 | 24 | it('reducers can be object', () => { 25 | const app = create(); 26 | expect(() => { 27 | app.model({ 28 | namespace: '_object', 29 | mutations: {}, 30 | }); 31 | }).not.toThrow(); 32 | }); 33 | 34 | it('reducers can not be array', () => { 35 | const app = create(); 36 | expect(() => { 37 | app.model({ 38 | namespace: '_neither', 39 | mutations: [], 40 | }); 41 | }).toThrow(/\[app\.model\] mutations should be plain object/); 42 | }); 43 | 44 | it('reducers can not be string', () => { 45 | const app = create(); 46 | expect(() => { 47 | app.model({ 48 | namespace: '_neither', 49 | mutations: '_', 50 | }); 51 | }).toThrow(/\[app\.model\] mutations should be plain object/); 52 | }); 53 | 54 | it('subscriptions should be plain object', () => { 55 | const app = create(); 56 | expect(() => { 57 | app.model({ 58 | namespace: '_', 59 | subscriptions: [], 60 | }); 61 | }).toThrow(/\[app\.model\] subscriptions should be plain object/); 62 | expect(() => { 63 | app.model({ 64 | namespace: '_', 65 | subscriptions: '_', 66 | }); 67 | }).toThrow(/\[app\.model\] subscriptions should be plain object/); 68 | }); 69 | 70 | it('subscriptions can be undefined', () => { 71 | const app = create(); 72 | expect(() => { 73 | app.model({ 74 | namespace: '_', 75 | }); 76 | }).not.toThrow(); 77 | }); 78 | 79 | it('effects should be plain object', () => { 80 | const app = create(); 81 | expect(() => { 82 | app.model({ 83 | namespace: '_', 84 | actions: [], 85 | }); 86 | }).toThrow(/\[app\.model\] actions should be plain object/); 87 | expect(() => { 88 | app.model({ 89 | namespace: '_', 90 | actions: '_', 91 | }); 92 | }).toThrow(/\[app\.model\] actions should be plain object/); 93 | expect(() => { 94 | app.model({ 95 | namespace: '_', 96 | actions: {}, 97 | }); 98 | }).not.toThrow(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoTextInput.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import TodoTextInput from './TodoTextInput'; 4 | 5 | const setup = propOverrides => { 6 | const props = Object.assign( 7 | { 8 | onSave: jest.fn(), 9 | text: 'Use Redux', 10 | placeholder: 'What needs to be done?', 11 | editing: false, 12 | newTodo: false, 13 | }, 14 | propOverrides 15 | ); 16 | 17 | const renderer = createRenderer(); 18 | 19 | renderer.render(); 20 | 21 | const output = renderer.getRenderOutput(); 22 | 23 | return { 24 | props: props, 25 | output: output, 26 | renderer: renderer, 27 | }; 28 | }; 29 | 30 | describe('components', () => { 31 | describe('TodoTextInput', () => { 32 | it('should render correctly', () => { 33 | const { output } = setup(); 34 | expect(output.props.placeholder).toEqual('What needs to be done?'); 35 | expect(output.props.value).toEqual('Use Redux'); 36 | expect(output.props.className).toEqual(''); 37 | }); 38 | 39 | it('should render correctly when editing=true', () => { 40 | const { output } = setup({ editing: true }); 41 | expect(output.props.className).toEqual('edit'); 42 | }); 43 | 44 | it('should render correctly when newTodo=true', () => { 45 | const { output } = setup({ newTodo: true }); 46 | expect(output.props.className).toEqual('new-todo'); 47 | }); 48 | 49 | it('should update value on change', () => { 50 | const { output, renderer } = setup(); 51 | output.props.onChange({ target: { value: 'Use Radox' } }); 52 | const updated = renderer.getRenderOutput(); 53 | expect(updated.props.value).toEqual('Use Radox'); 54 | }); 55 | 56 | it('should call onSave on return key press', () => { 57 | const { output, props } = setup(); 58 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } }); 59 | expect(props.onSave).toBeCalledWith('Use Redux'); 60 | }); 61 | 62 | it('should reset state on return key press if newTodo', () => { 63 | const { output, renderer } = setup({ newTodo: true }); 64 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } }); 65 | const updated = renderer.getRenderOutput(); 66 | expect(updated.props.value).toEqual(''); 67 | }); 68 | 69 | it('should call onSave on blur', () => { 70 | const { output, props } = setup(); 71 | output.props.onBlur({ target: { value: 'Use Redux' } }); 72 | expect(props.onSave).toBeCalledWith('Use Redux'); 73 | }); 74 | 75 | it('shouldnt call onSave on blur if newTodo', () => { 76 | const { output, props } = setup({ newTodo: true }); 77 | output.props.onBlur({ target: { value: 'Use Redux' } }); 78 | expect(props.onSave).not.toBeCalled(); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /examples/async/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'redva'; 4 | import Picker from '../components/Picker'; 5 | import Posts from '../components/Posts'; 6 | 7 | class App extends Component { 8 | static propTypes = { 9 | selectedSubreddit: PropTypes.string.isRequired, 10 | posts: PropTypes.array.isRequired, 11 | isFetching: PropTypes.bool.isRequired, 12 | lastUpdated: PropTypes.number, 13 | dispatch: PropTypes.func.isRequired, 14 | }; 15 | 16 | componentDidMount() { 17 | const { dispatch, selectedSubreddit } = this.props; 18 | dispatch({ 19 | type: 'postsBySubreddit/fetchPostsIfNeeded', 20 | subreddit: selectedSubreddit, 21 | }); 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) { 26 | const { dispatch, selectedSubreddit } = nextProps; 27 | dispatch({ 28 | type: 'postsBySubreddit/fetchPostsIfNeeded', 29 | subreddit: selectedSubreddit, 30 | }); 31 | } 32 | } 33 | 34 | handleChange = nextSubreddit => { 35 | this.props.dispatch({ 36 | type: 'selectedSubreddit/selectSubreddit', 37 | subreddit: nextSubreddit, 38 | }); 39 | }; 40 | 41 | handleRefreshClick = e => { 42 | e.preventDefault(); 43 | 44 | const { dispatch, selectedSubreddit } = this.props; 45 | dispatch({ 46 | type: 'postsBySubreddit/invalidateSubreddit', 47 | subreddit: selectedSubreddit, 48 | }); 49 | dispatch({ 50 | type: 'postsBySubreddit/fetchPostsIfNeeded', 51 | subreddit: selectedSubreddit, 52 | }); 53 | }; 54 | 55 | render() { 56 | const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props; 57 | const isEmpty = posts.length === 0; 58 | return ( 59 |
    60 | 65 |

    66 | {lastUpdated && ( 67 | 68 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '} 69 | 70 | )} 71 | {!isFetching && ( 72 | 73 | )} 74 |

    75 | {isEmpty ? ( 76 | isFetching ? ( 77 |

    Loading...

    78 | ) : ( 79 |

    Empty.

    80 | ) 81 | ) : ( 82 |
    83 | 84 |
    85 | )} 86 |
    87 | ); 88 | } 89 | } 90 | 91 | const mapStateToProps = state => { 92 | const { selectedSubreddit, postsBySubreddit } = state; 93 | const { isFetching, lastUpdated, items: posts } = postsBySubreddit[ 94 | selectedSubreddit 95 | ] || { 96 | isFetching: true, 97 | items: [], 98 | }; 99 | 100 | return { 101 | selectedSubreddit, 102 | posts, 103 | isFetching, 104 | lastUpdated, 105 | }; 106 | }; 107 | 108 | export default connect(mapStateToProps)(App); 109 | -------------------------------------------------------------------------------- /examples/async-loading/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'redva'; 4 | import Picker from '../components/Picker'; 5 | import Posts from '../components/Posts'; 6 | 7 | class App extends Component { 8 | static propTypes = { 9 | selectedSubreddit: PropTypes.string.isRequired, 10 | posts: PropTypes.array.isRequired, 11 | isFetching: PropTypes.bool.isRequired, 12 | lastUpdated: PropTypes.number, 13 | dispatch: PropTypes.func.isRequired, 14 | }; 15 | 16 | componentDidMount() { 17 | const { dispatch, selectedSubreddit } = this.props; 18 | dispatch({ 19 | type: 'postsBySubreddit/fetchPostsIfNeeded', 20 | subreddit: selectedSubreddit, 21 | }); 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) { 26 | const { dispatch, selectedSubreddit } = nextProps; 27 | dispatch({ 28 | type: 'postsBySubreddit/fetchPostsIfNeeded', 29 | subreddit: selectedSubreddit, 30 | }); 31 | } 32 | } 33 | 34 | handleChange = nextSubreddit => { 35 | this.props.dispatch({ 36 | type: 'selectedSubreddit/selectSubreddit', 37 | subreddit: nextSubreddit, 38 | }); 39 | }; 40 | 41 | handleRefreshClick = e => { 42 | e.preventDefault(); 43 | 44 | const { dispatch, selectedSubreddit } = this.props; 45 | dispatch({ 46 | type: 'postsBySubreddit/invalidateSubreddit', 47 | subreddit: selectedSubreddit, 48 | }); 49 | dispatch({ 50 | type: 'postsBySubreddit/fetchPostsIfNeeded', 51 | subreddit: selectedSubreddit, 52 | }); 53 | }; 54 | 55 | render() { 56 | const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props; 57 | const isEmpty = posts.length === 0; 58 | return ( 59 |
    60 | 65 |

    66 | {lastUpdated && ( 67 | 68 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '} 69 | 70 | )} 71 | {!isFetching && ( 72 | 73 | )} 74 |

    75 | {isEmpty ? ( 76 | isFetching ? ( 77 |

    Loading...

    78 | ) : ( 79 |

    Empty.

    80 | ) 81 | ) : ( 82 |
    83 | 84 |
    85 | )} 86 |
    87 | ); 88 | } 89 | } 90 | 91 | const mapStateToProps = state => { 92 | const { selectedSubreddit, postsBySubreddit } = state; 93 | const isFetching = !!state.loading.actions['postsBySubreddit/fetchPosts']; 94 | const { lastUpdated, items: posts } = postsBySubreddit[ 95 | selectedSubreddit 96 | ] || { 97 | items: [], 98 | }; 99 | 100 | return { 101 | selectedSubreddit, 102 | posts, 103 | isFetching, 104 | lastUpdated, 105 | }; 106 | }; 107 | 108 | export default connect(mapStateToProps)(App); 109 | -------------------------------------------------------------------------------- /docs/Concepts_zh-CN.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | [View this in English](./Concepts.md) 4 | 5 | ## 数据流向 6 | 7 | 数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 `dispatch` 发起一个 action,如果是同步行为会直接通过 `Mutations` 改变 `State` ,如果是异步行为(副作用)会先触发 `Actions` 然后流向 `Mutations` 最终改变 `State`,所以在 redva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。 8 | 9 | 10 | 11 | ## Models 12 | 13 | ### State 14 | 15 | `type State = any` 16 | 17 | State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。 18 | 19 | 在 redva 中你可以通过 redva 的实例属性 `_store` 看到顶部的 state 数据,但是通常你很少会用到: 20 | 21 | ```javascript 22 | const app = redva(); 23 | console.log(app._store); // 顶部的 state 数据 24 | ``` 25 | 26 | ### Action 27 | 28 | `type AsyncAction = any` 29 | 30 | Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 `type` 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 `dispatch` 函数;需要注意的是 `dispatch` 是在组件 connect Models以后,通过 props 传入的。 31 | ``` 32 | dispatch({ 33 | type: 'add', 34 | }); 35 | ``` 36 | 37 | ### dispatch 函数 38 | 39 | `type dispatch = (a: Action) => Action` 40 | 41 | dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。 42 | 43 | 在 redva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Mutations 或者 Actions,常见的形式如: 44 | 45 | ```javascript 46 | dispatch({ 47 | type: 'user/add', // 如果在 model 外调用,需要添加 namespace 48 | payload: {}, // 需要传递的信息 49 | }); 50 | ``` 51 | 52 | ### Mutation 53 | 54 | `type Mutation = (state: S, action: A)` 55 | 56 | Mutation函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,该函数直接修改之前已经累积运算的结果就可以了。 57 | 58 | ### Action 59 | 60 | Action 被称为副作用,在我们的应用中,最常见的就是异步操作。 61 | 62 | dva 为了控制副作用的操作,底层引入了[async/await](http://babeljs.io/docs/plugins/syntax-async-functions)做异步流程控制,所以将异步转成同步写法。 63 | 64 | ### Subscription 65 | 66 | Subscriptions 是一种从 __源__ 获取数据的方法,它来自于 elm。 67 | 68 | Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。 69 | 70 | ```javascript 71 | import key from 'keymaster'; 72 | ... 73 | app.model({ 74 | namespace: 'count', 75 | subscriptions: { 76 | keyEvent(dispatch) { 77 | key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) }); 78 | }, 79 | } 80 | }); 81 | ``` 82 | 83 | ## Router 84 | 85 | 这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 [History API](http://mdn.beonex.com/en/DOM/window.history.html) 可以监听浏览器url的变化,从而控制路由相关操作。 86 | 87 | redva 实例提供了 router 方法来控制路由,使用的是[react-router](https://github.com/reactjs/react-router)。 88 | 89 | ```javascript 90 | import { Router, Route } from 'redva/router'; 91 | app.router(({history}) => 92 | 93 | 94 | 95 | ); 96 | ``` 97 | 98 | ## Route Components 99 | 100 | 所以在 redva 中,通常需要 connect Model的组件都是 Route Components,组织在`/routes/`目录下,而`/components/`目录下则是纯组件(Presentational Components)。 101 | 102 | ## 参考引申 103 | 104 | - [redux docs](http://redux.js.org/docs/Glossary.html) 105 | - [es6](http://babeljs.io/) 106 | - [vuex](https://vuex.vuejs.org/) 107 | - [dva](https://github.com/dvajs/dva) 108 | -------------------------------------------------------------------------------- /packages/redva/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import invariant from 'invariant'; 3 | import createHashHistory from 'history/createHashHistory'; 4 | import { routerMiddleware, routerReducer as routing } from 'react-router-redux'; 5 | import document from 'global/document'; 6 | import { Provider } from 'react-redux'; 7 | import * as core from 'redva-core'; 8 | import { isFunction } from 'redva-core/lib/utils'; 9 | 10 | export default function(opts = {}) { 11 | const history = opts.history || createHashHistory(); 12 | const createOpts = { 13 | initialReducer: { 14 | routing, 15 | }, 16 | setupMiddlewares(middlewares) { 17 | return [routerMiddleware(history), ...middlewares]; 18 | }, 19 | setupApp(app) { 20 | app._history = patchHistory(history); 21 | }, 22 | }; 23 | 24 | const app = core.create(opts, createOpts); 25 | const oldAppStart = app.start; 26 | app.router = router; 27 | app.start = start; 28 | return app; 29 | 30 | function router(router) { 31 | invariant( 32 | isFunction(router), 33 | `[app.router] router should be function, but got ${typeof router}` 34 | ); 35 | app._router = router; 36 | } 37 | 38 | function start(container) { 39 | // 允许 container 是字符串,然后用 querySelector 找元素 40 | if (isString(container)) { 41 | container = document.querySelector(container); 42 | invariant(container, `[app.start] container ${container} not found`); 43 | } 44 | 45 | // 并且是 HTMLElement 46 | invariant( 47 | !container || isHTMLElement(container), 48 | `[app.start] container should be HTMLElement` 49 | ); 50 | 51 | // 路由必须提前注册 52 | invariant( 53 | app._router, 54 | `[app.start] router must be registered before app.start()` 55 | ); 56 | 57 | if (!app._store) { 58 | oldAppStart.call(app); 59 | } 60 | const store = app._store; 61 | 62 | // export _getProvider for HMR 63 | // ref: https://github.com/dvajs/dva/issues/469 64 | app._getProvider = getProvider.bind(null, store, app); 65 | 66 | // If has container, render; else, return react component 67 | if (container) { 68 | render(container, store, app, app._router); 69 | app._plugin.apply('onHmr')(render.bind(null, container, store, app)); 70 | } else { 71 | return getProvider(store, this, this._router); 72 | } 73 | } 74 | } 75 | 76 | function isHTMLElement(node) { 77 | return ( 78 | typeof node === 'object' && node !== null && node.nodeType && node.nodeName 79 | ); 80 | } 81 | 82 | function isString(str) { 83 | return typeof str === 'string'; 84 | } 85 | 86 | function getProvider(store, app, router) { 87 | const RedvaRoot = extraProps => ( 88 | 89 | {router({ app, history: app._history, ...extraProps })} 90 | 91 | ); 92 | return RedvaRoot; 93 | } 94 | 95 | function render(container, store, app, router) { 96 | const ReactDOM = require('react-dom'); // eslint-disable-line 97 | ReactDOM.render( 98 | React.createElement(getProvider(store, app, router)), 99 | container 100 | ); 101 | } 102 | 103 | function patchHistory(history) { 104 | const oldListen = history.listen; 105 | history.listen = callback => { 106 | callback(history.location); 107 | return oldListen.call(history, callback); 108 | }; 109 | return history; 110 | } 111 | -------------------------------------------------------------------------------- /packages/redva-core/test/subscriptions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { create } from '../src/index'; 3 | 4 | describe('subscriptions', () => { 5 | it('dispatch action', () => { 6 | const app = create(); 7 | app.model({ 8 | namespace: 'count', 9 | state: 0, 10 | mutations: { 11 | add(state, { payload }) { 12 | state.count += payload || 1; 13 | }, 14 | }, 15 | subscriptions: { 16 | setup({ dispatch }) { 17 | dispatch({ type: 'add', payload: 2 }); 18 | }, 19 | }, 20 | }); 21 | app.start(); 22 | expect(app._store.getState().count).toEqual(2); 23 | }); 24 | 25 | it('dispatch action with namespace will get a warn', () => { 26 | const app = create(); 27 | app.model({ 28 | namespace: 'count', 29 | state: 0, 30 | mutations: { 31 | add(state, { payload }) { 32 | state.count += payload || 1; 33 | }, 34 | }, 35 | subscriptions: { 36 | setup({ dispatch }) { 37 | dispatch({ type: 'count/add', payload: 2 }); 38 | }, 39 | }, 40 | }); 41 | app.start(); 42 | expect(app._store.getState().count).toEqual(2); 43 | }); 44 | 45 | it('dispatch not valid action', () => { 46 | const app = create(); 47 | app.model({ 48 | namespace: 'count', 49 | state: 0, 50 | subscriptions: { 51 | setup({ dispatch }) { 52 | dispatch('add'); 53 | }, 54 | }, 55 | }); 56 | expect(() => { 57 | app.start(); 58 | }).toThrow(/dispatch: action should be a plain Object with type/); 59 | }); 60 | 61 | it('dispatch action for other models', () => { 62 | const app = create(); 63 | app.model({ 64 | namespace: 'loading', 65 | state: false, 66 | mutations: { 67 | show(state) { 68 | state.loading = true; 69 | }, 70 | }, 71 | }); 72 | app.model({ 73 | namespace: 'count', 74 | state: 0, 75 | subscriptions: { 76 | setup({ dispatch }) { 77 | dispatch({ type: 'loading/show' }); 78 | }, 79 | }, 80 | }); 81 | app.start(); 82 | expect(app._store.getState().loading).toEqual(true); 83 | }); 84 | 85 | it('onError', () => { 86 | const errors = []; 87 | const app = create({ 88 | onError: error => { 89 | errors.push(error.message); 90 | }, 91 | }); 92 | app.model({ 93 | namespace: '-', 94 | state: {}, 95 | subscriptions: { 96 | setup(_obj, done) { 97 | done('subscription error'); 98 | }, 99 | }, 100 | }); 101 | app.start(); 102 | expect(errors).toEqual(['subscription error']); 103 | }); 104 | 105 | it('onError async', done => { 106 | const errors = []; 107 | const app = create({ 108 | onError: error => { 109 | errors.push(error.message); 110 | }, 111 | }); 112 | app.model({ 113 | namespace: '-', 114 | state: {}, 115 | subscriptions: { 116 | setup(_obj, done) { 117 | setTimeout(() => { 118 | done('subscription error'); 119 | }, 100); 120 | }, 121 | }, 122 | }); 123 | app.start(); 124 | expect(errors).toEqual([]); 125 | setTimeout(() => { 126 | expect(errors).toEqual(['subscription error']); 127 | done(); 128 | }, 200); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /packages/redva/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Reducer, 3 | AnyAction, 4 | ReducersMapObject, 5 | Dispatch, 6 | MiddlewareAPI, 7 | StoreEnhancer 8 | } from 'redux'; 9 | 10 | import { History } from "history"; 11 | 12 | export interface onActionFunc { 13 | (api: MiddlewareAPI): void; 14 | } 15 | 16 | export interface ReducerEnhancer { 17 | (reducer: Reducer): void 18 | } 19 | 20 | export interface Hooks { 21 | onError?: (e: Error, dispatch: Dispatch) => void; 22 | onAction?: onActionFunc | onActionFunc[]; 23 | onStateChange?: () => void; 24 | onReducer?: ReducerEnhancer; 25 | onEffect?: () => void; 26 | onHmr?: () => void; 27 | extraReducers?: ReducersMapObject; 28 | extraEnhancers?: StoreEnhancer[]; 29 | } 30 | 31 | export type DvaOption = Hooks & { 32 | initialState?: Object; 33 | history?: Object; 34 | } 35 | 36 | export interface EffectsCommandMap { 37 | put: (action: A) => any; 38 | call: Function; 39 | select: Function; 40 | take: Function; 41 | cancel: Function; 42 | [key: string]: any; 43 | } 44 | 45 | export type Effect = (action: AnyAction, effects: EffectsCommandMap) => void; 46 | export type EffectType = 'takeEvery' | 'takeLatest' | 'watcher' | 'throttle'; 47 | export type EffectWithType = [Effect, { type : EffectType }]; 48 | export type Subscription = (api: SubscriptionAPI, done: Function) => void; 49 | export type ReducersMapObjectWithEnhancer = [ReducersMapObject, ReducerEnhancer]; 50 | 51 | export interface EffectsMapObject { 52 | [key: string]: Effect | EffectWithType; 53 | } 54 | 55 | export interface SubscriptionAPI { 56 | history: History; 57 | dispatch: Dispatch; 58 | } 59 | 60 | export interface SubscriptionsMapObject { 61 | [key: string]: Subscription; 62 | } 63 | 64 | export interface Model { 65 | namespace: string, 66 | state?: any, 67 | reducers?: ReducersMapObject | ReducersMapObjectWithEnhancer, 68 | effects?: EffectsMapObject, 69 | subscriptions?: SubscriptionsMapObject, 70 | } 71 | 72 | export interface RouterAPI { 73 | history: History; 74 | app: DvaInstance; 75 | } 76 | 77 | export interface Router { 78 | (api?: RouterAPI): JSX.Element | Object; 79 | } 80 | 81 | export interface DvaInstance { 82 | /** 83 | * Register an object of hooks on the application. 84 | * 85 | * @param hooks 86 | */ 87 | use: (hooks: Hooks) => void, 88 | 89 | /** 90 | * Register a model. 91 | * 92 | * @param model 93 | */ 94 | model: (model: Model) => void, 95 | 96 | /** 97 | * Unregister a model. 98 | * 99 | * @param namespace 100 | */ 101 | unmodel: (namespace: string) => void, 102 | 103 | /** 104 | * Config router. Takes a function with arguments { history, dispatch }, 105 | * and expects router config. It use the same api as react-router, 106 | * return jsx elements or JavaScript Object for dynamic routing. 107 | * 108 | * @param router 109 | */ 110 | router: (router: Router) => void, 111 | 112 | /** 113 | * Start the application. Selector is optional. If no selector 114 | * arguments, it will return a function that return JSX elements. 115 | * 116 | * @param selector 117 | */ 118 | start: (selector?: HTMLElement | string) => any, 119 | } 120 | 121 | export default function dva(opts?: DvaOption): DvaInstance; 122 | 123 | /** 124 | * Connects a React component to Dva. 125 | */ 126 | export function connect( 127 | mapStateToProps?: Function, 128 | mapDispatchToProps?: Function, 129 | mergeProps?: Function, 130 | options?: Object 131 | ): Function; 132 | -------------------------------------------------------------------------------- /packages/redva/test/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import redva from '../src/index'; 4 | 5 | const countModel = { 6 | namespace: 'count', 7 | state: 0, 8 | mutations: { 9 | add(state, { payload }) { 10 | state.count += payload || 1; 11 | }, 12 | minus(state, { payload }) { 13 | state.count -= -payload || 1; 14 | }, 15 | }, 16 | }; 17 | 18 | describe('index', () => { 19 | xit('normal', () => { 20 | const app = redva(); 21 | app.model({ ...countModel }); 22 | app.router(() =>
    ); 23 | app.start('#root'); 24 | }); 25 | 26 | it('start without container', () => { 27 | const app = redva(); 28 | app.model({ ...countModel }); 29 | app.router(() =>
    ); 30 | app.start(); 31 | }); 32 | 33 | it('throw error if no routes defined', () => { 34 | const app = redva(); 35 | expect(() => { 36 | app.start(); 37 | }).toThrow(/router must be registered before app.start/); 38 | }); 39 | 40 | it('opts.initialState', () => { 41 | const app = redva({ 42 | initialState: { count: 1 }, 43 | }); 44 | app.model({ ...countModel }); 45 | app.router(() =>
    ); 46 | app.start(); 47 | expect(app._store.getState().count).toEqual(1); 48 | }); 49 | 50 | it('opts.extraMiddlewares', () => { 51 | let count; 52 | const countMiddleware = () => () => () => { 53 | count += 1; 54 | }; 55 | 56 | const app = redva({ 57 | extraMiddlewares: countMiddleware, 58 | }); 59 | app.router(() =>
    ); 60 | app.start(); 61 | 62 | count = 0; 63 | app._store.dispatch({ type: 'test' }); 64 | expect(count).toEqual(1); 65 | }); 66 | 67 | it('opts.extraMiddlewares with array', () => { 68 | let count; 69 | const countMiddleware = () => next => action => { 70 | count += 1; 71 | next(action); 72 | }; 73 | const count2Middleware = () => next => action => { 74 | count += 2; 75 | next(action); 76 | }; 77 | 78 | const app = redva({ 79 | extraMiddlewares: [countMiddleware, count2Middleware], 80 | }); 81 | app.router(() =>
    ); 82 | app.start(); 83 | 84 | count = 0; 85 | app._store.dispatch({ type: 'test' }); 86 | expect(count).toEqual(3); 87 | }); 88 | 89 | it('opts.extraEnhancers', () => { 90 | let count = 0; 91 | const countEnhancer = storeCreator => ( 92 | reducer, 93 | preloadedState, 94 | enhancer 95 | ) => { 96 | const store = storeCreator(reducer, preloadedState, enhancer); 97 | const oldDispatch = store.dispatch; 98 | store.dispatch = action => { 99 | count += 1; 100 | oldDispatch(action); 101 | }; 102 | return store; 103 | }; 104 | const app = redva({ 105 | extraEnhancers: [countEnhancer], 106 | }); 107 | app.router(() =>
    ); 108 | app.start(); 109 | 110 | app._store.dispatch({ type: 'test' }); 111 | expect(count).toEqual(1); 112 | }); 113 | 114 | it('opts.onStateChange', () => { 115 | let savedState = null; 116 | 117 | const app = redva({ 118 | onStateChange(state) { 119 | savedState = state; 120 | }, 121 | }); 122 | app.model({ 123 | namespace: 'count', 124 | state: 0, 125 | mutations: { 126 | add(state) { 127 | state.count += 1; 128 | }, 129 | }, 130 | }); 131 | app.router(() =>
    ); 132 | app.start(); 133 | 134 | app._store.dispatch({ type: 'count/add' }); 135 | expect(savedState.count).toEqual(1); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Footer.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import Footer from './Footer'; 4 | import FilterLink from '../containers/FilterLink'; 5 | import { 6 | SHOW_ALL, 7 | SHOW_ACTIVE, 8 | SHOW_COMPLETED, 9 | } from '../constants/TodoFilters'; 10 | 11 | const setup = propOverrides => { 12 | const props = Object.assign( 13 | { 14 | completedCount: 0, 15 | activeCount: 0, 16 | onClearCompleted: jest.fn(), 17 | }, 18 | propOverrides 19 | ); 20 | 21 | const renderer = createRenderer(); 22 | renderer.render(