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:
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.
If you to learn more about using TypeScript with React, you may find my course useful: