How to write reusable components in React?
Reusable component is one of the key features which has contributed to the growth of ReactJS, it implies a piece of UI which can be used across the application to build more than one UI instance. With the progression of ReactJS, a few patterns have evolved which are commonly used among many developers to utilize the reusable component feature of React in a very nifty way.
When to create a reusable pattern?
When we expect creating repetitive components with similar CSS style, handling events in the same way, or performing an action which can follow a pattern and can be reused with only configuration changes, we can consider creating a reusable component.
Let’s take a look at few of the patterns which are briefly described below.
Container – Presentational component:
Container component’s primary focus is on how things work. It provides data and behavior to presentational components or other container components. Whereas, the presentational component is concerned about how data is shown to the user. Combining these two components together makes it possible to separate handling application logic with the view. This makes it easy to enforce the separation of concerns. Examples: Sidebar, Page, ContextMenu, etc.
Generic and Specialized components:
In generic components, we create a very basic UI with limited functionality that is highly usable, and then build specialized components on top of it. For example, a very core highly reusable component in an application could be a custom input box, which is used throughout the project. We can create a simple component with design and basic validation function, and let all the other props forward to the HTML input tag.
import { useState } from "react";
const Form = ({
inputItems = [],
headerText = "",
submitButtonText = "Submit",
onSubmit = () => {},
}) => {
const [inputValues, setInputValues] = useState({});
const handleSubmit = () => {
onSubmit(inputValues);
};
const onChange = (itemId) => {
return (e) => {
setInputValues({ ...inputValues, [itemId]: e.target.value });
};
};
return;
<form onSubmit={handleSubmit}>
<h2>{headerText}</h2>
<div>
{inputItems.map((item) => (
<input
key={item.id}
onChange={onChange(item.id)}
placeholder={item.label}
value={inputValues[item.id] || ""}
/>
))}
</div>
<input value={submitButtonText} type="submit" />
</form>;
};
export default Form;
we can now create a specialized component, wrapping the above generic component handling use case scenarios specific to the requirements, which would help us avoid handling a lot of configurations in the core component.
import React from "react";
import Form from "./Form";
const NewCompanyRequestForm = () => {
const handleSubmitRequest = (values) => {
//handle form submitted here
};
return (
<Form
headerText={"New Company Request"}
inputItems={[
{ label: "Name", id: 1 },
{ label: "Email", id: 2 },
{ label: "Requested Organisation", id: 3 },
]}
submitButtonText={"Submit your request"}
onSubmit={handleSubmitRequest}
/>
);
};
export default NewCompanyRequestForm;
Flattening the props:
Props is one of the ways by which we pass data to a component. Designing the contract for props could have a significant impact on how efficient they are. One of the simple rules we can follow while passing the props is we can flatten them as much as possible, i.e., instead of passing the entire property as an object, we should have well defined keys for each property and pass them individually.
Few advantages of having flatten props are – they help us provide better readability, easier maintenance as handling objects can be sometimes difficult and lead to unexpected result for hooks like useEffect , more flexibility as very often multiple components though may have similar use cases but not have the same data structure.
import React from "react";
const UserAccount = ({ user }) => {
return (
<div>
<div>
<h2>{user.name}</h2>
<h3>{user.title}</h3>
</div>
<div>
{Object.keys(user.assets.image).map((imageTitle) => (
<div>
<p>{imageTitle}</p>
<img alt="" src={user.assets.image[imageTitle]} />
</div>
))}
</div>
</div>
);
};
export default UserAccount;
In the example above, the image field is buried a couple of levels deep in the user object. If we change our user data structure, this component could break. Also, we can use this component only to display the users’ accounts. If we want to reuse this component to show data for an organization, and the data structure of the organization object differs from the user object, we can’t do that. Instead we can avoid relying on nested object props and make it more simpler as shown below.
import React from "react";
const UserAccount = ({ title, subTitle, mediaMap }) => {
return (
<div>
<div>
<h2>{title}</h2>
<h3>{subTitle}</h3>
</div>
<div>
{Object.keys(mediaMap).map((imageTitle) => (
<div>
<p>{imageTitle}</p>
<img alt="" src={mediaMap[imageTitle]} />
</div>
))}
</div>
</div>
);
};
export default UserAccount;
Provide fallback props to the component:
When handling props in the component, it is always better to have fallback or default values, so that when the parent component chooses to skip some props which might not be relevant for them or developers unintentionally forget to pass them, we can still ensure that it doesn’t break the application.
import React from "react";
const List = ({ headerText, items }) => {
return (
<div>
<div>{headerText}</div>
<div>
{items.map((item) => (
<div>{item}</div>
))}
</div>
</div>
);
};
export default List;
In the above example, if the parent component does not pass `items` props for any reason, then this might lead to a blank screen for the users. Having a default value could help run the application as expected with empty values and at the same time will not crash the application.
import React from "react";
const List = ({ headerText, items = [] }) => {
return (
<div>
<div>{headerText}</div>
<div>
{items.map((item) => (
<div>{item}</div>
))}
</div>
</div>
);
};
export default List;
Limit the scope of the component:
A reusable component should always have its scope defined. For example, for styled components, we should avoid adding functional details, and rather pass them as props. For Container components, it would be ideal to not include any UI details and the children should be passed as props.
Conclusion:
Very often the pros of following best practices are not visible at the early stages of the development, but as the project grows it makes life much easier while adding new features, modifying the existing features and also making it easier to understand the codebase for new team members. Using the above patterns and practices, not only can we utilize the Reusable Component feature of React in a very robust way, but also enjoy the benefits of best practices and create easy to maintain, scalable applications.
Add Comment
You must be logged in to post a comment.
Sanjay
Very informative.
Sanjay
Very informative.