Components are the building blocks of React applications. It’s almost impossible to build a React application and not make use of components. It’s widespread to the point that some third-party packages provide you with components you can use to integrate functionality into your application.
These third-party components tend to be reusable. The difference between them and components you probably have in your application has to do with specificity.
Here’s what I mean. Say you run a company that sells polo shirts. There are two things you can do:
- You can produce polos that already have a design on them, or
- You can have the buyer choose the design they want.
Some fundamental things will be consistent, like all polos should be short-sleeved. But the user can pick variations of the shirts, such as the color and size. A short-sleeve polo would make a good React component in that case: it’s the same item with different variations.
Now let’s say you’re working on a login form. Like polo shirts, the form has consistent characteristics, but instead of size and color variations, we’d be looking at input fields, a submit button, perhaps even a lost password link. This can be componentized and implemented with variations in the inputs, buttons, links, etc.
Input Element Example
Let’s look at it from the perspective of creating an input field for your form. Here is how a typical text input will look like as a React component:
class Form extends React.Component {
this.state = {
username: ''
}
handleChange = (event) => {
this.setSate({ username: event.target.value })
}
render() {
return (
<input
name="username"
type={type}
placeholder="Enter username"
onChange={this.handleChange}
value={this.state.username}
/>
)
}
}
To make this input element reusable in other places and projects, we’ll have to extract it into its own component. Let’s call it FormInput
.
const FormInput = ({
name,
type,
placeholder,
onChange,
className,
value,
error,
children,
label,
...props
}) => {
return (
<React.Fragment>
<label htmlFor={name}>{label}</label>
<input
id={name}
name={name}
type={type}
placeholder={placeholder}
onChange={onChange}
value={value}
className={className}
style={error && {border: 'solid 1px red'}}
/>
{ error && <p>{ error }</p>}
</React.Fragment>
)
}
FormInput.defaultProps = {
type: "text",
className: ""
}
FormInput.propTypes = {
name: PropTypes.string.isRequired,
type: PropTypes.string,
placeholder: PropTypes.string.isRequired,
type: PropTypes.oneOf(['text', 'number', 'password']),
className: PropTypes.string,
value: PropTypes.any,
onChange: PropTypes.func.isRequired
}
The component accepts certain props, such as the attributes that we need to make the input with valid markup, including the placeholder, value and name. We set up the input element in the render function, setting the attribute values as the props that are passed to the component. We even bind the input to a label to ensure they’re always together. You can see that we’re not making assumptions by predefining anything. The idea is to ensure that the component can be used in as many scenarios as possible.
This makes for a good component because it enforces good markup (something Brad Frost calls dumb React) which goes to show that not every component has to be some highly complex piece of functionality. Then again, if we were talking about something super basic, say a static heading, then reaching for a React component might be overkill. The possible yardstick for making something a reusable component probably ought to be when you need the same functionality in other parts of an application. There’s generally no need for a “reusable” component if that component is only used once.
We can make use of our input component in another component, the LoginPage
.
class LoginPage extends React.Component {
state = {
user: {
username: "",
password: ""
},
errors: {},
submitted: false
};
handleChange = event => {
const { user } = this.state;
user[event.target.name] = event.target.value;
this.setState({ user });
};
onSubmit = () => {
const {
user: { username, password }
} = this.state;
let err = {};
if (!username) {
err.username = "Enter your username!";
}
if (password.length < 8) {
err.password = "Password must be at least 8 characters!";
}
this.setState({ errors: err }, () => {
if (Object.getOwnPropertyNames(this.state.errors).length === 0) {
this.setState({ submitted: true });
}
});
};
render() {
const {
submitted,
errors,
user: { username, password }
} = this.state;
return (
<React.Fragment>
{submitted ? (
<p>Welcome onboard, {username}!</p>
) : (
<React.Fragment>
<h3>Login!</h3>
<FormInput
label="Username"
name="username"
type="text"
value={username}
onChange={this.handleChange}
placeholder="Enter username..."
error={errors.username}
required
className="input"
/>
<FormInput
label="Password"
name="password"
type="password"
value={password}
onChange={this.handleChange}
placeholder="Enter password..."
error={errors.password}
className="input"
required
/>
<Button
type="submit"
label="Submit"
className="button"
handleClick={this.onSubmit}
/>
</React.Fragment>
)}
</React.Fragment>
);
}
}
See how LoginPage
uses the FormInput
twice? We’re using it both as the text input for a username and another text input for a password. If we want to make any changes to how the input functions, we can make those changes inside the FormInput
component file we created and they will be applied to every instance where the input component is used. That’s the fundamental upside to having reusable components: you don’t have to repeat yourself.
Even the errors are displayed from the FormInput
component.
The onSubmit
function first validates the user
object which we get from the form, ensuring that it fits the structure that there is a value for username
. Notice that we can even extend the input’s functionality, like we did to check that the password contains at least eight characters.
If you look at the code, you’ll see a have a Button
component in there. That’s not the same as a HTML <button>
element, but instead another component that takes the props that define the type of button we want (submit, reset, button), its class name, what to do on click, and the label. There are lots of other button attributes we could integrate to enforce whatever standard is needed.
const Button = (props) => (
<button
type={props.type}
className={props.className}
onClick={props.handleClick}
>
{props.label}
</button>
)
Here’s our final login form when all of our components are put together.
See the Pen
Reusable Button Component by Kingsley Silas Chijioke (@kinsomicrote)
on CodePen.
Wanna give this a try yourself? Try working on a reusable <select>
element. If that’s too difficult, you can start with a <textarea>
element, then perhaps a checkbox before hopping over to <select>
. The key idea is to make it generic. I’ll like to see what you came up with, so link up your work in the comment section!
Nice!
Let’s say that you want to add an “eye” icon for password inputs with show/hide functionality.
Would you add that logic in the FormInput component and then enable it via “addPasswordReveal” prop or rather create a new separate component “FormInputPassword”?
What if you have many different cases for different types?
Thank you, Roy.
For the first scenario, you mentioned, I’ll do as you said – add the icon then use a boolean value to either show or hide it. If I want to have different types, I can either create a new component called FormInputPassword as you mentioned. But I fear that doing that might not be needful. So I might opt for having a name prop to determine the type of icon to display. By default, I can choose to set the value as false, so when a name is based, the icon that fits that name will be used.
I hope that answers your question.
Hey quick accessibility heads-up. You connected the label and the input with the wrong attribute. The for attribute on the label points to an id. Unfortunately you don’t set the id on the input field. Thus you can’t click the label to focus the input.
If you don’t like to use for and id, you could wrap the input element with the label.
Hello Jonas,
Thanks for calling my attention to that, I have updated the post and the demo.
I appreciate.
Great article, thank you. I’ll get practicing.
Hi Silas,
Thanks for the article, it’s very well written. However I don’t think this is a good example. When you encapsulate the
<input>
, you are in effect creating a new API for the developer, so the developer has to look up the API before creating a new<input>
. To usefully encapsulate this, you need to add more to the component, otherwise it’s not DRY. For example, styling classes, hiding the input state, accessibility, input validation. I have seen this issue with a bootstrap REACT library – all the options that would normally be enabled with extra classes were turned into attributes/properties, sometimes with subtly different namesJohn