horizon-redux
A small library that helps you connect Horizon.io with Redux in a flexible, non-intrusive way.
What does it do?
horizon-redux helps you connect Redux with 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).
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.
horizon-redux has zero npm dependencies, and its only requirements are Horizon.io and Redux.
horizon-redux is compatible with Horizon.io 1.x and 2.x.
Interested in a different approach? See the Alternative Approaches section below for some different options for integrating Horizon with Redux.
Installation
npm i -S horizon-redux
Alternatively:
<script src="https://unpkg.com/horizon-redux/dist/horizon-redux.min.js"></script>
(exposes window.HorizonRedux as a global variable)
Usage
import HorizonRedux from 'horizon-redux'
const horizonRedux = HorizonRedux(horizon)
const hzMiddleware = horizonRedux.createMiddleware()
const store = createStore(rootReducer, [], applyMiddleware(hzMiddleware))
horizonRedux.takeLatest(
'WATCH_MESSAGES',
(horizon, action, getState) => horizon('messages').order('datetime', 'descending').limit(action.payload).watch(),
(result, action, dispatch) => dispatch({type: 'NEW_MESSAGES', payload: result}),
(err, action, dispatch) => console.log('failed to load messages:', err)
)
store.dispatch({ type: 'WATCH_MESSAGES', payload: 10 })
const someActionTaker = horizonRedux.addActionTaker()
someActionTaker.remove()
Check out the chat-app example in this repo for a basic working example based on the chat-app example from Horizon.io
API
import HorizonRedux from 'horizon-redux'
const horizonRedux = HorizonRedux(horizonInstance)
horizonRedux methods:
.createMiddleware()
Creates a Redux middleware that watches for actions that match any of the actionTakers created by horizonRedux. See horizonRedux.addActionTaker
below for more details.
Arguments:
n/a
Returns:
Redux middleware
.addActionTaker(pattern, observableQuery, successHandler, errorHandler, type)
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.
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).
Arguments:
pattern
- A string, array of strings, or function used to match against dispatched action's types.
- If it's a string, matches if
pattern === action.type
- If it's an array of strings, matches if any elements of the array are strictly equal to
action.type
- If it's a function, matches if pattern(action) returns a truthy value
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.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.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.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:
- If
'takeEvery'
, the actionTaker will add an additional subscription every time a matching action is dispatched. - 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.
Returns:
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.
Example:
horizonRedux.addActionTaker(
'WATCH_MESSAGES',
(horizon, action, getState) =>
horizon('messages').order('datetime', 'descending').limit(action.payload.limit || 10).watch(),
(result, action, dispatch) => {
dispatch(newMessages(result))
},
(err, action, dispatch) => {
console.log('failed to load messages:', err)
},
'takeLatest'
)
store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 10 } })
store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 20 } })
.takeLatest(pattern, observableQuery, successHandler, errorHandler)
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.
Example:
horizonRedux.takeLatest(
'WATCH_MESSAGES',
(horizon, action, getState) =>
horizon('messages').order('datetime', 'descending').limit(action.payload.limit || 10).watch(),
(result, action, dispatch) => {
dispatch(newMessages(result))
},
(err, action, dispatch) => {
console.log('failed to load messages:', err)
}
)
store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 10 } })
store.dispatch({ type: 'WATCH_MESSAGES', payload: { limit: 20 } })
.takeEvery(pattern, observableQuery, successHandler, errorHandler)
Identical to addActionTaker(...)
except that the type is automatically set to 'takeEvery'
(see above). Matching actions will add new subscriptions (without replacing previous ones).
Example:
horizonRedux.takeEvery(
'ADD_MESSAGE_REQUEST',
(horizon, action, getState) => horizon('messages').store(action.payload),
(id, action, dispatch) => dispatch(addMessageSuccess(id, action.payload)),
(err, action, dispatch) => dispatch(addMessageFailure(err, action.payload))
)
I'm very open to feedback, and will respond to issues quickly. Feel free to get in touch!
Alternative Approaches
-
redux-observable 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.
-
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.
License
MIT