Building Reusable Components in ReactJS

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

  1. Build a reusable Toggler component which maintains its own state and uses render-props to allow any other component access to its toggle() method and isToggled state.

  2. Build a reusable DataList component using render-props which maps through any arbitrary array and renders an array of arbitrary components provided to it via props.

  3. 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 :)