Skip to content

Instantly share code, notes, and snippets.

@steve-taylor
Last active October 27, 2021 11:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steve-taylor/9c1ecb0bcc8c55212c696efde1da4249 to your computer and use it in GitHub Desktop.
Save steve-taylor/9c1ecb0bcc8c55212c696efde1da4249 to your computer and use it in GitHub Desktop.
Connect a React component to Bacon.js streams and buses
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