Skip to content

Instantly share code, notes, and snippets.

@wosephjeber
Last active October 6, 2019 22:19
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 wosephjeber/0fd4ab8c8592614ec300e5a644cf327e to your computer and use it in GitHub Desktop.
Save wosephjeber/0fd4ab8c8592614ec300e5a644cf327e to your computer and use it in GitHub Desktop.
Render Props vs Higher Order Components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class NameForm extends Component {
constructor() {
super()
this.state = { value: '' }
}
handleChange = ({ target }) => {
this.setState({ value: target.value })
}
render() {
return (
<form onSubmit={() => { onSubmit(value) }}>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
type="text"
value={this.state.value}
onChange={this.onChange}
/>
<button type="submit">Save</button>
</form>
)
}
}
NameForm.propTypes = {
onSubmit: PropTypes.func
}
export default NameForm
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import withSimpleFormState from './simple_form_hoc'
function NameForm({ value, onChange, onSubmit }) {
return (
<form onSubmit={() => { onSubmit(value) }}>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
type="text"
value={value}
onChange={onChange}
/>
<button type="submit">Save</button>
</form>
)
}
NameForm.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
onSubmit: PropTypes.func
}
export { NameForm }
export default withSimpleFormState(NameForm)
import React from 'react'
import { shallow } from 'enzyme'
import { NameForm } from './name_form_hoc'
describe('<NameForm />', function() {
it('sets input value from props', function() {
const wrapper = shallow(<NameForm value="Phil" />)
expect(wrapper.find('input').prop('value')).to.eq('Phil')
})
it('calls onChange callback when input changes', function() {
const onChange = sinon.spy()
const wrapper = shallow(<NameForm value="" onChange={onChange} />)
wrapper.find('input').simulate('change', {
target: { value: 'Phil' }
})
expect(onChange).to.be.calledWith('Phil')
})
it('calls onSubmit callback when form is submitted', function() {
const onSubmit = sinon.spy()
const wrapper = shallow(<NameForm value="Phil" onSubmit={onSubmit} />)
wrapper.find('form').simulate('submit')
expect(onSubmit).to.be.calledWith('Phil')
})
})
import React from 'react'
import PropTypes from 'prop-types'
import SimpleFormStateProvider from './simple_form_provider'
function NameForm({ onSubmit }) {
return (
<SimpleFormStateProvider>
{({ value, onChange }) => (
<form onSubmit={() => { onSubmit(value) }}>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
type="text"
value={value}
onChange={onChange}
/>
<button type="submit">Save</button>
</form>
)}
</SimpleFormStateProvider>
)
}
NameForm.propTypes = {
onSubmit: PropTypes.func
}
export default NameForm
import React from 'react'
import { shallow } from 'enzyme'
import NameForm from './name_form_render_props'
import SimpleFormStateProvider from './simple_form'
describe('<NameForm />', function() {
it('sets input value from SimpleFormStateProvider', function() {
const wrapper = shallow(
shallow(<NameForm />)
.find(SimpleFormStateProvider)
.prop('children')({ value: 'Phil' })
)
expect(wrapper.find('input').prop('value')).to.eq('Phil')
})
it('calls SimpleFormStateProvider onChange callback when input changes', function() {
const onChange = sinon.spy()
const wrapper = shallow(
shallow(<NameForm />)
.find(SimpleFormStateProvider)
.prop('children')({ onChange })
)
wrapper.find('input').simulate('change', {
target: { value: 'Phil' }
})
expect(onChange).to.be.calledWith('Phil')
})
it('calls onSubmit callback when form is submitted', function() {
const onSubmit = sinon.spy()
const wrapper = shallow(
shallow(<NameForm onSubmit={onSubmit} />)
.find(SimpleFormStateProvider)
.prop('children')({ value: 'Phil' })
)
wrapper.find('form').simulate('submit')
expect(onSubmit).to.be.calledWith('Phil')
})
})

React – Render Props vs Higher-order Components

Both provide solutions for code reuse.

Example: NameForm

Higher-order Components (HOCs)

A function that returns a component that renders your component. The HOC encapsulates special behavior (e.g. state management, event handlers, etc) and renders your component with props. Expects a component constructor (either a function or a class).

Example: NameForm with HOC

Pros

  • Allows you to share common functionality and behavior between components without repetition.
  • Makes it easy to unit test. The wrapped component can be mounted and tested in complete isolation from the HOC, allowing you to simply pass in mocks via props in the unit test.

Cons

  • Creates a level of indirection. It's not obvious where the wrapped component receives certain data from. Without looking at the HOC source code, there's no way to tell which props it gets from the parent that renders it and which it gets from the HOC.
  • HOCs need to be (or at least should be) configured outside of the component, which means they can't be configured based on component state.
  • HOCs wrap your entire component, so you have less flexibility in composition.

Render Prop Providers (RPPs)

A component that calls a function to render children. The RPP encapsulates special behavior (e.g. state management, event handlers, etc) and calls the render prop function with arguments. Expects a function that returns a React element.

Example: NameForm with RPP

Pros

  • Allow you to share common functionality and behavior between components without repetition.
  • Make it obvious where data comes from in your component. Props come from the parent that renders it, and data from the RPP comes from the render function parameters.
  • RPP can be configured based on both props and component state.
  • RPP can be composed within your component's render method however you'd like.

Cons

  • Can clutter your render method depending on the amount of configuration needed in the RPP and the amount of nesting.
  • Makes it very difficult to unit test your component in isolation from the RPP. Mounting your component also mounts the RPP, so the RPP must somehow be stubbed to provide mock data. Aside from some convoluted import/export techniques and replacing the original RPP module export with a double, completely stubbing out the RPP behavior is impossible.

More complex example

A more complex example involves GraphQL queries and Apollo.

With the HOC, the query cannot be configured based on state. If that's required, the state either needs to be elevated to a parent component or the query needs to be lowered to a child component.

With the RPP, the query can be configured based on props and state. But mounting the component for unit tests will trigger Apollo's behavior of executing the query, which we don't want, and will make it difficult to specify mock data.

Resources

import React, { Component } from 'react'
function withSimpleFormState(WrappedComponent) {
class ComponentWithState extends Component {
constructor() {
super()
this.state = { value }
}
handleChange = ({ target }) => {
this.setState({ value: target.value })
}
render() {
return (
<WrappedComponent
{...this.props}
onChange={this.handleChange}
value={this.state.value}
/>
)
}
}
return ComponentWithState
}
export default withSimpleFormState
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class SimpleFormStateProvider extends Component {
constructor() {
super()
this.state = { value }
}
handleChange = ({ target }) => {
this.setState({ value: target.value })
}
render() {
return this.props.children({
value: this.state.value,
onChange: this.handleChange
})
}
}
SimpleFormStateProvider.propTypes = {
children: PropTypes.func.isRequired
}
export default SimpleFormStateProvider
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment