Reusable React components are one of the most valuable tools for building maintainable frontend applications. Instead of repeating the same UI logic across multiple pages, you can create small, configurable components that work consistently throughout your application.
However, reusability is not simply about moving JSX into a separate file. A good component has a clear responsibility, accepts meaningful props, handles variations predictably, and remains easy to test and maintain.
In this guide, we will explore practical patterns for building reusable React components using JavaScript. You will learn how to design flexible APIs, compose components, handle default values, and avoid common abstractions that make code harder to understand.
Designing Reusable React Components with Composition and Props
The foundation of a reusable React component is a well-designed interface. Props allow parent components to control a component's content, appearance, and behavior without changing its internal implementation. For example, a single Button component can support different labels, visual variants, loading states, and click handlers.
Start by identifying the behavior that multiple parts of your application share. If several pages display buttons with consistent styling, create a common Button component rather than copying the same markup into every page. Keep its props explicit and provide sensible defaults so that simple use cases remain straightforward.
Composition is another important pattern. Instead of adding a separate prop for every possible customization, accept children and allow consumers to supply their own content. This approach works particularly well for cards, panels, modals, and layout components because it keeps the component flexible without forcing it to understand every possible use case.
The following example demonstrates reusable React components with JavaScript. It includes a configurable Button, a composable Card, a UserProfile component, and a small application that demonstrates how these pieces work together. The components remain focused on their own responsibilities while the parent component manages application data and event handling.
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
// Step 1: Create a reusable Button with a small, explicit API.
function Button({
children,
variant = "primary",
disabled = false,
onClick,
}) {
console.log(`[Button] Rendering ${variant} button`);
// Keep shared styling in one place to ensure consistent UI.
const baseStyles = {
padding: "10px 16px",
border: "none",
borderRadius: "6px",
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.6 : 1,
marginRight: "8px",
};
const variants = {
primary: { background: "#2563eb", color: "white" },
secondary: { background: "#e5e7eb", color: "#111827" },
danger: { background: "#dc2626", color: "white" },
};
// Unknown variants safely fall back to the primary appearance.
const variantStyles = variants[variant] ?? variants.primary;
return (
<button
type="button"
style={{ ...baseStyles, ...variantStyles }}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
// Step 2: Build a reusable Card using composition.
function Card({ title, children }) {
console.log(`[Card] Rendering card: ${title}`);
return (
<section
style={{
border: "1px solid #d1d5db",
borderRadius: "10px",
padding: "20px",
marginBottom: "16px",
maxWidth: "420px",
}}
>
<h2 style={{ marginTop: 0 }}>{title}</h2>
{children}
</section>
);
}
// Step 3: Create a focused component for displaying user information.
function UserProfile({ user, onFollow }) {
console.log(`[UserProfile] Rendering ${user.name}`);
return (
<Card title="Developer Profile">
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Role:</strong> {user.role}</p>
<p><strong>Followers:</strong> {user.followers}</p>
<Button onClick={onFollow}>Follow Developer</Button>
</Card>
);
}
// Step 4: Compose the components and keep state in the parent.
function App() {
const [user, setUser] = useState({
name: "Ansh",
role: "JavaScript Developer",
followers: 120,
});
console.log("[App] Rendering application");
// Update only the relevant data while preserving the other fields.
function handleFollow() {
console.log("[App] Follow button clicked");
setUser((currentUser) => ({
...currentUser,
followers: currentUser.followers + 1,
}));
}
function handleReset() {
console.log("[App] Resetting follower count");
setUser((currentUser) => ({
...currentUser,
followers: 120,
}));
}
return (
<main style={{ fontFamily: "Arial, sans-serif", padding: "24px" }}>
<h1>Reusable React Components</h1>
<p>One component API, multiple reusable building blocks.</p>
<UserProfile user={user} onFollow={handleFollow} />
<Card title="Button Variants">
<p>Reuse the same Button component with different appearances.</p>
<Button variant="primary" onClick={() => console.log("Primary clicked")}>
Primary
</Button>
<Button variant="secondary" onClick={() => console.log("Secondary clicked")}>
Secondary
</Button>
<Button variant="danger" onClick={() => console.log("Danger clicked")}>
Danger
</Button>
<Button disabled>Disabled</Button>
</Card>
<Button variant="secondary" onClick={handleReset}>
Reset Followers
</Button>
</main>
);
}
// Step 5: Mount the application in the browser.
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error('Missing root element. Add <div id="root"></div> to index.html.');
}
console.log("[Entry] Mounting React application");
createRoot(rootElement).render(<App />);
console.log("[Entry] React render requested");
Conclusion
Building reusable React components starts with understanding what should be shared and what should remain specific to a particular feature. Components such as buttons, cards, and profile sections become easier to maintain when they have clear responsibilities and receive their data and behavior through props.
Prefer composition over adding dozens of configuration props, use sensible defaults, and keep state in the component that owns the relevant behavior. Also, remember that console logs in render functions may appear more than once during development, especially when React Strict Mode is enabled.
As your application grows, evaluate abstractions based on actual repetition and maintenance needs rather than trying to make every component universally reusable. The goal is not to eliminate every repeated line of code; it is to create a component system that makes your application easier to understand, extend, and test.
What reusable React component has saved you the most development time, and which props or composition patterns made it effective?
Top comments (0)