Improving Component Consumption with Defaults


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.

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:

Terminal window
npm install lodash

Let’s not forget to install the TypeScript types:

Terminal window
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.

Learn React with TypeScript - 3rd Edition

New

A comprehensive guide to building modern React applications with TypeScript. Learn best practices, advanced patterns, and real-world development techniques.

View on Amazon
Learn React with TypeScript - Third Edition