Render Prop
Demo available at ashtonsix.com/render-prop
The term "render prop" (reactjs.org/docs/render-props) refers to a simple technique for sharing code between React components using a prop whose value is a function.
This library makes using the technique a little easier. Here the "render prop" is called with the value of this.state:
import React from 'react'
import RenderProp from 'render-prop'
class TimerModel extends RenderProp {
state = {seconds: 0}
didMount() {
this.interval = setInterval(() => {
this.setState({seconds: this.state.seconds + 1})
}, 1000)
}
willUnmount() {
clearInterval(this.interval)
}
}
class TimerView extends React.Component {
render() {
const {seconds} = this.props
return <span>{seconds} seconds</span>
}
}
const Timer = () => (
<TimerModel render={({seconds}) => <TimerView seconds={seconds} />} />
)Integrating with Stores
Stores help manage state. Here we subscribe to updates on a global store (via this.subscribeTo):
import React from 'react'
import RenderProp, {Store} from 'render-prop'
const CounterStore = new Store(
(state, action) => {
switch (action.type) {
case 'INCREMENT':
return {counter: state.counter + action.payload}
default:
return state
}
},
{counter: 0}
)
class CounterModel extends RenderProp {
state = {counter: 0}
didMount() {
this.update = this.update.bind(this)
this.subscribeTo(CounterStore, this.update)
}
update() {
const {counter} = CounterStore.getState()
this.setState({counter})
}
}
class CounterView extends React.Component {
render() {
const {counter} = this.props
return (
<div>
Value: {counter}
<button
onClick={() => CounterStore.dispatch({type: 'INCREMENT', payload: 1})}
>
Increment
</button>
</div>
)
}
}
const Counter = () => (
<CounterModel render={({counter}) => <CounterView counter={counter} />} />
)
Storeis provided for convenience, howeverthis.subscribeToalso works with Redux.
Using Lifecycle Methods
Models created by render-prop are a simple extension of React components. You can use props, state and lifecycle methods just like you would anywhere else. These methods are available:
- willMount()
- didMount()
- willReceiveProps(nextProps)
- shouldUpdate(nextProps, nextState)
- willUpdate(nextProps, nextState)
- didUpdate(prevProps, prevState)
- willUnmount()
- didCatch()
We've removed the word "component" from the lifecycle methods to avoid naming collisions with the library itself.
Improving Performance
We want to update our view when one of our todos changes, but not when changes occur in other (unrelated) parts of the store's state. You can optionally pass a selector (or a function, or an array of selectors) to this.subscribeTo; now the callback will only be activated when this part of the store's state changes:
import React from 'react'
import RenderProp from 'render-prop'
class TodosModel extends RenderProp {
state = {}
didMount() {
this.update = this.bind.update()
// shallow equality check on every todo in `GlobalStore.getState().todos`
this.subscribeTo(GlobalStore, this.update, 'todos.[].{}')
}
update() {
/* ... */
}
}The compare utility implements the selector-based comparison above. Here's how you can use it independently:
import {compare} from 'render-prop'
// returns `true` if something changed
compare(stateA, stateB, ['todos.[].{}', 'filter'])