Last active
October 27, 2021 11:03
-
-
Save steve-taylor/9c1ecb0bcc8c55212c696efde1da4249 to your computer and use it in GitHub Desktop.
Connect a React component to Bacon.js streams and buses
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import Bacon from 'baconjs'; | |
/** | |
* Create a factory of higher order components that render the specified inner | |
* component using the specified mapping of property names to the streams that | |
* feed them values and the specified mapping of callback property names to | |
* the buses onto which the callbacks' first parameter is pushed when called. | |
* | |
* This is similar in concept to react-redux's connect() function, but for | |
* Bacon.js instead of Redux, and without the bells and whistles. | |
* | |
* Example (not tested!): | |
* | |
* <pre> | |
* const MyComponent = ({foo, bar, onChangeFoo, onChangeBar, onSubmit}) => ( | |
* <div> | |
* <input type="text" value={foo} onChange={onChangeFoo}/> | |
* <input type="text" value={bar} onChange={onChangeBar}/> | |
* <button onClick={onSubmit}> | |
* Submit | |
* </button> | |
* </div> | |
* ); | |
* | |
* const fooBus = new Bacon.Bus(); | |
* const barBus = new Bacon.Bus(); | |
* const submitBus = new Bacon.Bus(); | |
* | |
* const fooStream = fooBus.map(foo => foo.toUpperCase()).startWith('foo'); | |
* const barStream = barBus.map(bar => bar.toLowerCase()).startWith('bar'); | |
* | |
* Bacon.combineTemplate({ | |
* foo: fooStream, | |
* bar: barStream, | |
* }).combine(submitBus).onValue(({foo, bar}) => { | |
* // TODO: Submit the form | |
* }); | |
* | |
* const MyComponentConnected = connect({ | |
* foo: fooStream, | |
* bar: barStream, | |
* }, { | |
* onChangeFoo: fooBus, | |
* onChangeBar: barBus, | |
* onSubmit: submitBus, | |
* }); | |
* | |
* ReactDOM.render(<MyComponentConnected/>, document.querySelector('#app')); | |
* | |
* </pre> | |
* | |
* @param {object} mapStreamsToProps - mapping of property names to streams | |
* @param {object} mapBusesToProps - mapping of property names to buses | |
* @param {string} [name] - an optional display name to provide the component | |
* @returns {function} the connected component | |
*/ | |
export const connect = (mapStreamsToProps, mapBusesToProps, name) => Component => { | |
class Mapper extends React.PureComponent { | |
constructor(props) { | |
super(props); | |
this.openValve = new Bacon.Bus(); | |
this.closeValve = new Bacon.Bus(); | |
// Valve to buffer events while this component isn't mounted. | |
this.valveClosed = Bacon.update( | |
true, | |
[this.openValve], () => false, | |
[this.closeValve], () => true, | |
); | |
// Subscribe to input streams, mapping their values to the specified props | |
// via this component's state. | |
this.subscribers = Object | |
.keys(mapStreamsToProps || {}) | |
.map(prop => mapStreamsToProps[prop].holdWhen(this.valveClosed).onValue(value => { | |
this.setState({[prop]: value}); | |
})); | |
// Map callback props to bus pushes via this component's state. | |
this.state = Object | |
.keys(mapBusesToProps || {}) | |
.reduce((buses, prop) => Object.assign(buses, { | |
[prop]: mapBusesToProps[prop].push.bind(mapBusesToProps[prop]), | |
}), {}); | |
} | |
componentDidMount() { | |
this.openValve.push(); | |
} | |
componentWillUnmount() { | |
this.closeValve.push(); | |
// Unsubscribe from streams | |
this.subscribers.forEach(subscriber => { | |
subscriber(); | |
}); | |
} | |
render() { | |
return ( | |
<Component | |
{...this.props} // Directly provided props | |
{...this.state} // Stream and bus mappings | |
/> | |
); | |
} | |
} | |
Mapper.displayName = name || `connect(${Component.displayName})`; | |
return Mapper; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment