Do something when the conditions are right and cancel them if they're not.
- Simple: No complex api
- No wrapper's needed around other API's
- Focused on doing one thing
- Small in size
- Works for multiple purposes
Say you want to connect to a socket when you’re logged in and want to disconnect when you’re not.
This can only work once the socket url is known.
Also it must be disconnected when the url is changed and connect to the new one.
Here's how you can do this with raincheck:
import { when } from 'raincheck'
const doConnectWhen = when().do((url) => {
const socket = new WebSocket()
socket.connect(url)
return () => socket.close()
})
doConnectWhen("ws://url1")
doConnectWhen("ws://url2")
doConnectWhen(false)
Say you want to keep multiple connections open:
import { forEach } from 'raincheck'
const doConnectForEach = forEach().do((url) => {
const socket = new WebSocket()
socket.connect(url)
return () => socket.close()
})
doConnectForEach(['ws://url1', 'ws://url2'])
doConnectForEach(['ws://url2'])
doConnectForEach([])
The function passed into do
starts a process and returns a cancel function.
Syntax
Raincheck consists of 3 function:
when
: For single objectsforEach
: For an array of objectsforEachEntry
: For an key-value based objects
Each function accepts a map function a settings object or both:
when(s => s.value)
.do(handleFnc)
when({
do: handleFnc
})
when(s => s.value, {
do: handleFnc
})
Changes
To identify that an object has been changed, raincheck uses keys, just like react.
when
when
will always cancel & call do when a object is changed, but when you add keyExtractor
it will call changed
when the key is the same as the one before.
const check = when({
keyExtractor: item => `${item.id}+${item.url}`,
do: (item) => connectToSocket(item.url),
changed: (newValue, oldValue, key) => {
}
})
check({ url: 'url1', name: '1', id: '1' })
check({ url: 'url1', name: '2', id: '1' })
forEach
When you pass in an array to forEach
it will use the value as key. But if the objects are not strings you may use the keyExtractor
:
const check = forEach({
keyExtractor: item => `${item.id}+${item.url}`,
do: (item) => connectToSocket(item.url),
changed: (newValue, oldValue, key) => {
}
})
check([{ url: 'url1', name: '1', id: '1' }])
check([{ url: 'url1', name: '2', id: '1' }])
forEachEntry
With forEachEntry
the key of the object is used, so no keyExtractor is needed:
const check = forEachEntry({
do: (item) => connectToSocket(item.url),
changed: (newValue, oldValue, key) => {
}
})
check({'1': { url: '', name: '1' }})
check({'1': { url: '', name: '2' }})
Examples
React
class Compontent extends React.Component {
doConnectWhen = when(props => props.isLoggedIn && props.url, {
do: (url) => {
}
})
componentDidMount() {
this.doConnectWhen(this.props)
}
componentDidUpdate() {
this.doConnectWhen(this.props)
}
componentDidUnmount() {
this.doConnectWhen(null)
}
render () {
}
}
Redux
Raincheck
is particularly great when used with Redux.
To connect to a store you use createMiddleware()
, like so:
export default createMiddleware(
when(state => state.login.isLoggedIn && state.socket.url)
.do(connectToSocket)
)
Or just call it yourself inside a middleware:
export default (store) => {
let check = forEach((state) => state.sockets, {
do: item => connectToSocket(item.url),
keyExtractor: item => `${item.id}+${item.url}`,
})
(next) => (action) => {
let ret = next(action)
check(store.getState())
return ret
}
}
Reselect
const selector = createSelector(
s => s.projects,
s => s.user,
(projects, user) => (projects && {projects, user}),
)
when(selector).do(({project, login}) => {
})
PouchDB
Here an example to sync an array to PouchDb:
const check = forEach({
do: items => {
db.put(item)
return () => db.delete(item)
},
keyExtractor: item => item._id,
changed: (newValue, oldValue) => {
db.put(newValue)
}
})
Unit testing
With the mock()
function you can easily test your setup:
const connectToSocket = () => {}
const doConnectToSocketWhen = when(
({isLoggedIn, url}) => isLoggedIn && !!url
).do(connectToSocket)
it('should connect to socket', () => {
const listener = jest.fn()
const destruct = jest.fn()
let tester = doConnectToSocketWhen.mock(listener, destruct)
const url = "dfgh"
tester({
isLoggedIn: true,
url
})
expect(listener).toBeCalledWith(connectToSocket, url, expect.anything())
})
Deprecated
doForAll
, doForAllKeys
, doWhenTrue
& doWhenChanged
are deprecated and will be removed in 1.0.
Replacements
doForAll
will be replaced by forEachEntity
doForAllKeys
will be replaced by forEach
, it doesn't have to be a string anymore the key is can now be extract with keyExtractor
Both doWhenTrue
& doWhenChanged
will be replace by when
.
Chaining API
Say the socket is disconnected and you fire up a timeout to reconnect. Whenever the conditions change the cancel function will be called. In this situation you don't want to call the socket.close(), but you do want to clear the timeout.
For this situation you can use the chaining API.
The second argument of the construct function contains the functions of the chaining API. This makes it possible to 'register' a new destruct function and opt-out of the current destruct function by using next()
. If the current action isn’t finished yet you can use branch()
.
See an example of loading an image after every second,
it will cancel whatever process is active right now:
const getImage = (url, next, store, action) => {
const setTimer = ({next}) => {
let timer = setTimeout(() => next(sendRequest), 1000)
return () => clearTimeout(timer)
}
const loadImage = (result: string) => ({finish}) => {
let img = new Image()
img.onload = () => {
finish()
}
img.src = result
return () => img.src = ""
}
const readFile = (response) => ({next}) => {
let reader = new FileReader()
reader.onload = (e) => {
next(loadImage(reader.result))
}
reader.readAsDataURL(response)
return () => reader.abort()
}
const sendRequest = ({next, branch}) => {
let xhrreq = new XMLHttpRequest()
xhrreq.open('POST', url)
xhrreq.responseType = "blob";
xhrreq.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhrreq.onload = (ev) => {
if (xhrreq.status === 200) {
branch(setTimer)
next(readFile(xhrreq.response))
}
}
xhrreq.send(body)
return () => xhrreq.abort()
}
return sendRequest(next)
}
createMiddleware(
when(state => state.image.isActive && state.image.url)
.do(getImage)
)
TODO v1.0 (backwards incompatible)
- Change
map
to apply to all entries - Remove
doForAll
, doForAllKeys
, doWhenTrue
& doWhenChanged
- Choose functions for chaining API:
branch
or fork
complete
, finish
or resolve
chain
or next
- Change
when
to send all parameters to do
instead of just the value of the selector?