redux-saga-sc
redux-saga-sc copied to clipboard
Provides sagas to easily dispatch redux actions over SocketCluster websockets
redux-saga-sc
This package provides ready to use sagas to connect SocketCluster clients. It can be used to let your server dispatch redux actions on the client and vice verca. Or to sync a shared redux state across multiple nodes or clients.
Examples
- redux-saga-sc-demo - A demo chat app showing redux-saga-sc in action
- epic - React example project, that takes you from fun development to high quality production
Documentation
-
Sending actions between remote redux stores
-
Sending notifications with
socketEmit
action creator -
Requesting data with
socketRequest
-
Sending notifications with
-
Advanced
-
Using the
createEventChannel
factory to connect to socket events -
Using the
createChannelSubscription
factory to connect to socket channels
-
Using the
Sending actions between remote redux stores
You'll notice that this guide does not use the terms "server" and "client". Why? You could use this server to server, client to client, it doesn't matter. Instead you have a "sender" and a "receiver".
The "sender" can emit
something, the "receiver" listens for the emit
and may decide to emit
something back in response.
The "sender" can also request
something from the "receiver" requiring a response of either successType
or failureType
.
Sending notifications with socketEmit
action creator
You can use socketEmit
to dispatch simple actions that does not require a response. If there's a network problem, like if the device switch between a 3G to 4G connection, it'll automatically retry sending the emit
until it gets a delivery notification back from the receiver.
To use it, wrap your action that you want dispatched on the remote redux store like this:
./actions/chat.js
import { socketEmit } from 'redux-saga-sc'
import {
RECEIVE_MESSAGE,
} from '../constants/ActionTypes'
export const sendMessage = (message, sender) => socketEmit({
type: RECEIVE_MESSAGE,
payload: {
message,
sender,
},
})
Next step is to setup the watcher that will take actions created by socketEmit
and send it over the websocket for us.
./sagas/index.js
import { watchRequests } from 'redux-saga-sc'
import socketCluster from 'socketcluster-client'
const socket = socketCluster.connect()
export default function *sagas() {
yield [
... // your other sagas
watchEmits(socket),
]
}
And the last step is to add the watchRemote
worker to the receiver, in this example the receiver is a SocketCluster workerController:
./web/worker.js
import express from 'express'
import createStore from './store'
export const run = worker => {
const app = express()
worker.httpServer.on('request', app)
worker.scServer.on('connection', socket => createStore(socket))
}
./web/store.js
import * as reducers from '../reducers'
import createSagaMiddleware from 'redux-saga'
import { createStore, applyMiddleware, combineReducers } from 'redux'
import sagas from '../sagas'
export default (socket) => {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
combineReducers(reducers),
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(sagas, socket)
return store
}
./web/sagas/index.js
import { watchRemote } from 'redux-saga-sc'
export default function *sagas(socket) {
yield [
... // your other sagas
watchRemote(socket)
]
}
You can setup this on both sides of the websocket if you need the ability to pass actions back and forth. Actions will dispatch like any other action. Meaning you can use
yield take(RECEIVE_MESSAGE)
in the sagas of your receiver to act on it. And you can also use RECEIVE_MESSAGE
in your reducers.
The actions you dispatch wrapped in socketEmit
only dispatch on the receiver, not the local redux store.
It will automatically retry if the server does not respond.
By default emits will be done on the "dispatch" event on SocketCluster. You can change this by passing a second argument to socketEmit and use whatever event you want.
Just be sure to set the watchRemote
saga on the receiver to the same event, as that too use 'dispatch' as the default event.
Requesting data with socketRequest
While socketEmit
is useful for notifications and other stuff that you don't need to know the result, only that it was delivered, there's socketRequest
that is suitable for more advanced situations.
When you do a socketRequest
you have to pass both a successType
and failureType
that the receiver must emit back in a given timeframe.
Here's an example:
./actions/auth.js
import { socketRequest } from 'redux-saga-sc'
import {
AUTHENTICATE_REQUESTED,
AUTHENTICATE_SUCCESS,
AUTHENTICATE_FAILURE,
} from '../constants/ActionTypes'
export const signInWithEmailAndPassword = credentials => socketRequest({
type: AUTHENTICATE_REQUESTED,
payload: {
successType: AUTHENTICATE_SUCCESS,
failureType: AUTHENTICATE_FAILURE,
credentials,
},
})
To dispatch socket requests, setup watchRequests
the same way you did watchEmits
.
You setup the receiver just like you do in the socketEmit
example, using watchRemote
.
There are two important differences though. First of all, unlike socketEmit
, the action you pass in socketRequest
will also dispatch locally. This is to allow for stuff like creating progress spinners and similar.
The other difference is that you need to setup your own watcher that will act on the AUTHENTICATE_REQUESTED
in this example, and return either AUTHENTICATE_SUCCESS
or AUTHENTICATE_FAILURE
.
Here's what it should look like:
./web/sagas/auth.js
import { socketEmit } from 'redux-saga-sc'
import { authenticate } from '../models/user'
import {
AUTHENTICATE_REQUESTED
} from '../constants/ActionTypes'
export function *watchAuthenticateRequest(socket) {
while (true) { // eslint-disable-line no-constant-condition
const { payload: {
successType,
failureType,
credentials,
} } = yield take(AUTHENTICATE_REQUESTED)
try {
const authToken = yield call(authenticate, credentials)
yield put(socketEmit({ type: successType, payload: authToken }))
} catch(err) {
yield put(socketEmit({ type: failureType, payload: err }))
}
}
}
Advanced
A little more info on lower level stuff if you need to do more than what the provided watchers and action creators provides out of the box.
Using the createEventChannel
factory to connect to socket events
On the client:
import { createEventChannel } from 'redux-saga-sc'
import socketCluster from 'socketcluster-client'
const socket = socketCluster.connect({
hostname: process.env.SOCKET_HOSTNAME || location.hostname
})
export function *watchIncomingActions() {
const chan = yield call(createEventChannel, socket, 'dispatch')
while (true) {
const action = yield take(chan)
yield put(action)
}
}
On the server:
socket.emit('dispatch', {type: 'MY_ACTION', payload: { foo: 'bar' }}, () => {
console.log('Client dispatched the action!')
})
Using the createChannelSubscription
factory to connect to socket channels
One of the coolest features of SocketCluster are channels. Especially when you use channels on the Exchange as it allows you to distribute actions both vertically (multiple worker processes) and horizontally (multiple servers).
Since publishing to a channel is very straightforward there's no utility in redux-saga-sc
for that, the example below shows you how it's done (exchange
in these examples are assumed to be scWorker.exchange
passed to the store from your workerController, that's how it's done in redux-saga-sc-demo):
import { cps, take } from 'redux-saga/effects'
export function *watchMessages(exchange) {
while (true) { // eslint-disable-line no-constant-condition
const message = yield take('MESSAGE')
yield cps([exchange, exchange.publish], 'chat', message)
}
}
And here's how createChannelSubscription is implemented to dispatch actions from the channel:
import { call, take, put } from 'redux-saga/effects'
import { socketEmit, createChannelSubscription } from 'redux-saga-sc'
export function *watchExchange(socket, exchange) {
const chan = yield call(createChannelSubscription, exchange, 'chat')
while (true) { // eslint-disable-line no-constant-condition
const action = yield take(chan)
yield put(socketEmit(action))
}
}