Render Props
If you haven't already, check out the post on Higher Order Components, which is referenced in this article.
Tools: React
Overview
Render-props is a composition pattern that makes reusing React components many times easier. They are very similar to Higher Order Components with some notable differences; one being that they are quite a bit more configurable.
Think of render-props as an equipment rental store. You can rent methods and states from a particular component when you need it, instead of 'owning' them yourself. In reality, it is just a function called render
which is simply passed as props.
Render-props gets its name from how it is commonly used:
<EquipmentStore render={props => <Job {...props}/>}/>
Real World Example
React Router uses the render-props pattern in the <Route />
component to make redirecting easy, while Formik uses it extensively to make dynamic, validatable forms.
Guide
Let's demonstrate how render-props works by starting with a Toggler
component from the earlier post on HOCS:
class Toggler extends Component {
constructor() {
super()
this.state = {
on: false
}
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState(({ on }) => ({ on: !on }))
}
render() {
const { on } = this.state;
const { component , ...props } = this.props;
const C = component;
return (
<C on={on}toggle={this.toggle}{...props}/>
)
}
}
export const withToggler = C => props => <Toggler component={C}{...props}/>
This is a really handy way to to write a toggling effect exactly once throughout an entire app:
export default withToggler(AnyComponentNeedingToggler)
However, we don't have an obvious way to set the initial state of the toggle. We're stuck with setting the on
value to either false
or true
. This could be problematic if in one situation (such as an edit pop-up form) we want it to be false
to start, but in another (A login modal) we want it to be true
.
This is certainly avoidable by simply using the !
operator inside our component but then we are reinterpreting the meaning of on
instead of setting it the appropriate value.
Let's refactor Toggler
so that we can use it like a normal component employing render-props:
class Toggler extends Component {
constructor(props) {
super(props)
this.state = {
//initialize state with the 'on' value from props
on: props.on
}
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState(({ on }) => ({ on: !on }))
}
render() {
//compile all the methods and state into one object and provide it as an argument to this.props.render
const props = {
toggle: this.toggle,
...this.state
}
return this.props.render(props)
}
}
//We no longer need the higher order function withToggler
In our application we can now employ Toggler
anywhere we want, and still initialize it with a particular on
value:
//if there is no equal sign, then JSX will assume a prop value is true
<Toggler on render={({on, toggle}) => (
<div>
<button onClick={toggle}>Menu</button>
<nav className={on ? "show" : "hide"}>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</div>
)}/>
Children
You will probably come across a pattern where props.children
is used in place of props.render
. For example:
render() {
const props = {
toggle: this.toggle,
...this.state
}
return this.props.children(props)
}
This simply allows you to provide a function as children instead of React elements.
<Toggler on>
{({on, toggle}) => (
<button onClick={toggle}>Menu</button>
<nav className={on ? "show" : "hide"}>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
)}/>
</Toggler>
Forms
I would highly recommend using Formik for dealing with forms, but here is a simplified version of how it works:
export default class Form extends Component {
constructor(props) {
super(props);
this.state = props.inputs;
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange({target: {name, value, checked, type}}) {
this.setState({
[name]: type === "checkbox" ? checked : value
});
}
handleSubmit(e) {
e.preventDefault();
this.props.submit(this.state);
this.props.reset && this.setState(this.props.inputs);
}
render() {
const props = {
handleChange: this.handleChange,
handleSubmit: this.handleSubmit,
inputs: this.state,
};
return this.props.render(props)
}
}
Now you can write forms as simple presentational components receiving props:
const Login = ({handleSubmit, handleChange, inputs}) => {
return (
<form onSubmit={handleSubmit}>
<input type="text"name="username"value={inputs.username}onChange={handleChange}/>
<input type="password"name="password"value={inputs.password}onChange={handleChange}/>
</form>
)
}
Our final implementation is very simple and compact.
<Form
reset
inputs={{username: "", password: ""}
submit={inputs => postToServer(inputs)}
render={Login}
/>
Summary
Render-props is a powerful way to make your app dynamic and less verbose. By providing a function as a property of props you can expose a component's inner methods and state to the outside, which effectively lets you 'rent' common functionalities across the component tree.
Exercises
- TBD
Additional Resources
Render Props - ReactJS Docs
Advanced React Patterns - Kent C. Dodds