Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Improving Component Consumption with Defaults

December 19, 2018
reacttypescript

Default prop values allow us to improve the consumption experience of our components. In this post, we refactor an existing React TextAreaField component, to make it easier to consume. Our React component is built with TypeScript.

defaultProps

Current component

Here’s the component we need to refactor:

import React, { FC, ChangeEvent } from "react";

interface IProps {
  id: string;
  label: string;
  value: string;
  rows: number;
  onChange: (newValue: string) => void;
}
const TextAreaField: FC<IProps> = ({ id, label, value, onChange, rows }) => {
  const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
    onChange(e.currentTarget.value);
  };
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <textarea id={id} value={value} onChange={handleChange} rows={rows} />
    </div>
  );
};

export default TextAreaField;

So, the component renders a label with a textarea containing the value passed in via the props. We also let the consuming component know when the value has changed via a onChange function prop so that it can manage the value in its state.

An example of a consuming component referencing this is in the following snipet:

<TextAreaField
  id="notes"
  label="Notes"
  value={this.state.notes}
  onChange={this.handleNotesChange}
  rows={3}
/>

So, there are quite a few props we need to pass.

Adding default values

We could default a couple of these props so that we don’t have to supply these when consuming the component unless we require a value that is different to the default. We are going to default id to a unique id and rows to 3.

First, we need to make these props in the interface optional by adding a ? before the type annotations:

interface IProps {
  id?: string;  label: string;
  value: string;
  rows?: number;  onChange: (newValue: string) => void;
}

We are going to use the uniqueId function from lodash, so, let’s install it first:

npm install lodash

Let’s not forget to install the TypeScript types:

npm install @types/lodash --save-dev

We can now import the uniqueId function into our component file:

import { uniqueId } from "lodash";

Now it’s time to define the default values on our component. We could do this using the defaultProps static prop on the component as follows:

const TextAreaField: FC<IProps> = ({ id, label, value, onChange, rows }) => {
  ...
};
TextAreaField.defaultProps = {  id: uniqueId(),  rows: 3};

Alternatively, we can just add the defaults where we destructure the props which is arguably a little more readable:

const TextAreaField: SFC<IProps> = ({
  id,
  label = uniqueId(),  value,
  onChange,  rows = 3
}) => {
  ...
};

Our consuming component is now as follows, if we are happy to take the default id and rows:

<TextAreaField
  label="Notes"
  value={this.state.notes}
  onChange={this.handleNotesChange}
/>

Optional function parameters

Let’s make our component even nicer by not requiring the label. If the label is not supplied let’s not render it.

So, let’s make this optional in our props interface:

interface IProps {
  id?: string;
  label?: string;
  value: string;
  rows?: number;
  onChange: (newValue: string) => void;
}

We can then only render the label when it contains a value:

<div>
  {label !== undefined && <label htmlFor={id}>{label}</label>}  <textarea id={id} value={value} onChange={handleChange} rows={rows} />
</div>

Job done! If we don’t want a label for our field, the consumption has been simplified to the following:

<TextAreaField value={this.state.notes} onChange={this.handleNotesChange} />

Class component equivalent

What about setting defaults in class-based components?

Well, we can set default values using the defaultProps static prop:

class TextAreaField extends Component<IProps> {
  public static defaultProps = {    id: uniqueId(),    rows: 3  };  public render() {
    const { label, id, value, rows } = this.props;
    return (
      <div>
        {label !== undefined && <label htmlFor={id}>{label}</label>}
        <textarea
          id={id}
          value={value}
          onChange={this.handleChange}
          rows={rows}
        />
      </div>
    );
  }
  private handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
    this.props.onChange(e.currentTarget.value);
  };
}

Alternatively, we can set defaults when we destructure the props in the render method:

class TextAreaField extends Component<IProps> {
  public render() {
    const { label, id = uniqueId(), value, rows = 3 } = this.props;    return (
      ...
    );
  }
  ...
}

Wrap up

Adding default prop values to our components is super simple and doing this when we destructure the props makes the code arguably easier to read. Taking the time to think and implement sensible defaults improves the consumption experience of our components.

Discuss this post on twitter

Using TypeScript with React

For React developers eager to learn TypeScript

Learn how to utilize TypeScript’s sophisticated type system to make React development faster and your code more readable
Using TypeScript with React
Find out more