Building Reusable Components in ReactJS
This post aims to demonstrate how to make truly reusable components using what are known as render-props
and props.children
.
Modularity Vs. Reusability
Just because it's modular doesn't mean it's reusable. Probably the most common example of this is with forms. If you're making a <Form />
component for posting data, chances are you're also making one for editing that same data. If you're making a <Login />
, then you're probably also making a <Signup />
. A strictly modular way of doing this would be to make a bunch of individual components like this:
class Login extends Component{
constructor(props){
super(props);
this.state = {
inputs: {
username: "",
password: ""
}
}
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e){
// ...state changes
}
handleSubmit(e){
e.preventDefault();
this.props.submit(this.state.inputs)
}
render(){
<form onSubmit={this.handleSubmit}>
// ...input elements
</form>
}
}
class Signup extends Component{
...
}
class AddForm extends Component{
...
}
class EditForm extends Component{
...
}
None of these are reusable. They are built specifically for a single context. They also share a TON of code: They all have identical handleChange
and handleSubmit
methods. Plus, the render
methods only differ by the set of inputs unique to each form. Same with this.state
. If we continue with this purely modular approach, we would inevitably repeat these lines over and over again.
There is a better way. Enter render-props
.
Render Props
Render props are just that: a function passed into a component via props called render
. This is important because it allows us to abstract away all the repeated code into a single component. We then instantiate it with a unique set of code only when we need to. Here is a trivial demonstration:
const Container = props => {
const someReusableMethod = dataFromBeyond => console.log(dataFromBeyond);
return props.render({someReusableMethod});
}
const Component = props => {
const {someReusableMethod, uniqueData} = props;
return (
<div>
<button onClick={()=> someReusableMethod(uniqueData)}/>
</div>
)
}
const App = props => {
const rawData = "I'm data!";
return (
<Container render={props => <Component {...props} uniqueData={rawData}/>}/>
)
}
In this example <Container />
has a method that can be used (borrowed) by any other component without having to explicitly re-create it. When we want the functionality provided by the Container
(its reusableMethod
plus whatever other goodies it might have built-in), we simply create an instance of the <Container />
component, pass it a prop called render
which takes an object with a someReusableMethod
property, and output whatever we want!
Applying render-props
to Forms
The key to implementing render-props
properly is identifying which methods/variables we want to make universally available to other components. In the case of basic forms, it's really only 3 things: handleChange
, handleSubmit
, and inputs
. These will become the parameters of the props.render
function:
class FormContainer extends Component{
constructor(props){
...
}
handleChange(e){
const {type, name, checked, value} = e.target;
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[name]: type === "checkbox" ? checked : value
}
}))
}
handleSubmit(e){
e.preventDefault();
this.props.submit(this.state.inputs);
}
render(){
return this.props.render({
handleSubmit: this.handleSubmit,
handleChange: this.handleChange,
inputs: this.state.inputs
})
}
}
We also have to consider what methods/variables <FormContainer />
will need to be provided via props in order to function properly.
First, we will want to populate our state with inputs from props because this component is intended to be reusable for ALL forms:
constructor(props){
this.state = props.inputs;
}
Second, note that handleSubmit
calls a function props.submit()
. That is also something we will have to provide when we instantiate <FormContainer />
.
Reusing FormContainer
Now that we've abstracted all the repetitive code into FormContainer
, let's use it!
We can now make forms with WAY less code than before:
const AuthenticationForm = props => {
const {handleSubmit, handleChange, inputs} = props;
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>
)
}
//Login and Signup
<FormContainer
submit={inputs => location.url === "/login" ?
login(inputs) : signup(inputs)}
inputs={{
username: "",
password: ""
}}
render={props => <AuthenticationForm {...props}/>}
/>
Props.children
In addition to render-props
, props.children
offers another way to make components more reusable. You've probably seen them before:
const msg = "and so is pretty much anything you put in between the Component tags";
<Component >
<h1>This is a child prop</h1>
<h2>So is this</h2>
{msg}
</Component>
We often like to check for certain conditions prior to rendering particular components. Two common examples are async loading and error handling.
The deliberate approach looks something like this:
const DataComponent = props => {
const {loading, err, data} = props;
if(loading){ return <div>...Loading</div>}
if(err){return <div>...Sorry, your data is unavailable</div>}
return (
<div>{data}</div>
)
}
Not only is this repetitive for every component waiting for incoming data, but we're forcing the DataComponent
to do a job that isn't really its responsibility.
We can use props.children
to handle this more elegantly:
const Loader = props => {
const {loading, render} = props;
if(loading) return render();
return props.children;
}
const LoadingDisplay = props => {
return (
<div>...Loading</div>
)
}
const ErrorDisplay = props => {
const {code, msg} = props.err;
return (
<div>Error! Status:{code} Message: {msg}</div>
)
}
const ErrorHandler = props => {
const {err, render} = props;
if(err.response.status) return props.render({msg: err.message, code: err.response.status});
return props.children;
}
const DataComponent = props => {
const {data} = props;
return (
<div>{data}</div>
);
}
const App = props => {
const {loading, err, data} = props;
return (
<Loader loading={loading} render={()=> <LoadingDisplay>}>
<ErrorHandler err={err}render={props => <ErrorDisplay {...props}/>}>
<DataComponent data={data}/>
</ErrorHandler>
</Loader>
)
}
The Loader
and ErrorHandler
each check a condition in case a separate component needs to render before defaulting to its children. There is a separation of concerns between all the components, allowing them to be reused in multiple contexts.
Challenges
-
Build a reusable
Toggler
component which maintains its own state and usesrender-props
to allow any other component access to itstoggle()
method andisToggled
state. -
Build a reusable
DataList
component usingrender-props
which maps through any arbitrary array and renders an array of arbitrary components provided to it via props. -
Come up with some ways to make the
FormContainer
from above even more dynamic. What if you want to be able to reset the inputs after submission? Or what about dynamic form validation? Get creative!
Check out my github repository if you get stuck :)