├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── examples └── chat-app │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc │ ├── .flowconfig │ ├── .gitignore │ ├── README.md │ ├── app │ ├── actions │ │ └── chat.js │ ├── components │ │ ├── MessageListItem.js │ │ └── Root.js │ ├── containers │ │ ├── MessageInput.js │ │ └── MessageList.js │ ├── horizon │ │ ├── connection.js │ │ └── redux.js │ ├── index.js │ └── reducers │ │ ├── chat.js │ │ └── index.js │ ├── package.json │ ├── static-server.js │ └── webpack.config.js ├── package.json ├── src ├── actionHandler.js ├── actionTaker.js ├── createMiddleware.js ├── index.js └── index.js.flow ├── types ├── horizon-redux.js.flow ├── horizon.js.flow └── redux.js.flow └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["transform-flow-strip-types"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | es 4 | lib 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "extends" : [ 4 | "standard" 5 | ], 6 | "plugins": [ 7 | "flowtype" 8 | ], 9 | "env" : { 10 | "browser" : true, 11 | "node": true 12 | }, 13 | "globals" : { 14 | "expect": true 15 | }, 16 | "rules": { 17 | "semi" : [2, "never"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | ./src 5 | 6 | [libs] 7 | 8 | [options] 9 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 10 | suppress_comment= \\(.\\|\n\\)*\\$FlowIssue 11 | suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | dist 5 | lib 6 | es 7 | coverage 8 | examples/*/node_modules 9 | examples/*/rethinkdb_data 10 | examples/*/.hz 11 | .nyc_output 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: 5 | - npm run clean 6 | - npm run build 7 | branches: 8 | only: 9 | - master 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Shane Cavaliere 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # horizon-redux 2 | A small library that helps you connect Horizon.io with Redux in a flexible, non-intrusive way. 3 | 4 | [![Build Status](https://travis-ci.org/shanecav/horizon-redux.svg?branch=master)](https://travis-ci.org/shanecav/horizon-redux) 5 | 6 | ## What does it do? 7 | horizon-redux helps you connect [Redux](https://github.com/reactjs/redux) with [Horizon.io](http://horizon.io/). It works by letting you create simple "actionTakers" that respond to matching actions with a Horizon query, and in turn respond to the Horizon query subscription results (usually by dispatching another action). 8 | 9 | All of your interactions with Horizon.io, whether you're initiating or responding to queries, will happen through Redux actions. This approach allows you to use Redux to manage your app's entire state, as opposed to having external Horizon.io bindings tied directly to your UI components. This way, you can enjoy the simplicity of Horizon.io without losing the benefits of a well-structured Redux app. 10 | 11 | horizon-redux has zero npm dependencies, and its only requirements are Horizon.io and Redux. 12 | 13 | horizon-redux is compatible with Horizon.io 1.x and 2.x. 14 | 15 | _Interested in a different approach? See the [Alternative Approaches](#alternative-approaches) section below for some different options for integrating Horizon with Redux._ 16 | 17 | ## Installation 18 | 19 | `npm i -S horizon-redux` 20 | 21 | Alternatively: 22 | 23 | `` (exposes window.HorizonRedux as a global variable) 24 | 25 | ## Usage 26 | 27 | ```js 28 | import HorizonRedux from 'horizon-redux' 29 | // initialize horizonRedux with a Horizon client instance 30 | const horizonRedux = HorizonRedux(horizon) 31 | // create horizon-redux middleware 32 | const hzMiddleware = horizonRedux.createMiddleware() 33 | // Create the Redux store with horizon-redux middleware 34 | const store = createStore(rootReducer, [], applyMiddleware(hzMiddleware)) 35 | 36 | // Add an actionTaker that watches for a WATCH_MESSAGES action. When that action 37 | // is dispatched, Horizon grabs the most recent 10 messages from the messages 38 | // table. Because we added watch(), this actionTaker's successHandler will get 39 | // called every time new messages are added. 40 | horizonRedux.takeLatest( 41 | 'WATCH_MESSAGES', 42 | (horizon, action, getState) => horizon('messages').order('datetime', 'descending').limit(action.payload).watch(), 43 | (result, action, dispatch) => dispatch({type: 'NEW_MESSAGES', payload: result}), 44 | (err, action, dispatch) => console.log('failed to load messages:', err) 45 | ) 46 | // Notice how we added an actionTaker after the middleware has already been 47 | // added to the store - horizon-redux internally stores an array of actionTakers, 48 | // which the middleware returned by `createMiddleware()` accesses. This allows 49 | // you to add/remove actionTakers at any time. 50 | 51 | // Now we can dispatch the action that tells Horizon to watch for chat messages. 52 | store.dispatch({ type: 'WATCH_MESSAGES', payload: 10 }) 53 | 54 | // addActionTaker returns an actionTakerManager with a remove() method. 55 | // Removing an actionTaker automatically unsubscribes from all Horizon subscriptions 56 | // associated with it, and removes it from horizonRedux. (takeEvery and 57 | // takeLatest also return a manager.) 58 | const someActionTaker = horizonRedux.addActionTaker(/* ... */) 59 | someActionTaker.remove() 60 | ``` 61 | 62 | #### Check out the [chat-app](examples/chat-app) example in this repo for a basic working example based on the chat-app example from Horizon.io 63 | 64 | # API 65 | 66 | ```js 67 | import HorizonRedux from 'horizon-redux' 68 | const horizonRedux = HorizonRedux(horizonInstance) 69 | ``` 70 | 71 | ## horizonRedux methods: 72 | 73 | ### .createMiddleware() 74 | 75 | Creates a Redux middleware that watches for actions that match any of the actionTakers created by horizonRedux. See `horizonRedux.addActionTaker` below for more details. 76 | 77 | #### Arguments: 78 | 79 | n/a 80 | 81 | #### Returns: 82 | 83 | Redux middleware 84 | 85 | --- 86 | 87 | ### .addActionTaker(pattern, observableQuery, successHandler, errorHandler, type) 88 | 89 | Adds an actionTaker to horizonRedux's internal array. Every action that goes through horizonRedux's middleware will be matched against every added actionTaker. The actionTaker determines how to respond to matching actions with Horizon queries. 90 | 91 | *Rather than calling this method directly, you can call `takeLatest(...)` or `takeEvery(...)`, which simply call `addActionTaker(...)` with the corresponding `type` argument injected automatically (see below).* 92 | 93 | #### Arguments: 94 | 95 | 1. `pattern` - A string, array of strings, or function used to match against dispatched action's types. 96 | * If it's a string, matches if `pattern === action.type` 97 | * If it's an array of strings, matches if any elements of the array are strictly equal to `action.type` 98 | * If it's a function, matches if pattern(action) returns a truthy value 99 | 2. `observableQuery` - A function that takes a Horizon client instance, an action, and your Redux store's `getState` method, and returns a Horizon query. The query must be an "observable" type (`fetch()`, `watch()`, `store()`, `upsert()`, `insert()`, `replace()`, `update()`, `remove()`, or `removeAll()`). Do not call the `subscribe()` method on the query here - HorizonRedux takes care of that automatically. 100 | 3. `successHandler` (optional) - A function that takes result (the result of the query), action (the action associated with that query) and the Redux store's dispatch method. You can handle the successful query however you'd like - usually by dispatching another action with the results. 101 | 4. `errorHandler` (optional) - A function that takes the error, action (the action associated with that query) and the Redux store's dispatch method. You can handle an error scenario however you'd like. 102 | 5. `type` (optional) - A string representing the type of actionTaker to add. Must be either `'takeEvery'` or `'takeLatest'` (defaults to `'takeEvery'` if omitted). This argument determines how the actionTaker manages its subscriptions when new matching actions are dispatched: 103 | * If `'takeEvery'`, the actionTaker will add an additional subscription every time a matching action is dispatched. 104 | * If `'takeLatest'`, the actionTaker will replace the existing subscription (first calling its `unsubscribe()` method) with a new subscription every time a matching action is dispatched. Keep in mind that your success/error handlers will no longer fire after the old subscription has been unsubscribed. 105 | 106 | #### Returns: 107 | 108 | An actionTaker "manager" with a single method: `remove()`. Calling the `remove()` method automatically unsubscribes from all Horizon subscriptions associated with the actionTaker, and removes it from horizonRedux so that it no longer responds to its matching actions. 109 | 110 | #### Example: 111 | 112 | ```js 113 | // Adds an actionTaker that matches 'WATCH_MESSAGES' actions and responds by 114 | // telling Horizon to watch for new messages in the 'messages' table. The max 115 | // number of returned documents (limit) is set by the action's payload.limit. 116 | // Because we set the type to 'takeLatest', it will replace the old Horizon query 117 | // subscription (if it exists) with a new subscription every time a matching 118 | // action is dispatched. 119 | horizonRedux.addActionTaker( 120 | 'WATCH_MESSAGES', 121 | (horizon, action, getState) => 122 | horizon('messages').order('datetime', 'descending').limit(action.payload.limit || 10).watch(), 123 | (result, action, dispatch) => { 124 | dispatch(newMessages(result)) 125 | }, 126 | (err, action, dispatch) => { 127 | console.log('failed to load messages:', err) 128 | }, 129 | 'takeLatest' 130 | ) 131 | 132 | // Start watching messages and return 10 at a time 133 | store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 10 } }) 134 | 135 | // ...now return 20 at a time instead 136 | store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 20 } }) 137 | ``` 138 | 139 | --- 140 | 141 | ### .takeLatest(pattern, observableQuery, successHandler, errorHandler) 142 | 143 | Identical to `addActionTaker(...)` except that the type is automatically set to `'takeLatest'` (see above). Matching actions will replace the subscription from the previous matching action (first calling its `unsubscribe()` method) with the new subscription. 144 | 145 | #### Example: 146 | 147 | ```js 148 | // This is equivalent to the 'addActionTaker' example above. 149 | horizonRedux.takeLatest( 150 | 'WATCH_MESSAGES', 151 | (horizon, action, getState) => 152 | horizon('messages').order('datetime', 'descending').limit(action.payload.limit || 10).watch(), 153 | (result, action, dispatch) => { 154 | dispatch(newMessages(result)) 155 | }, 156 | (err, action, dispatch) => { 157 | console.log('failed to load messages:', err) 158 | } 159 | ) 160 | 161 | // Start watching messages and return 10 at a time 162 | store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 10 } }) 163 | 164 | // ...now return 20 at a time instead 165 | store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 20 } }) 166 | ``` 167 | 168 | --- 169 | 170 | ### .takeEvery(pattern, observableQuery, successHandler, errorHandler) 171 | 172 | Identical to `addActionTaker(...)` except that the type is automatically set to `'takeEvery'` (see above). Matching actions will add new subscriptions (without replacing previous ones). 173 | 174 | #### Example: 175 | 176 | ```js 177 | // Adds an actionTaker that matches 'ADD_MESSAGE_REQUEST' actions and responds 178 | // by telling Horizon to insert the message in the action's payload into the 179 | // 'messages' table. Because we're using 'takeEvery', the subscriptions created 180 | // by previously dispatched 'ADD_MESSAGE_REQUEST' actions will not be overwritten, 181 | // so their success/error handlers will still fire even if new messages have 182 | // since been added. 183 | horizonRedux.takeEvery( 184 | 'ADD_MESSAGE_REQUEST', 185 | (horizon, action, getState) => horizon('messages').store(action.payload), 186 | (id, action, dispatch) => dispatch(addMessageSuccess(id, action.payload)), 187 | (err, action, dispatch) => dispatch(addMessageFailure(err, action.payload)) 188 | ) 189 | ``` 190 | 191 | ## Questions/Comments/Issues? 192 | 193 | I'm very open to feedback, and will respond to issues quickly. Feel free to [get in touch](https://twitter.com/shanecav)! 194 | 195 | ## Alternative Approaches 196 | 197 | 1. [redux-observable](https://redux-observable.js.org/) is honestly a more elegant approach. If you aren't interested in learning RxJS, then horizon-redux will work fine, but redux-observable is a great library made by smart people (and it's worth learning RxJS if you're using Horizon). Because most Horizon.io collection methods return RxJS Observables, using redux-observable should be pretty easy to integrate. 198 | 199 | 2. [redux-saga](https://github.com/yelouafi/redux-saga) is a great option if you find that you need more power than horizon-redux offers. redux-saga is a much bigger library with a larger API. With this approach, you'll likely end up writing more code than you would with horizon-redux, but it may be necessary for more complex apps. [Check out an example app using Horizon.io with redux-saga](https://github.com/shanecav/horizon-redux-saga). 200 | 201 | ## License 202 | 203 | [MIT](LICENSE.md) 204 | -------------------------------------------------------------------------------- /examples/chat-app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/chat-app/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | *.spec.js 4 | index.html 5 | -------------------------------------------------------------------------------- /examples/chat-app/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "extends" : [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "plugins": [ 8 | "flowtype" 9 | ], 10 | "env" : { 11 | "browser" : true 12 | }, 13 | "globals" : { 14 | "__DEV__" : false, 15 | "__PROD__" : false, 16 | "__DEBUG__" : false, 17 | "__DEBUG_NEW_WINDOW__" : false 18 | }, 19 | "rules": { 20 | "semi" : [2, "never"], 21 | "handle-callback-err": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/chat-app/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/dist/.* 3 | .*/node_modules/fbjs/lib/.* 4 | 5 | [include] 6 | ../../src/ 7 | ../../types/ 8 | 9 | [libs] 10 | 11 | [options] 12 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 13 | suppress_comment= \\(.\\|\n\\)*\\$FlowIssue 14 | suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore 15 | -------------------------------------------------------------------------------- /examples/chat-app/.gitignore: -------------------------------------------------------------------------------- 1 | rethinkdb_data 2 | **/*.log 3 | .hz/secrets.toml 4 | -------------------------------------------------------------------------------- /examples/chat-app/README.md: -------------------------------------------------------------------------------- 1 | # chat-app 2 | 3 | Based on the [React chat-app example from Horizon.io](https://github.com/rethinkdb/horizon/tree/next/examples/react-chat-app). 4 | 5 | ## Running the example 6 | 7 | You'll need Horizon installed first: `npm install -g horizon` 8 | 9 | 1. `git clone git@github.com:shanecav/horizon-redux.git` 10 | 2. `cd horizon-redux` 11 | 3. `npm install` 12 | 4. `cd examples/chat-app` 13 | 5. `npm install` 14 | 6. `hz init` 15 | 7. `hz serve --dev` 16 | 8. In a new terminal (at the same directory): `npm start` 17 | 18 | Now you can view the chat-app example at [http://localhost:3000/](http://localhost:3000/). Play around with it to see how horizon-redux works! Try opening it in another browser window to see the realtime changes take effect. 19 | -------------------------------------------------------------------------------- /examples/chat-app/app/actions/chat.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | 3 | // Tell horizon-redux to start watching for new messages in the 'messages' table 4 | // Limit determines the max number of messages to return 5 | export const WATCH_MESSAGES = 'WATCH_MESSAGES' 6 | export const watchMessages = createAction( 7 | WATCH_MESSAGES, 8 | (limit) => limit 9 | ) 10 | 11 | // A new complete set of messages has been fetched from Horizon 12 | export const NEW_MESSAGES = 'NEW_MESSAGES' 13 | export const newMessages = createAction( 14 | NEW_MESSAGES, 15 | (messages) => messages 16 | ) 17 | 18 | // Add a new message and handle results 19 | export const ADD_MESSAGE_REQUEST = 'ADD_MESSAGE_REQUEST' 20 | export const ADD_MESSAGE_SUCCESS = 'ADD_MESSAGE_SUCCESS' 21 | export const ADD_MESSAGE_FAILURE = 'ADD_MESSAGE_FAILURE' 22 | export const addMessageRequest = createAction(ADD_MESSAGE_REQUEST) 23 | export const addMessageSuccess = createAction( 24 | ADD_MESSAGE_SUCCESS, 25 | (id, message) => ({ 26 | id, 27 | message 28 | }) 29 | ) 30 | export const addMessageFailure = createAction( 31 | ADD_MESSAGE_FAILURE, 32 | (err, message) => ({ 33 | err, 34 | message 35 | }) 36 | ) 37 | -------------------------------------------------------------------------------- /examples/chat-app/app/components/MessageListItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MessageListItem 4 | * 5 | */ 6 | 'use strict' 7 | 8 | import React, { PropTypes } from 'react' 9 | 10 | const MessageListItem = ({ message }) => { 11 | return ( 12 |
  • {message.text}
  • 13 | ) 14 | } 15 | 16 | MessageListItem.propTypes = { 17 | message: PropTypes.shape({ 18 | text: PropTypes.string.isRequired, 19 | datetime: PropTypes.instanceOf(Date) 20 | }).isRequired 21 | } 22 | 23 | export default MessageListItem 24 | -------------------------------------------------------------------------------- /examples/chat-app/app/components/Root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Root 4 | * 5 | */ 6 | 'use strict' 7 | 8 | import React from 'react' 9 | 10 | import MessageList from '../containers/MessageList' 11 | import MessageInput from '../containers/MessageInput' 12 | 13 | const Root = () => { 14 | return ( 15 |
    16 | 17 | 18 |
    19 | ) 20 | } 21 | 22 | export default Root 23 | -------------------------------------------------------------------------------- /examples/chat-app/app/containers/MessageInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MessageInput 4 | * 5 | */ 6 | 'use strict' 7 | 8 | import React, { PropTypes, Component } from 'react' 9 | import { connect } from 'react-redux' 10 | 11 | import { addMessageRequest } from '../actions/chat' 12 | 13 | class MessageInput extends Component { 14 | static propTypes = { 15 | dispatch: PropTypes.func.isRequired 16 | }; 17 | 18 | render () { 19 | return ( 20 |
    21 | 22 |
    23 | ) 24 | } 25 | 26 | _handleKeyUp = (event) => { 27 | if (event.keyCode === 13) { 28 | this.props.dispatch(addMessageRequest({ 29 | text: event.target.value, 30 | datetime: new Date() 31 | })) 32 | event.target.value = '' 33 | } 34 | } 35 | } 36 | 37 | export default connect()(MessageInput) 38 | -------------------------------------------------------------------------------- /examples/chat-app/app/containers/MessageList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MessageList 4 | * 5 | */ 6 | 'use strict' 7 | 8 | import React, { PropTypes } from 'react' 9 | import { connect } from 'react-redux' 10 | 11 | import MessageListItem from '../components/MessageListItem' 12 | 13 | const MessageList = ({ messages }) => { 14 | return ( 15 |
    16 | 21 |
    22 | ) 23 | } 24 | 25 | MessageList.propTypes = { 26 | messages: PropTypes.arrayOf( 27 | PropTypes.shape({ 28 | text: PropTypes.string, 29 | datetime: PropTypes.instanceOf(Date) 30 | }) 31 | ).isRequired 32 | } 33 | 34 | const mapStateToProps = (state) => ({ 35 | messages: state.messages 36 | }) 37 | 38 | export default connect(mapStateToProps)(MessageList) 39 | -------------------------------------------------------------------------------- /examples/chat-app/app/horizon/connection.js: -------------------------------------------------------------------------------- 1 | import Horizon from '@horizon/client' 2 | 3 | const horizon = Horizon({ host: 'localhost:8181' }) 4 | horizon.connect() 5 | 6 | export default horizon 7 | -------------------------------------------------------------------------------- /examples/chat-app/app/horizon/redux.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import horizon from './connection' 4 | import HorizonRedux from '../../../../src' 5 | 6 | const horizonRedux = HorizonRedux(horizon) 7 | 8 | export default horizonRedux 9 | -------------------------------------------------------------------------------- /examples/chat-app/app/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | 'use strict' 4 | 5 | import React from 'react' 6 | import ReactDOM from 'react-dom' 7 | import { AppContainer } from 'react-hot-loader' 8 | import { Provider } from 'react-redux' 9 | import { compose, applyMiddleware, createStore } from 'redux' 10 | import createLogger from 'redux-logger' 11 | import horizonRedux from './horizon/redux' 12 | 13 | import { 14 | ADD_MESSAGE_REQUEST, 15 | WATCH_MESSAGES, 16 | watchMessages, 17 | newMessages, 18 | addMessageSuccess, 19 | addMessageFailure } from './actions/chat' 20 | import rootReducer from './reducers' 21 | import Root from './components/Root' 22 | 23 | const hzMiddleware = horizonRedux.createMiddleware() 24 | 25 | // Create the Redux store 26 | const store = createStore( 27 | rootReducer, 28 | window.initialState, 29 | compose( 30 | applyMiddleware(hzMiddleware, createLogger()), 31 | window.devToolsExtension ? window.devToolsExtension() : f => f 32 | ) 33 | ) 34 | 35 | // You can add/remove actionTakers any time, even after creating the middleware. 36 | // I've added them here for simplicity, but in a real app they could live 37 | // wherever makes the most sense with the app's structure. 38 | 39 | // Watch for all ADD_MESSAGE_REQUEST actions and store their payload in the 40 | // messages table. If successful, dispatch ADD_MESSAGE_SUCCESS. If there's an 41 | // error, dispatch ADD_MESSAGE_FAILURE and log a message to the console. 42 | horizonRedux.takeEvery( 43 | ADD_MESSAGE_REQUEST, 44 | (horizon, action, getState) => 45 | horizon('messages').store(action.payload), 46 | (id, action, dispatch) => { 47 | // the success handler for write queries doesn't occur until the write is 48 | // confirmed by the database, so you may see the NEW_MESSAGES action 49 | // get dispatched before the ADD_MESSAGE_SUCCESS action. 50 | dispatch(addMessageSuccess(id, action.payload)) 51 | }, 52 | (err, action, dispatch) => { 53 | console.log('failed to add message:', action.payload) 54 | dispatch(addMessageFailure(err, action.payload)) 55 | } 56 | ) 57 | 58 | // Watch for WATCH_MESSAGES action and grab the most recent messages from the 59 | // messages table. The max number of messages to retrieve is set by the matching 60 | // dispatched action (defaults to 10). Because we added watch(), this 61 | // actionTaker's successHandler will get called every time new messages are added. 62 | horizonRedux.takeLatest( 63 | WATCH_MESSAGES, 64 | (horizon, action, getState) => 65 | horizon('messages').order('datetime', 'descending').limit(action.payload || 10).watch(), 66 | (result, action, dispatch) => { 67 | console.log('dispatch:', dispatch) 68 | dispatch(newMessages(result)) 69 | }, 70 | (err, action, dispatch) => { 71 | console.log('failed to load messages') 72 | } 73 | ) 74 | 75 | // Now we can dispatch the initial action that tells horizon to watch for chat 76 | // messages. 77 | store.dispatch(watchMessages(10)) 78 | 79 | const appNode = document.createElement('div') 80 | document.body.appendChild(appNode) 81 | 82 | const renderRoot = (RootComponent) => { 83 | ReactDOM.render( 84 | 85 | 86 | 87 | 88 | , 89 | appNode 90 | ) 91 | } 92 | renderRoot(Root) 93 | 94 | if (module.hot) { 95 | module.hot.accept('./components/Root', () => { 96 | const Root = require('./components/Root').default 97 | renderRoot(Root) 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /examples/chat-app/app/reducers/chat.js: -------------------------------------------------------------------------------- 1 | import { NEW_MESSAGES } from '../actions/chat' 2 | 3 | const chat = (state = [], action) => { 4 | switch (action.type) { 5 | case NEW_MESSAGES: 6 | return [...action.payload].reverse() 7 | default: 8 | return state 9 | } 10 | } 11 | 12 | export default chat 13 | -------------------------------------------------------------------------------- /examples/chat-app/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import chatReducer from './chat' 4 | 5 | const rootReducer = combineReducers({ 6 | messages: chatReducer 7 | }) 8 | 9 | export default rootReducer 10 | -------------------------------------------------------------------------------- /examples/chat-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app/index.js", 6 | "scripts": { 7 | "start": "node static-server.js", 8 | "build": "cross-env NODE_ENV=production webpack -p", 9 | "generate": "plop --plopfile internals/generators/index.js" 10 | }, 11 | "author": "Shane Cavaliere ", 12 | "license": "UNLICENSED", 13 | "dependencies": { 14 | "@horizon/client": "^2.0.0", 15 | "classnames": "^2.2.5", 16 | "es6-promise": "^3.2.1", 17 | "react": "^15.2.1", 18 | "react-dom": "^15.2.1", 19 | "react-redux": "^4.4.5", 20 | "redux": "^3.5.2", 21 | "redux-actions": "^0.10.1", 22 | "redux-logger": "^2.6.1" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.11.4", 26 | "babel-core": "^6.11.4", 27 | "babel-eslint": "^6.1.2", 28 | "babel-loader": "^6.2.4", 29 | "babel-preset-es2015": "^6.9.0", 30 | "babel-preset-react": "^6.11.1", 31 | "babel-preset-stage-0": "^6.5.0", 32 | "cross-env": "^2.0.0", 33 | "css-loader": "^0.23.1", 34 | "eslint": "^3.1.1", 35 | "eslint-config-standard": "^5.3.5", 36 | "eslint-config-standard-jsx": "^2.0.0", 37 | "eslint-config-standard-react": "^3.0.0", 38 | "eslint-plugin-flowtype": "^2.6.4", 39 | "eslint-plugin-promise": "^2.0.0", 40 | "eslint-plugin-react": "^5.2.2", 41 | "eslint-plugin-standard": "^2.0.0", 42 | "extract-text-webpack-plugin": "^1.0.1", 43 | "flow-bin": "^0.30.0", 44 | "html-webpack-plugin": "^2.22.0", 45 | "plop": "^1.5.0", 46 | "qs": "^6.2.1", 47 | "react-hot-loader": "^3.0.0-beta.2", 48 | "strip-loader": "^0.1.2", 49 | "style-loader": "^0.13.1", 50 | "webpack": "^1.13.1", 51 | "webpack-dev-server": "^1.14.1", 52 | "webpack-merge": "^0.14.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/chat-app/static-server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var WebpackDevServer = require('webpack-dev-server') 3 | var config = require('./webpack.config') 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true, 9 | stats: { 10 | colors: true 11 | } 12 | }).listen(3000, 'localhost', function (err) { 13 | if (err) { 14 | console.log(err) 15 | } 16 | 17 | console.log('Listening at localhost:3000') 18 | }) 19 | -------------------------------------------------------------------------------- /examples/chat-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var qs = require('qs') 4 | var merge = require('webpack-merge') 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | 8 | var TARGET = process.env.npm_lifecycle_event 9 | 10 | var common = { 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js' 14 | }, 15 | module: { 16 | loaders: [{ 17 | test: /\.js$/, 18 | loaders: ['babel'], 19 | exclude: /node_modules/ 20 | }] 21 | }, 22 | plugins: [ 23 | new HtmlWebpackPlugin({ title: 'Chat App' }) 24 | ] 25 | } 26 | 27 | if (TARGET === 'start') { 28 | module.exports = merge(common, { 29 | devtool: 'source-map', 30 | entry: [ 31 | 'webpack-dev-server/client?http://localhost:3000', 32 | 'webpack/hot/only-dev-server', 33 | 'react-hot-loader/patch', 34 | './app/index' 35 | ], 36 | output: { 37 | publicPath: '/' 38 | }, 39 | module: { 40 | loaders: [ 41 | { 42 | test: /\.css$/, 43 | loaders: [ 44 | 'style', 45 | 'css?modules&' + qs.stringify({ 46 | importLoaders: 1, 47 | localIdentName: '[name]__[local]___[hash:base64:5]' 48 | }) 49 | ], 50 | exclude: /node_modules/ 51 | } 52 | ] 53 | }, 54 | plugins: [ 55 | new webpack.HotModuleReplacementPlugin() 56 | ] 57 | }) 58 | } 59 | 60 | if (TARGET === 'build') { 61 | module.exports = merge(common, { 62 | devtool: 'cheap-module-source-map', 63 | entry: [ 64 | './app/index' 65 | ], 66 | output: { 67 | publicPath: '/dist/' 68 | }, 69 | module: { 70 | loaders: [ 71 | { 72 | test: /\.js$/, 73 | loader: 'strip-loader?strip[]=console.log', 74 | exclude: /node_modules/ 75 | }, 76 | { 77 | test: /\.css$/, 78 | loader: ExtractTextPlugin.extract( 79 | 'style', 80 | 'css?modules&' + qs.stringify({ 81 | importLoaders: 1, 82 | localIdentName: '[name]__[local]___[hash:base64:5]' 83 | }) 84 | ), 85 | exclude: /node_modules/ 86 | } 87 | ] 88 | }, 89 | plugins: [ 90 | new webpack.DefinePlugin({ 91 | 'process.env': { 92 | 'NODE_ENV': JSON.stringify('production') 93 | } 94 | }), 95 | new webpack.optimize.UglifyJsPlugin({ 96 | compress: { 97 | warnings: true 98 | } 99 | }), 100 | new webpack.optimize.DedupePlugin(), 101 | new webpack.optimize.AggressiveMergingPlugin(), 102 | new ExtractTextPlugin('styles.css') 103 | ] 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "horizon-redux", 3 | "version": "2.3.0", 4 | "description": "A small library that helps you connect Horizon.io with Redux in a flexible, non-intrusive way.", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "dist", 8 | "lib", 9 | "src", 10 | "types" 11 | ], 12 | "scripts": { 13 | "clean": "rimraf lib dist es", 14 | "build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es", 15 | "prepublish": "npm run clean && npm run build", 16 | "lint": "eslint src", 17 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib --copy-files", 18 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es --copy-files", 19 | "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack", 20 | "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/shanecav/horizon-redux.git" 25 | }, 26 | "keywords": [ 27 | "redux", 28 | "action", 29 | "state", 30 | "horizon.io", 31 | "horizon", 32 | "@horizon/client" 33 | ], 34 | "author": "Shane Cavaliere (https://github.com/shanecav)", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/shanecav/horizon-redux/issues" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.11.4", 41 | "babel-core": "^6.11.4", 42 | "babel-eslint": "^6.1.2", 43 | "babel-loader": "^6.2.4", 44 | "babel-plugin-istanbul": "^1.0.3", 45 | "babel-plugin-transform-flow-strip-types": "^6.8.0", 46 | "babel-preset-es2015": "^6.9.0", 47 | "babel-preset-stage-0": "^6.5.0", 48 | "cross-env": "^2.0.0", 49 | "eslint": "^3.1.1", 50 | "eslint-config-standard": "^5.3.5", 51 | "eslint-plugin-flowtype": "^2.3.1", 52 | "eslint-plugin-promise": "^2.0.0", 53 | "eslint-plugin-standard": "^2.0.0", 54 | "flow-bin": "^0.30.0", 55 | "rimraf": "^2.5.4", 56 | "webpack": "^1.13.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/actionHandler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { HorizonInstance } from '../types/horizon' 4 | import type { Action, Dispatch, GetState } from '../types/redux' 5 | import type { 6 | ActionTaker, 7 | ActionTakers 8 | } from '../types/horizon-redux' 9 | 10 | export function actionTakerMatchesAction ( 11 | action: Action, 12 | actionTaker: ActionTaker 13 | ): bool { 14 | return ( 15 | typeof actionTaker.pattern === 'string' && 16 | actionTaker.pattern === action.type 17 | ) || ( 18 | Array.isArray(actionTaker.pattern) && 19 | actionTaker.pattern.indexOf(action.type) !== -1 20 | ) || ( 21 | typeof actionTaker.pattern === 'function' && 22 | actionTaker.pattern(action) 23 | ) 24 | } 25 | 26 | /** 27 | * Function that creates an actionHandler. The actionHandler is called for every 28 | * action that is dispatched through the horizon-redux middleware. It matches 29 | * that action against every actionTaker in the actionTakers array, and then 30 | * performs the appropriate operations for each matching actionTaker. 31 | */ 32 | export function createActionHandler ( 33 | hz: HorizonInstance, 34 | actionTakers: ActionTakers, 35 | dispatch: Dispatch, 36 | getState: GetState 37 | ) { 38 | return (action: Action): void => { 39 | actionTakers.forEach((actionTaker) => { 40 | if (actionTakerMatchesAction(action, actionTaker)) { 41 | const observable = actionTaker.observableQuery(hz, action, getState) 42 | 43 | // if this actionTaker's query has a success or error handler, set up 44 | // the new subscription 45 | if (actionTaker.successHandler || actionTaker.errorHandler) { 46 | // set up observableQuery subscriber 47 | const subscriber = observable.subscribe( 48 | actionTaker.successHandler 49 | // $FlowIssue: not detecting above conditional 50 | ? (result) => actionTaker.successHandler(result, action, dispatch) 51 | : undefined, 52 | actionTaker.errorHandler 53 | // $FlowIssue: not detecting above conditional 54 | ? (err) => actionTaker.errorHandler(err, action, dispatch) // eslint-disable-line handle-callback-err 55 | : undefined 56 | ) 57 | 58 | // add observable to actionTaker's subscribers so it can be 59 | // unsubscribed if the actionTaker is removed 60 | if (actionTaker.type === 'takeEvery') { 61 | actionTaker.subscribers.push(subscriber) 62 | } else if (actionTaker.type === 'takeLatest') { 63 | // Ensure that this actionTaker has only one active subscription, 64 | // which is based on the latest matching action dispatched 65 | actionTaker.subscribers.forEach((subscriber) => { 66 | subscriber.unsubscribe() 67 | }) 68 | actionTaker.subscribers = [subscriber] 69 | } 70 | } 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/actionTaker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | ActionTakerPattern, 5 | ActionTakerObservableQuery, 6 | ActionTakerSuccessHandler, 7 | ActionTakerErrorHandler, 8 | ActionTaker, 9 | ActionTakers, 10 | ActionTakerManager 11 | } from '../types/horizon-redux' 12 | 13 | /** 14 | * Function that creates an actionTaker. An actionTaker is a plain object that 15 | * sits in horizon-redux's actionTakers array (with every other actionTakler). 16 | * The actionHandler function (called by the middleware) uses this array of 17 | * actionTakers, which are matched against every dispatched action that passes 18 | * through the horizon-redux middleware. 19 | */ 20 | export function createActionTaker ( 21 | pattern: ActionTakerPattern, 22 | observableQuery: ActionTakerObservableQuery, 23 | successHandler?: ActionTakerSuccessHandler, 24 | errorHandler?: ActionTakerErrorHandler, 25 | type?: string = 'takeEvery' 26 | ) { 27 | if ( 28 | typeof pattern !== 'string' && 29 | !Array.isArray(pattern) && 30 | typeof pattern !== 'function' 31 | ) { 32 | throw new Error('pattern must be a string, array of strings, or function') 33 | } 34 | 35 | if (typeof observableQuery !== 'function') { 36 | throw new Error('observableQuery must be a function that returns an observable horizon query') 37 | } 38 | 39 | if (successHandler && typeof successHandler !== 'function') { 40 | throw new Error('successHandler must be a function') 41 | } 42 | 43 | if (errorHandler && typeof errorHandler !== 'function') { 44 | throw new Error('errorHandler must be a function') 45 | } 46 | 47 | return { 48 | pattern, 49 | observableQuery, 50 | successHandler, 51 | errorHandler, 52 | type, 53 | subscribers: [] 54 | } 55 | } 56 | 57 | /** 58 | * Function that creates an actionTakerManager from an actionTaker. An 59 | * actionTakerManager is a plain object with one property: remove(). remove() 60 | * unsubscribes every one of its actionTaker's subscriptions, and then removes 61 | * that actionTaker from the actionTakers array. 62 | */ 63 | export function createActionTakerManager ( 64 | actionTakers: ActionTakers, 65 | actionTaker: ActionTaker 66 | ): ActionTakerManager { 67 | const remove = () => { 68 | // unsubscribe from all of this actionTaker's subscribers 69 | actionTaker.subscribers.forEach((subscriber) => { 70 | subscriber.unsubscribe() 71 | }) 72 | // remove this actionTaker from actionTakers array 73 | actionTakers.splice(actionTakers.indexOf(actionTaker), 1) 74 | } 75 | 76 | return { 77 | remove 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/createMiddleware.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createActionHandler } from './actionHandler' 4 | 5 | import type { HorizonInstance, Dispatch, Action, Middleware } from './' 6 | import type { ActionTakers } from '../types/horizon-redux' 7 | 8 | /** 9 | * Function that creates the horizon-redux middleware, and also provides 10 | * horizon-redux with the Redux store's dispatch method when the middleware is 11 | * added to the store. 12 | */ 13 | export default function createMiddlewareCreator ( 14 | hz: HorizonInstance, 15 | actionTakers: ActionTakers 16 | ) { 17 | return (): Middleware => ({ dispatch, getState }) => { 18 | // create the actionHandler 19 | const handleAction = createActionHandler(hz, actionTakers, dispatch, getState) 20 | 21 | return (next: Dispatch) => (action: Action) => { 22 | handleAction(action) 23 | return next(action) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import createMiddlewareCreator from './createMiddleware' 4 | import { createActionTaker, createActionTakerManager } from './actionTaker' 5 | 6 | import type { HorizonInstance } from '../types/horizon' 7 | import type { Middleware } from '../types/redux' 8 | import type { ActionTakers, ActionTakerAdder } from '../types/horizon-redux' 9 | 10 | export default function HorizonRedux ( 11 | hz: HorizonInstance 12 | ): { 13 | createMiddleware: () => Middleware, 14 | addActionTaker: ActionTakerAdder, 15 | takeEvery: ActionTakerAdder, 16 | takeLatest: ActionTakerAdder 17 | } { 18 | if (typeof hz !== 'function') { 19 | throw new Error('must pass HorizonRedux a Horizon.io client instance') 20 | } 21 | 22 | let actionTakers:ActionTakers = [] 23 | 24 | // The Redux middleware creator. When this is added to the Redux store, it 25 | // will replace the dispatch function with Redux's dispatch. 26 | const createMiddleware = createMiddlewareCreator(hz, actionTakers) 27 | 28 | // Function that adds action takers to the actionTakers array 29 | const addActionTaker: ActionTakerAdder = ( 30 | pattern, 31 | observableQuery, 32 | successHandler, 33 | errorHandler, 34 | type 35 | ) => { 36 | // Create the actionTaker 37 | const actionTaker = createActionTaker(pattern, observableQuery, successHandler, errorHandler, type) 38 | // Add the new actionTaker to actionTakers array 39 | actionTakers.push(actionTaker) 40 | 41 | // Create & return the actionTakerManager (which has the remove() method) 42 | return createActionTakerManager(actionTakers, actionTaker) 43 | } 44 | 45 | const takeEvery: ActionTakerAdder = ( 46 | pattern, 47 | observableQuery, 48 | successHandler, 49 | errorHandler 50 | ) => addActionTaker(pattern, observableQuery, successHandler, errorHandler, 'takeEvery') 51 | 52 | const takeLatest: ActionTakerAdder = ( 53 | pattern, 54 | observableQuery, 55 | successHandler, 56 | errorHandler 57 | ) => addActionTaker(pattern, observableQuery, successHandler, errorHandler, 'takeLatest') 58 | 59 | return { 60 | createMiddleware, 61 | addActionTaker, 62 | takeEvery, 63 | takeLatest 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ActionTakerAdder } from '../types/horizon-redux' 4 | import type { HorizonInstance } from '../types/horizon' 5 | import type { Middleware } from '../types/redux' 6 | 7 | /** 8 | * horizon-redux module interface 9 | */ 10 | declare module.exports: (hz: HorizonInstance) => { 11 | createMiddleware: () => Middleware, 12 | addActionTaker: ActionTakerAdder, 13 | takeEvery: ActionTakerAdder, 14 | takeLatest: ActionTakerAdder 15 | } 16 | -------------------------------------------------------------------------------- /types/horizon-redux.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Action, ActionType, Dispatch, GetState } from './redux' 4 | import type { HorizonInstance, HorizonObservable } from './horizon' 5 | 6 | export type ActionTakerPattern = ActionType|Array|(a:Action)=>boolean 7 | export type ActionTakerObservableQuery = (hz:HorizonInstance, action:Action, getState:GetState)=>HorizonObservable 8 | export type ActionTakerSuccessHandler = (result:any, action:Action, dispatch:Dispatch)=>void 9 | export type ActionTakerErrorHandler = (err:any, action:Action, dispatch:Dispatch)=>void 10 | export type ActionTakerType = string 11 | 12 | export type ActionTaker = { 13 | pattern: ActionTakerPattern, 14 | observableQuery: ActionTakerObservableQuery, 15 | successHandler?: ActionTakerSuccessHandler, 16 | errorHandler?: ActionTakerErrorHandler, 17 | subscribers: [], 18 | type: ActionTakerType 19 | } 20 | 21 | export type ActionTakers = Array 22 | 23 | export type ActionTakerManager = { 24 | remove: ()=>void 25 | } 26 | 27 | export type ActionTakerAdder = ( 28 | pattern: ActionTakerPattern, 29 | observableQuery: ActionTakerObservableQuery, 30 | successHandler?: ActionTakerSuccessHandler, 31 | errorHandler?: ActionTakerErrorHandler, 32 | type?: ActionTakerType 33 | )=>ActionTakerManager 34 | -------------------------------------------------------------------------------- /types/horizon.js.flow: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | export type HorizonCollection = any 6 | export type HorizonObservable = any 7 | export type HorizonInstance = (collectionName: string) => HorizonCollection 8 | export type Horizon = (config: Object) => HorizonInstance 9 | -------------------------------------------------------------------------------- /types/redux.js.flow: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | // Redux/Middleware types 6 | // Mostly taken from https://github.com/flowtype/flow-typed/blob/master/definitions/npm/redux_v3.x.x/flow_%3E%3Dv0.23.x/redux_v3.x.x.js 7 | export type State = any 8 | export type ActionType = string|Symbol 9 | export type Action = { 10 | type: ActionType, 11 | payload?: any, 12 | error?: boolean, 13 | meta?: any 14 | } 15 | export type Reducer = (state: State, action: Action) => State 16 | export type Dispatch = (a: Action) => any 17 | export type GetState = () => any 18 | export type Store = { 19 | dispatch: Dispatch, 20 | getState: () => State, 21 | subscribe: (listener: () => void) => () => void, 22 | replaceReducer: (reducer: Reducer) => void 23 | } 24 | export type MiddlewareAPI = { dispatch: Dispatch, getState: () => State } 25 | export type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | const { NODE_ENV } = process.env 5 | 6 | const plugins = [ 7 | new webpack.optimize.OccurenceOrderPlugin(), 8 | new webpack.DefinePlugin({ 9 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV) 10 | }) 11 | ] 12 | 13 | const filename = `horizon-redux${NODE_ENV === 'production' ? '.min' : ''}.js` 14 | 15 | NODE_ENV === 'production' && plugins.push( 16 | new webpack.optimize.UglifyJsPlugin({ 17 | compressor: { 18 | pure_getters: true, 19 | unsafe: true, 20 | unsafe_comps: true, 21 | screw_ie8: true, 22 | warnings: false 23 | } 24 | }) 25 | ) 26 | 27 | module.exports = { 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.js$/, 32 | loaders: ['babel'], 33 | exclude: /node_modules/ 34 | } 35 | ] 36 | }, 37 | entry: [ 38 | './src/index' 39 | ], 40 | output: { 41 | path: path.join(__dirname, 'dist'), 42 | filename, 43 | library: 'HorizonRedux', 44 | libraryTarget: 'umd' 45 | }, 46 | plugins 47 | } 48 | --------------------------------------------------------------------------------