├── .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 | [](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 |