├── .prettierignore ├── .npmignore ├── examples ├── counter │ ├── .babelrc │ ├── src │ │ ├── Actions │ │ │ ├── types.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── index.html │ │ ├── Reducers │ │ │ └── Counter.js │ │ └── Components │ │ │ ├── Counter.vue │ │ │ └── CounterProvider.vue │ └── package.json └── counterJsx │ ├── .babelrc │ ├── src │ ├── Actions │ │ ├── types.js │ │ └── index.js │ ├── index.js │ ├── index.html │ ├── Reducers │ │ └── Counter.js │ └── Components │ │ ├── Counter.vue │ │ └── CounterProvider.jsx │ └── package.json ├── .editorconfig ├── .gitignore ├── .prettierrc.js ├── tests ├── test.vue ├── testTwoStore.vue ├── provider.spec.js ├── StoreProvider.vue └── TwoStoreProvider.vue ├── .travis.yml ├── .babelrc ├── rollup.config.js ├── index.js ├── package.json ├── bundle.es.js ├── bundle.js └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | bundle.es.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | examples/ 3 | tests/ -------------------------------------------------------------------------------- /examples/counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space -------------------------------------------------------------------------------- /examples/counterJsx/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@vue/babel-preset-jsx"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | # 3 | node_modules/ 4 | yarn* 5 | .idea/ 6 | .cache/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | } 7 | -------------------------------------------------------------------------------- /examples/counter/src/Actions/types.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT = 'INCREMENT' 2 | export const DECREMENT = 'DECREMENT' 3 | export const RESET = 'RESET' 4 | -------------------------------------------------------------------------------- /examples/counterJsx/src/Actions/types.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT = 'INCREMENT' 2 | export const DECREMENT = 'DECREMENT' 3 | export const RESET = 'RESET' 4 | -------------------------------------------------------------------------------- /tests/test.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /examples/counter/src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import CounterProvider from './Components/CounterProvider.vue' 3 | 4 | new Vue({ 5 | components: { CounterProvider }, 6 | render: h => h(CounterProvider), 7 | }).$mount('#app') 8 | -------------------------------------------------------------------------------- /examples/counterJsx/src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import CounterProvider from './Components/CounterProvider.jsx' 3 | 4 | new Vue({ 5 | components: { CounterProvider }, 6 | render: h => h(CounterProvider), 7 | }).$mount('#app') 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/getting-started/ 2 | language: node_js 3 | 4 | node_js: 5 | - '10' 6 | script: yarn && yarn test && pushd ./examples/counter && yarn && yarn build && popd && pushd ./examples/counterJsx && yarn && yarn build && popd 7 | -------------------------------------------------------------------------------- /examples/counter/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counter 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/counterJsx/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counter 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/testTwoStore.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "ie": "11" 8 | }, 9 | "modules": false 10 | } 11 | ] 12 | ], 13 | "env": { 14 | "testing": { 15 | "presets": ["@babel/env"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/counter/src/Actions/index.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT, RESET } from './types.js' 2 | export function increment() { 3 | return { type: INCREMENT } 4 | } 5 | 6 | export function decrement() { 7 | return { type: DECREMENT } 8 | } 9 | 10 | export function reset() { 11 | return { type: RESET } 12 | } 13 | -------------------------------------------------------------------------------- /examples/counterJsx/src/Actions/index.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT, RESET } from './types' 2 | 3 | export function increment() { 4 | return { type: INCREMENT } 5 | } 6 | 7 | export function decrement() { 8 | return { type: DECREMENT } 9 | } 10 | 11 | export function reset() { 12 | return { type: RESET } 13 | } 14 | -------------------------------------------------------------------------------- /examples/counter/src/Reducers/Counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT, RESET } from '../Actions/types' 2 | 3 | export function counter(state = 0, action) { 4 | switch (action.type) { 5 | case INCREMENT: 6 | return state + 1 7 | case DECREMENT: 8 | return state - 1 9 | case RESET: 10 | return 0 11 | default: 12 | return state 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/counterJsx/src/Reducers/Counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT, RESET } from '../Actions/types' 2 | 3 | export function counter(state = 0, action) { 4 | switch (action.type) { 5 | case INCREMENT: 6 | return state + 1 7 | case DECREMENT: 8 | return state - 1 9 | case RESET: 10 | return 0 11 | default: 12 | return state 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/counter/src/Components/Counter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /examples/counterJsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter-jsx", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "author": "Titouan CREACH", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "parcel ./src/index.html", 9 | "build": "parcel build ./src/index.html" 10 | }, 11 | "devDependencies": { 12 | "parcel-bundler": "^1.12.3", 13 | "redux": "^4.0.1", 14 | "vue": "^2.6.10" 15 | }, 16 | "dependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /examples/counterJsx/src/Components/Counter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "author": "Titouan CREACH", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "parcel ./src/index.html", 9 | "build": "parcel build ./src/index.html" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.4.4", 13 | "parcel-bundler": "^1.12.3", 14 | "redux": "^3.7.2", 15 | "vue": "^2.5.8" 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | 3 | export default [ 4 | { 5 | input: 'index.js', 6 | output: { 7 | file: 'bundle.js', 8 | format: 'cjs', 9 | }, 10 | plugins: [ 11 | babel({ 12 | exclude: 'node_modules/**', 13 | }), 14 | ], 15 | }, 16 | { 17 | input: 'index.js', 18 | output: { 19 | file: 'bundle.es.js', 20 | format: 'es', 21 | }, 22 | plugins: [ 23 | babel({ 24 | exclude: 'node_modules/**', 25 | }), 26 | ], 27 | }, 28 | ] 29 | -------------------------------------------------------------------------------- /tests/provider.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import StoreProvider from './StoreProvider.vue' 3 | import TwoStoreProvider from './TwoStoreProvider.vue' 4 | 5 | test('Test wrapper component', () => { 6 | const testProvider = { 7 | components: { StoreProvider }, 8 | render: h => h(StoreProvider), 9 | } 10 | 11 | const vm = new Vue(testProvider).$mount() 12 | }) 13 | 14 | test('Test wrapper component', () => { 15 | const testProvider = { 16 | components: { TwoStoreProvider }, 17 | render: h => h(TwoStoreProvider), 18 | } 19 | 20 | const vm = new Vue(testProvider).$mount() 21 | }) 22 | -------------------------------------------------------------------------------- /tests/StoreProvider.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | mapDispatchToProps: { 4 | required: false, 5 | default: () => ({}), 6 | type: Function, 7 | }, 8 | 9 | mapStateToProps: { 10 | required: false, 11 | default: () => ({}), 12 | type: Function, 13 | }, 14 | 15 | store: { 16 | required: true, 17 | type: Object, 18 | }, 19 | }, 20 | 21 | data: ctx => ({ 22 | state: ctx.store.getState(), 23 | }), 24 | 25 | created() { 26 | this.unsubscribe = this.store.subscribe(() => { 27 | this.state = this.store.getState() 28 | }) 29 | }, 30 | 31 | destroyed() { 32 | this.unsubscribe() 33 | }, 34 | 35 | render() { 36 | const nodes = this.$scopedSlots.default({ 37 | ...this.mapDispatchToProps(this.store.dispatch), 38 | ...this.mapStateToProps(this.state), 39 | }) 40 | if (Array.isArray(nodes)) { 41 | return nodes[0] 42 | } else { 43 | return nodes 44 | } 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /examples/counter/src/Components/CounterProvider.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 42 | -------------------------------------------------------------------------------- /examples/counterJsx/src/Components/CounterProvider.jsx: -------------------------------------------------------------------------------- 1 | import { createStore, bindActionCreators } from 'redux' 2 | import Provider from '../../../../bundle.js' 3 | import * as Actions from '../Actions' 4 | import Counter from './Counter.vue' 5 | import { counter } from '../Reducers/Counter' 6 | 7 | export default { 8 | methods: { 9 | mapStateToProps(state) { 10 | return { counterValue: state } 11 | }, 12 | 13 | mapDispatchToProps(dispatch) { 14 | return { actions: bindActionCreators(Actions, dispatch) } 15 | }, 16 | }, 17 | 18 | components: { 19 | Counter, 20 | Provider, 21 | }, 22 | 23 | render(h) { 24 | return ( 25 | 30 | {({ actions, counterValue }) => ( 31 | 36 | )} 37 | 38 | ) 39 | }, 40 | 41 | data: () => ({ 42 | store: createStore(counter), 43 | title: 'Counter using vuejs-redux', 44 | }), 45 | } 46 | -------------------------------------------------------------------------------- /tests/TwoStoreProvider.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-redux", 3 | "version": "2.4.1", 4 | "scripts": { 5 | "build": "NODE_ENV=production && rollup --config rollup.config.js", 6 | "format": "prettier --config ./.prettierrc.js --write './**/*.{js,vue,jsx}'", 7 | "jest": "NODE_ENV=testing jest", 8 | "lint": "prettier --config ./.prettierrc.js --list-different './**/*.{js,vue}'", 9 | "precommit": "lint-staged", 10 | "prepublish": "yarn run build", 11 | "test": "yarn lint && yarn jest" 12 | }, 13 | "description": "Simple but functional binding between Vuejs and Redux", 14 | "main": "bundle.js", 15 | "module": "bundle.es.js", 16 | "repository": "https://github.com/titouancreach/vuejs-redux", 17 | "author": "Titouan CREACH ", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@babel/cli": "^7.4.4", 21 | "@babel/core": "^7.4.4", 22 | "@babel/preset-env": "^7.4.4", 23 | "babel-jest": "^24.8.0", 24 | "husky": "^2.2.0", 25 | "jest": "^29.2.0", 26 | "lint-staged": "^8.1.6", 27 | "prettier": "1.17.0", 28 | "redux": "^4.0.1", 29 | "rollup": "^1.11.3", 30 | "rollup-plugin-babel": "^4.3.2", 31 | "vue": "^2.6.10", 32 | "vue-jest": "^4.0.0-beta.2", 33 | "vue-loader": "^15.7.0", 34 | "vue-template-compiler": "^2.6.10" 35 | }, 36 | "jest": { 37 | "moduleFileExtensions": [ 38 | "js", 39 | "vue" 40 | ], 41 | "transform": { 42 | "^.+\\.js$": "/node_modules/babel-jest", 43 | "^.+\\.vue$": "/node_modules/vue-jest" 44 | } 45 | }, 46 | "lint-staged": { 47 | "*.{js,vue}": [ 48 | "prettier --config ./.prettierrc.js --write", 49 | "git add" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bundle.es.js: -------------------------------------------------------------------------------- 1 | function _defineProperty(obj, key, value) { 2 | if (key in obj) { 3 | Object.defineProperty(obj, key, { 4 | value: value, 5 | enumerable: true, 6 | configurable: true, 7 | writable: true 8 | }); 9 | } else { 10 | obj[key] = value; 11 | } 12 | 13 | return obj; 14 | } 15 | 16 | function _objectSpread(target) { 17 | for (var i = 1; i < arguments.length; i++) { 18 | var source = arguments[i] != null ? arguments[i] : {}; 19 | var ownKeys = Object.keys(source); 20 | 21 | if (typeof Object.getOwnPropertySymbols === 'function') { 22 | ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { 23 | return Object.getOwnPropertyDescriptor(source, sym).enumerable; 24 | })); 25 | } 26 | 27 | ownKeys.forEach(function (key) { 28 | _defineProperty(target, key, source[key]); 29 | }); 30 | } 31 | 32 | return target; 33 | } 34 | 35 | var index = { 36 | props: { 37 | mapDispatchToProps: { 38 | required: false, 39 | default: function _default() { 40 | return {}; 41 | }, 42 | type: Function 43 | }, 44 | mapStateToProps: { 45 | required: false, 46 | default: function _default() { 47 | return {}; 48 | }, 49 | type: Function 50 | }, 51 | store: { 52 | required: true, 53 | type: Object 54 | } 55 | }, 56 | data: function data(ctx) { 57 | return { 58 | state: ctx.store.getState() 59 | }; 60 | }, 61 | created: function created() { 62 | var _this = this; 63 | 64 | this.unsubscribe = this.store.subscribe(function () { 65 | _this.state = _this.store.getState(); 66 | }); 67 | }, 68 | destroyed: function destroyed() { 69 | this.unsubscribe(); 70 | }, 71 | render: function render() { 72 | var nodes = this.$scopedSlots.default(_objectSpread({}, this.mapDispatchToProps(this.store.dispatch), this.mapStateToProps(this.state))); 73 | 74 | if (Array.isArray(nodes)) { 75 | return nodes[0]; 76 | } else { 77 | return nodes; 78 | } 79 | } 80 | }; 81 | 82 | export default index; 83 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _defineProperty(obj, key, value) { 4 | if (key in obj) { 5 | Object.defineProperty(obj, key, { 6 | value: value, 7 | enumerable: true, 8 | configurable: true, 9 | writable: true 10 | }); 11 | } else { 12 | obj[key] = value; 13 | } 14 | 15 | return obj; 16 | } 17 | 18 | function _objectSpread(target) { 19 | for (var i = 1; i < arguments.length; i++) { 20 | var source = arguments[i] != null ? arguments[i] : {}; 21 | var ownKeys = Object.keys(source); 22 | 23 | if (typeof Object.getOwnPropertySymbols === 'function') { 24 | ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { 25 | return Object.getOwnPropertyDescriptor(source, sym).enumerable; 26 | })); 27 | } 28 | 29 | ownKeys.forEach(function (key) { 30 | _defineProperty(target, key, source[key]); 31 | }); 32 | } 33 | 34 | return target; 35 | } 36 | 37 | var index = { 38 | props: { 39 | mapDispatchToProps: { 40 | required: false, 41 | default: function _default() { 42 | return {}; 43 | }, 44 | type: Function 45 | }, 46 | mapStateToProps: { 47 | required: false, 48 | default: function _default() { 49 | return {}; 50 | }, 51 | type: Function 52 | }, 53 | store: { 54 | required: true, 55 | type: Object 56 | } 57 | }, 58 | data: function data(ctx) { 59 | return { 60 | state: ctx.store.getState() 61 | }; 62 | }, 63 | created: function created() { 64 | var _this = this; 65 | 66 | this.unsubscribe = this.store.subscribe(function () { 67 | _this.state = _this.store.getState(); 68 | }); 69 | }, 70 | destroyed: function destroyed() { 71 | this.unsubscribe(); 72 | }, 73 | render: function render() { 74 | var nodes = this.$scopedSlots.default(_objectSpread({}, this.mapDispatchToProps(this.store.dispatch), this.mapStateToProps(this.state))); 75 | 76 | if (Array.isArray(nodes)) { 77 | return nodes[0]; 78 | } else { 79 | return nodes; 80 | } 81 | } 82 | }; 83 | 84 | module.exports = index; 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuejs-redux 2 | 3 | [![npm version](https://badge.fury.io/js/vuejs-redux.svg)](https://badge.fury.io/js/vuejs-redux) 4 | [![Build Status](https://travis-ci.com/titouancreach/vuejs-redux.svg?branch=master)](https://travis-ci.com/titouancreach/vuejs-redux) 5 | [![GitHub contributors](https://img.shields.io/github/contributors/titouancreach/vuejs-redux.svg)](https://github.com/titouancreach/vuejs-redux/graphs/contributors/) 6 | [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/titouancreach) 7 | 8 | ## Description 9 | 10 | Flexible binding between Vue and Redux, allowing use of multiple stores. 11 | It works, in the same way, like render props does in React. It uses Scoped Slot - [read my article about it](https://medium.com/@titouan.creach_44544/emulate-render-props-in-vuejs-c14086dc8dfa). 12 | 13 | _Note:_ 14 | The previous version was using Higher Order Components (HOC); this version uses Scoped slots instead. 15 | No more magic with the connect methods. Everything is explicit which will prevent props collision 16 | and an [ugly trick with the render function](https://github.com/vuejs/vue/issues/6201). 17 | 18 | Why you should use it: 19 | 20 | - Just 45 lines of code. 21 | - No dependencies at all 22 | - Easy to read, understand, and extend. 23 | - Same API as [react-redux](https://github.com/reactjs/react-redux). 24 | - Combine multiple Providers to be populated by multiple sources. 25 | - No hard coded dependencies between 'Vue' and the store, so more composable. 26 | - Doesn't polluate `data`, so you can use the power of the `functional component` 27 | - Debuggable in the [Vue devtool browser extension](https://github.com/vuejs/vue-devtools). 28 | - Elegant JSX syntax. 29 | 30 | # Table of Contents 31 | 32 | - [vuejs-redux](#vuejs-redux) 33 | - [Description](#description) 34 | - [Install](#install) 35 | - [Counter example](#counter-example) 36 | - [Multiple stores](#multiple-stores) 37 | - [Avoid passing the store to every <Provider ...>](#avoid-passing-the-store-to-every-provider-) 38 | - [Running the examples locally](#running-the-examples-locally) 39 | - [Testing](#testing) 40 | - [Rematch](#rematch) 41 | - [Live examples](#live-examples) 42 | - [CONTRIBUTING](#contributing) 43 | 44 | Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) 45 | 46 | ## Install 47 | 48 | ``` 49 | npm install --save vuejs-redux 50 | ``` 51 | 52 | ## Counter example 53 | 54 | Let's build a counter app. The full code can be found in the `example/` directory. 55 | 56 | Start with a reducer: 57 | 58 | ```javascript 59 | export function counter(state = 0, action) { 60 | switch (action.type) { 61 | case 'INCREMENT': 62 | return state + 1 63 | case 'DECREMENT': 64 | return state - 1 65 | case 'RESET': 66 | return 0 67 | default: 68 | return state 69 | } 70 | } 71 | ``` 72 | 73 | Create the action creators in order to update our state: 74 | 75 | ```javascript 76 | export function increment() { 77 | return { type: 'INCREMENT' } 78 | } 79 | 80 | export function decrement() { 81 | return { type: 'DECREMENT' } 82 | } 83 | 84 | export function reset() { 85 | return { type: 'RESET' } 86 | } 87 | ``` 88 | 89 | We can now create the CounterProvider component. It acts as a Provider for our CounterComponent: 90 | 91 | ```vue 92 | 105 | 106 | 137 | ``` 138 | 139 | And finally our Counter component: 140 | 141 | ```vue 142 | 152 | 153 | 158 | ``` 159 | 160 | The Counter component is not aware that we are using redux. 161 | 162 | If you use JSX, you can use the same syntax as React render props: 163 | 164 | ```jsx 165 | render(h) { 166 | return ( 167 | 168 | {({actions, counterValue}) => ( 169 | 170 | )} 171 | 172 | ); 173 | }, 174 | ``` 175 | 176 | ## Multiple stores 177 | 178 | You can combine multiple store if needed, use the Provider component various times. 179 | You can obviously create an helper component or whatever to compose this. 180 | 181 | ```vue 182 |