├── .babelrc ├── lib ├── index.js ├── dispatcher.js ├── buildReducer.js └── withState.js ├── __tests__ ├── fake │ └── TestComponent.jsx ├── buildReducer.test.js ├── __snapshots__ │ └── withState.test.js.snap ├── dispatcher.test.js └── withState.test.js ├── demo ├── index.html ├── webpack.config.js └── App.jsx ├── dist ├── dispatcher.js ├── index.js ├── buildReducer.js └── withState.js ├── .gitignore ├── .circleci └── config.yml ├── package.json └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import withState from './withState' 2 | import dispatcher from './dispatcher' 3 | import buildReducer from './buildReducer' 4 | 5 | export {withState, dispatcher, buildReducer} 6 | 7 | -------------------------------------------------------------------------------- /__tests__/fake/TestComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import withState from '../../lib/withState' 3 | 4 | function Foo() { 5 | return
Hello
6 | } 7 | 8 | export default withState([], {})(Foo) 9 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | T-Redux sample 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /__tests__/buildReducer.test.js: -------------------------------------------------------------------------------- 1 | import buildReducer from '../lib/buildReducer' 2 | 3 | test('should return a funtion', () => { 4 | const reducer = buildReducer({ 5 | 'FOO_ACTION': (s, a) => s 6 | }) 7 | expect(typeof (reducer)).toEqual('function') 8 | }) 9 | 10 | test('can receive a function', () => { 11 | const reducer = buildReducer(function(state, action){ 12 | return state 13 | }) 14 | expect(typeof (reducer)).toEqual('function') 15 | }) -------------------------------------------------------------------------------- /lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | const subscribers = new Map() 2 | 3 | const dispatcher = { 4 | register(subscriber) { 5 | let counter = 1 6 | while(subscribers.has(`s_${counter}`)){ 7 | counter++; 8 | } 9 | const sid = `s_${counter}` 10 | subscribers.set(sid, subscriber) 11 | return sid 12 | }, 13 | unregister(sid) { 14 | subscribers.delete(sid) 15 | }, 16 | dispatch(action) { 17 | subscribers.forEach(s => s(action)) 18 | } 19 | } 20 | 21 | export default dispatcher -------------------------------------------------------------------------------- /__tests__/__snapshots__/withState.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`withState should accept a pre-middleware function 1`] = ` 4 |
5 | Hello, 6 | ! 7 |
8 | `; 9 | 10 | exports[`withState should wrap 1`] = ` 11 |
12 | Hello 13 |
14 | `; 15 | 16 | exports[`withState supports partial application 1`] = ` 17 |
18 | Hello, 19 | Dan 20 | ! 21 |
22 | `; 23 | 24 | exports[`withState supports partial application 2`] = ` 25 |
26 | Ciao, 27 | Dan 28 | ! 29 |
30 | `; 31 | -------------------------------------------------------------------------------- /lib/buildReducer.js: -------------------------------------------------------------------------------- 1 | const createReducer = (reducer, projectorFn = s => s) => { 2 | reducer.project = projectorFn 3 | return reducer 4 | } 5 | 6 | const buildReducer = function(mapOrFn, projectorFn = s => s){ 7 | if (typeof(mapOrFn) === 'function'){ 8 | return createReducer(mapOrFn, projectorFn) 9 | } 10 | const reducer = function(state, action) { 11 | if (action.type in mapOrFn){ 12 | return mapOrFn[action.type](state, action) 13 | } 14 | return state 15 | } 16 | reducer.project = projectorFn 17 | return reducer 18 | } 19 | 20 | export default buildReducer -------------------------------------------------------------------------------- /__tests__/dispatcher.test.js: -------------------------------------------------------------------------------- 1 | import dispatcher from '../lib/dispatcher' 2 | 3 | test('dispatch the action', done => { 4 | dispatcher.register(action => { 5 | expect(action.type).toBe('FOO') 6 | expect(action.content.bar).toBe(42) 7 | done() 8 | }) 9 | dispatcher.dispatch({type: 'FOO', content: {bar: 42}}) 10 | }) 11 | 12 | test('unregister', done => { 13 | const id = dispatcher.register(action => { 14 | throw new Error('You should not be here') 15 | }) 16 | dispatcher.unregister(id) 17 | dispatcher.dispatch({type: 'FOO', content: {bar: 42}}) 18 | setTimeout(function(){ done() }, 10) 19 | }) -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, 'App.jsx'), 5 | output: { 6 | path: path.resolve(__dirname, '.'), 7 | filename: './app.js', 8 | }, 9 | resolve: { 10 | modules: [ 11 | path.resolve('./'), 12 | path.resolve('./node_modules'), 13 | ], 14 | extensions: ['.js', '.jsx'], 15 | }, 16 | module: { 17 | rules: [ 18 | {test: /\.html$/, use: [{loader: 'file-loader?name=[name].[ext]'}]}, 19 | {test: /\.jsx?$/, use: [{loader: 'babel-loader'}]}, 20 | ] 21 | } 22 | }; -------------------------------------------------------------------------------- /dist/dispatcher.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var subscribers = new Map(); 7 | 8 | var dispatcher = { 9 | register: function register(subscriber) { 10 | var counter = 1; 11 | while (subscribers.has("s_" + counter)) { 12 | counter++; 13 | } 14 | var sid = "s_" + counter; 15 | subscribers.set(sid, subscriber); 16 | return sid; 17 | }, 18 | unregister: function unregister(sid) { 19 | subscribers.delete(sid); 20 | }, 21 | dispatch: function dispatch(action) { 22 | subscribers.forEach(function (s) { 23 | return s(action); 24 | }); 25 | } 26 | }; 27 | 28 | exports.default = dispatcher; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.buildReducer = exports.dispatcher = exports.withState = undefined; 7 | 8 | var _withState = require('./withState'); 9 | 10 | var _withState2 = _interopRequireDefault(_withState); 11 | 12 | var _dispatcher = require('./dispatcher'); 13 | 14 | var _dispatcher2 = _interopRequireDefault(_dispatcher); 15 | 16 | var _buildReducer = require('./buildReducer'); 17 | 18 | var _buildReducer2 = _interopRequireDefault(_buildReducer); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | exports.withState = _withState2.default; 23 | exports.dispatcher = _dispatcher2.default; 24 | exports.buildReducer = _buildReducer2.default; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | data/ 31 | env 32 | .sass-cache 33 | .temp 34 | .idea/**/* 35 | .DS_Store 36 | .vscode 37 | jsconfig.json -------------------------------------------------------------------------------- /demo/App.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import React from 'react' 3 | import {Component} from 'react' 4 | import {buildReducer, withState, dispatcher} from '../lib' 5 | 6 | class MyCounter extends React.Component { 7 | constructor() { 8 | super() 9 | this.plusOne = this.plusOne.bind(this) 10 | } 11 | plusOne() { 12 | dispatcher.dispatch({type: 'PLUS_ONE', content: this.props.counter}) 13 | } 14 | render() { 15 | return ( 16 |
17 |
Click count: {this.props.counter}
18 | 19 |
) 20 | } 21 | } 22 | 23 | const reducers = buildReducer({ 24 | 'PLUS_ONE': (state, action) => ({counter: state.counter + 1}) 25 | }) 26 | 27 | const INITIAL_STATE = { counter: 0 } 28 | 29 | const App = withState([reducers], INITIAL_STATE)(MyCounter) 30 | 31 | 32 | 33 | ReactDOM.render(, document.getElementById('app')) -------------------------------------------------------------------------------- /dist/buildReducer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var createReducer = function createReducer(reducer) { 7 | var projectorFn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (s) { 8 | return s; 9 | }; 10 | 11 | reducer.project = projectorFn; 12 | return reducer; 13 | }; 14 | 15 | var buildReducer = function buildReducer(mapOrFn) { 16 | var projectorFn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (s) { 17 | return s; 18 | }; 19 | 20 | if (typeof mapOrFn === 'function') { 21 | return createReducer(mapOrFn, projectorFn); 22 | } 23 | var reducer = function reducer(state, action) { 24 | if (action.type in mapOrFn) { 25 | return mapOrFn[action.type](state, action); 26 | } 27 | return state; 28 | }; 29 | reducer.project = projectorFn; 30 | return reducer; 31 | }; 32 | 33 | exports.default = buildReducer; -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | dependencies: 7 | pre: 8 | - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 9 | jobs: 10 | build: 11 | docker: 12 | - image: circleci/node:8.10 13 | 14 | working_directory: ~/repo 15 | 16 | steps: 17 | - checkout 18 | 19 | # Download and cache dependencies 20 | - restore_cache: 21 | keys: 22 | - v1-dependencies-{{ checksum "package.json" }} 23 | # fallback to using the latest cache if no exact match is found 24 | - v1-dependencies- 25 | 26 | - run: npm install 27 | 28 | - save_cache: 29 | paths: 30 | - node_modules 31 | key: v1-dependencies-{{ checksum "package.json" }} 32 | 33 | - run: npm test 34 | 35 | - run: npm run build 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t-redux", 3 | "version": "1.1.3", 4 | "description": "A mini redux pattern implementation", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "babel ./lib --out-dir ./dist --ignore __tests__", 8 | "build-demo": "webpack --config ./demo/webpack.config.js", 9 | "test": "jest" 10 | }, 11 | "jest": { 12 | "verbose": true, 13 | "testPathIgnorePatterns": [ 14 | "/node_modules/", 15 | "/__tests__/fake/" 16 | ] 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/emadb/t-redux.git" 21 | }, 22 | "keywords": [ 23 | "redux", 24 | "react", 25 | "flux", 26 | "reactjs" 27 | ], 28 | "author": "Emanuele DelBono ", 29 | "license": "MIT", 30 | "dependencies": { 31 | "babel": "^6.23.0", 32 | "babel-cli": "^6.26.0", 33 | "react": "15.6.1" 34 | }, 35 | "devDependencies": { 36 | "babel-jest": "20.0.3", 37 | "babel-loader": "7.1.1", 38 | "babel-preset-es2015": "6.24.1", 39 | "babel-preset-react": "6.24.1", 40 | "jest": "20.0.4", 41 | "react-dom": "15.6.1", 42 | "react-test-renderer": "15.6.1", 43 | "webpack": "3.4.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/withState.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import dispatcher from './dispatcher' 3 | 4 | function combineReducers(reducers, state, action){ 5 | const newState = reducers.reduce((acc, r) => { 6 | const projection = r.project(acc) 7 | return Object.assign({}, acc, r(projection, action)) 8 | }, state) 9 | return newState 10 | } 11 | 12 | function withState(reducers = [], initialState = {}, middleware = () =>{}) { 13 | return function(WrappedComponent) { 14 | return class WithState extends React.Component { 15 | constructor(props) { 16 | super(props) 17 | this.state = { innerState: initialState } 18 | } 19 | 20 | componentWillMount() { 21 | this.regId = dispatcher.register(action => { 22 | middleware(this.state.innerState, action) 23 | const nextState = combineReducers(reducers, this.state.innerState, action) 24 | this.setState({innerState: nextState}) 25 | }) 26 | } 27 | componentWillUnmount() { 28 | dispatcher.unregister(this.regId) 29 | } 30 | render() { 31 | return 32 | } 33 | } 34 | } 35 | } 36 | 37 | export default withState 38 | -------------------------------------------------------------------------------- /__tests__/withState.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import TestComponent from './fake/TestComponent' 4 | import withState from '../lib/withState' 5 | import dispatcher from '../lib/dispatcher' 6 | 7 | test('withState should wrap', () => { 8 | const component = renderer.create() 9 | let tree = component.toJSON() 10 | expect(tree).toMatchSnapshot() 11 | }) 12 | 13 | test('withState supports partial application ', () => { 14 | const A = ({ name }) =>
Hello, {name}!
15 | const B = ({ name }) =>
Ciao, {name}!
16 | 17 | const withName = withState([], {name: 'Dan'}) 18 | 19 | const AWithName = withName(A) 20 | const BWithName = withName(B) 21 | 22 | let tree = renderer.create().toJSON() 23 | expect(tree).toMatchSnapshot() 24 | 25 | tree = renderer.create().toJSON() 26 | expect(tree).toMatchSnapshot() 27 | }) 28 | 29 | test('withState should accept a pre-middleware function', () => { 30 | const A = ({ name }) =>
Hello, {name}!
31 | let done = false 32 | const middleware = (state, action) => { done = true } 33 | 34 | const Wrapped = withState([], {}, middleware)(A) 35 | const component = renderer.create() 36 | let tree = component.toJSON() 37 | 38 | dispatcher.dispatch({type: 'FAKE_ACTION', content: {}}) 39 | 40 | expect(tree).toMatchSnapshot() 41 | expect(done).toBeTruthy() 42 | }) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## T-Redux 2 | 3 | This is a mini library that implements the Redux pattern. 4 | Useful if you don't need all the stuff that the redux framework gives you. 5 | 6 | [![CircleCI](https://circleci.com/gh/emadb/t-redux/tree/master.svg?style=svg)](https://circleci.com/gh/emadb/t-redux/tree/master) 7 | 8 | ### How to use it 9 | 1) Install the library 10 | ``` 11 | npm install t-redux 12 | ``` 13 | 14 | 2) Import in your files 15 | ``` 16 | import {withState, dispatcher, buildReducer} from 't-redux' 17 | ``` 18 | 19 | 3) What you get is 20 | - A High Order Component to wrap your React components 21 | - An action dispatcher 22 | - An utility function to build the reducers 23 | 24 | ### One file example 25 | 26 | ```javascript 27 | // import the needed modules 28 | import {withState, dispatcher, buildReducer} from 't-redux' 29 | 30 | // this is a PORC (Plain Old React Component) 31 | class MyCounter extends React.Component { 32 | constructor() { 33 | super() 34 | this.plusOne = this.plusOne.bind(this) 35 | } 36 | plusOne() { 37 | // Dispacth the action (the content is optional) 38 | dispatcher.dispatch({type: 'PLUS_ONE', content: this.props.counter}) 39 | } 40 | render() { 41 | return ( 42 |
43 |
Click count: {this.props.counter}
44 | 45 |
) 46 | } 47 | } 48 | // Build the reducers as a map ACTION:(state, action) => state 49 | const reducers = buildReducer({ 50 | 'PLUS_ONE': (state, action) => ({counter: state.counter + 1}) 51 | }) 52 | 53 | // Define the initial state 54 | const INITIAL_STATE = { counter: 0 } 55 | 56 | const middlewareFn = (state, action) => { 57 | // inspect the state or the action. Useful for logging and debugging or to store the events. 58 | } 59 | 60 | // export the wrapped component passing the reducers and the initial state 61 | export default withState([reducers], INITIAL_STATE, middlewareFn)(MyCounter) 62 | ``` 63 | 64 | ### API 65 | 66 | #### `buildReducer(map)` 67 | Build the reducers map. The map is composed by an action type and a reducer function. The reducer function is f: (state, action) -> state 68 | The state is changed applying the action type reducer. 69 | 70 | #### `dispatcher` 71 | Is an object with three methods: 72 | ##### `register(fn)` 73 | Register the function fn in the list of subscribed functions. Returns the id of the registered entry. 74 | ##### `unregister(id)` 75 | Remove the function from the list of subscribers. 76 | ##### `dispatch(action)` 77 | Call every subscribed functions passing the action 78 | 79 | #### `withState(reducers, initialState, middleware)` 80 | Connect the magic. Returns a function that it can be used to connect a component with the reducers and the initial state. After the initial state it accept a function that will be called before applying the action. 81 | 82 | 83 | ### Why 84 | This package was born while I was learning react and redux. I love the principles behind Redux but I don't like boilerplate and overcomplicated code so I decided to try to write a simple implementation of the redux pattern. 85 | Feel free to use it or to continue to use the [Real One](https://github.com/reactjs/redux). 86 | 87 | [Michele Bertoli](https://github.com/MicheleBertoli) gave a [terrific presentation](https://speakerdeck.com/michelebertoli/setstate-ftw) on how to manage state in react applications. `t-redux` is one of the available options. 88 | -------------------------------------------------------------------------------- /dist/withState.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _dispatcher = require('./dispatcher'); 16 | 17 | var _dispatcher2 = _interopRequireDefault(_dispatcher); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 22 | 23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 24 | 25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 26 | 27 | function combineReducers(reducers, state, action) { 28 | var newState = reducers.reduce(function (acc, r) { 29 | var projection = r.project(acc); 30 | return Object.assign({}, acc, r(projection, action)); 31 | }, state); 32 | return newState; 33 | } 34 | 35 | function withState() { 36 | var reducers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 37 | var initialState = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 38 | var middleware = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {}; 39 | 40 | return function (WrappedComponent) { 41 | return function (_React$Component) { 42 | _inherits(WithState, _React$Component); 43 | 44 | function WithState(props) { 45 | _classCallCheck(this, WithState); 46 | 47 | var _this = _possibleConstructorReturn(this, (WithState.__proto__ || Object.getPrototypeOf(WithState)).call(this, props)); 48 | 49 | _this.state = { innerState: initialState }; 50 | return _this; 51 | } 52 | 53 | _createClass(WithState, [{ 54 | key: 'componentWillMount', 55 | value: function componentWillMount() { 56 | var _this2 = this; 57 | 58 | this.regId = _dispatcher2.default.register(function (action) { 59 | middleware(_this2.state.innerState, action); 60 | var nextState = combineReducers(reducers, _this2.state.innerState, action); 61 | _this2.setState({ innerState: nextState }); 62 | }); 63 | } 64 | }, { 65 | key: 'componentWillUnmount', 66 | value: function componentWillUnmount() { 67 | _dispatcher2.default.unregister(this.regId); 68 | } 69 | }, { 70 | key: 'render', 71 | value: function render() { 72 | return _react2.default.createElement(WrappedComponent, _extends({}, this.state.innerState, this.props)); 73 | } 74 | }]); 75 | 76 | return WithState; 77 | }(_react2.default.Component); 78 | }; 79 | } 80 | 81 | exports.default = withState; --------------------------------------------------------------------------------