Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Inspired by theadam/react-flyd. Allows for flyd streams to be embedded directly into JSX, and to update content when the streams fire events.
import { Component, createElement, cloneElement, isValidElement } from 'react'
import { isStream, on, combine } from 'flyd'
export function reactive(tag='') {
class ReactiveClass extends Component {
constructor(props) {
super(props)
this.displayName = `ReactiveClass-${typeof tag==='string' ? tag : (tag.displayName || tag.name || '')}`;
this.state = {}
}
componentWillMount() {
this.subscribe(this.props)
}
componentWillReceiveProps(nextProps) {
this.subscribe(nextProps)
}
componentWillUnmount() {
this.unsubscribe()
}
addPropListener(name, prop$) {
on((value) => {
// don't re-render if value is the same.
if (value === this.state[name]) {
return
}
this.setState({ [name]: value })
}, prop$)
// return these prop$ rather than above on$ because we usually create streams in jsx. Just end those on$ will left out those prop$s which will lead to a memory leak.
// And since prop$ is the parent of on$, just end prop$ is enough.
// And since we will end prop$, user shouldn't use outter streams directly on jsx. Just a stream.map(v => v) is enough.
return prop$
}
subscribe(props) {
if (this.subscriptions) {
this.unsubscribe()
}
this.subscriptions = []
Object.keys(props).forEach(key => {
const value = props[key]
if (isStream(value)) {
const subscription = this.addPropListener(key, value)
this.subscriptions.push(subscription)
}
})
}
unsubscribe() {
this.subscriptions.forEach(subscription => subscription.end(true))
this.subscriptions = null
this.state = {}
}
render() {
const finalProps = {...this.props, ...this.state}
if (tag) {
return createElement(tag, finalProps)
}
const {children, ...props} = finalProps
return cloneElement(children, props)
}
}
return ReactiveClass
}
function hasStream(obj) {
return Object.keys(obj).some(key => isStream(obj[key]))
}
function wrapChildren(children) {
const notValidElement$s = children.filter(child => isStream(child) && !isValidElement(child()))
if (notValidElement$s.length > 0) {
return [combine(() => {
return children.map(child => {
if (!isStream(child)) {
return child
}
if (notValidElement$s.indexOf(child) > -1) {
return child()
}
return createElement(reactive(), {}, child)
})
}, notValidElement$s)]
}
return children.map(child => {
if (!isStream(child)) {
return child
}
return createElement(reactive(), {}, child)
})
}
export default function h(tag, props, ...children) {
let defaultProps = props || {}
let wrappedChildren = wrapChildren(children)
if (hasStream(defaultProps) || hasStream(wrappedChildren)) {
return createElement(reactive(tag), defaultProps, ...wrappedChildren)
}
return createElement(tag, defaultProps, ...wrappedChildren)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment