At some point in every frontend project, this happens:
You build a button.
Then another one.
Then another… slightly different one.
Now you’ve got:
PrimaryButtonSecondaryButtonBigButtonBlueButtonSubmitButton
And suddenly your codebase feels messy.
The idea of reusable components sounds simple, but in practice, a lot of developers either:
- don’t reuse enough, or
- over-engineer everything trying to make it reusable
Let’s break this down in a practical, no-BS way.
Table of Contents
- The Problem Every Frontend Developer Knows
- What Reusable Actually Means
- Step 1: Start with Props
- Step 2: Add Variants Instead of New Components
- Step 3: Do Not Try to Make It Perfect on Day One
- Step 4: Keep Components Small
- Step 5: Use Composition When Props Get Messy
- Step 6: Separate Logic from UI
- Step 7: Move Shared Components to One Place
- Step 8: Make Components Predictable
- Step 9: Type Your Components with TypeScript
- Step 10: Build Accessibility In from Day One
- Step 11: Do Not Over-Abstract
- A Solid Practical Button Example
- Common Mistakes
- Quick Checklist
- My Thoughts
The Problem Every Frontend Developer Knows
At some point in every frontend project, this happens:
You build a button. Then another one. Then another... slightly different one.
Now you have got:
PrimaryButtonSecondaryButtonBigButtonBlueButtonSubmitButton
And suddenly your codebase feels like a junk drawer.
The idea of reusable components sounds simple, but in practice, a lot of developers either do not reuse enough, or over-engineer everything trying to make it reusable.
Let us break this down in a practical, no-BS way.
What Reusable Actually Means
A reusable component is not just something you can copy-paste.
A truly reusable component is portable, predictable, and purposeful. Portable means it can be used in multiple places and projects without deep rewrites. Predictable means it behaves the same way given the same inputs. Purposeful means it solves a specific UI/UX need without dragging along unrelated behaviors.
A simple test: if teammates can drop your component into a new page, configure it with a few props, and it "just works," you are on the right track.
Here is a component that does not pass that test.
Not Really Reusable
function Button() {
return <button className="bg-blue-500 text-white px-4 py-2">Submit</button>;
}
Looks fine... until you need:
- a different label
- a different color
- a disabled state
Now you are either editing this everywhere or creating new versions.
Step 1: Start with Props
The easiest way to make something reusable is to stop hardcoding values.
function Button({ children, onClick }) {
return (
<button className="bg-blue-500 text-white px-4 py-2" onClick={onClick}>
{children}
</button>
);
}
Now you can do:
<Button onClick={handleSave}>Save</Button>
<Button onClick={handleDelete}>Delete</Button>
Already better. Props are the lifeblood of flexible and reusable components. They allow you to pass data and callbacks, tailoring components to meet specific needs.
When working with props, use clear, descriptive names to make props self-explanatory. Set default values for props to handle cases where they are not provided. And leverage type checking with tools like PropTypes or TypeScript to catch errors early.
Step 2: Add Variants Instead of New Components
Instead of creating multiple button files, just support variations.
function Button({ children, variant = "primary", onClick }) {
const styles = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
danger: "bg-red-500 text-white",
};
return (
<button className={`px-4 py-2 rounded ${styles[variant]}`} onClick={onClick}>
{children}
</button>
);
}
Usage:
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
This is where things start to feel clean. One of React's most powerful features is composition. Instead of creating monolithic components with dozens of props to handle every possible variation, design small components that can be nested and combined. The children prop is your best friend here.
Step 3: Do Not Try to Make It Perfect on Day One
This is where most people go wrong.
They try to build the perfect reusable component upfront. That usually leads to:
- too many props
- confusing logic
- hard-to-read code
A better approach:
- Build it for one use case
- Notice repetition
- Then refactor
To create a reusable component in React, you first identify a repeated UI element in your application. Then, you abstract its structure and logic into a new, self-contained component file. You make it flexible by using props to pass in dynamic data and callback functions, and you use props.children to allow for content composition. Finally, you document its usage and add it to your shared library.
Reusability should come from real needs, not assumptions.
Step 4: Keep Components Small
If your component is doing too much, it is not reusable.
The Single Responsibility Principle is the easiest one to feel in React. Robert C. Martin phrased it well: "A class should have one, and only one, reason to change." If a component fetches data, manages state, transforms that data, and renders UI, it is doing too much. Splitting those responsibilities makes the component easier to reuse, test, and extend.
Bad example:
function UserCard() {
// fetch data
// render UI
// handle modal
// manage form
}
This is hard to reuse and harder to maintain.
Better approach:
-
UserCard-- just UI -
useUser-- logic hook -
UserModal-- modal behavior
Break things down. Smaller pieces = more reuse.
Step 5: Use Composition When Props Get Messy
Sometimes you will feel like your component needs 10+ props. That is a sign to stop.
Instead of this:
<Button icon="save" iconPosition="left" loading size="large" />
Try composition:
<Button size="lg">
<SaveIcon />
Save
</Button>
For instance, instead of a CardWithHeaderAndFooter component, create a generic Card component that accepts other components as children: <Card><CardHeader /><CardBody /><CardFooter /></Card>.
This approach is:
- easier to read
- more flexible
- easier to extend
Step 6: Separate Logic from UI
Do not mix data fetching or heavy logic inside UI components.
Reusable components are pure and predictable pieces of your UI. These components are free from complex business logic and are generic in nature. This means when you give them the same data, they will always display the same consistent user interface. For a React component to be truly reusable, it should avoid side effects like data fetching, reading from localStorage, or making API calls.
Bad:
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users").then((res) => res.json()).then(setUsers);
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Better:
function useUsers() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users").then((res) => res.json()).then(setUsers);
}, []);
return users;
}
function UserList({ users }) {
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Usage
function UsersPage() {
const users = useUsers();
return <UserList users={users} />;
}
Now:
- your UI is clean and testable
- custom hooks are the preferred way to extract and reuse stateful logic without being tied to a specific component hierarchy. This makes both your UI components and your business logic independently reusable.
Step 7: Move Shared Components to One Place
As your app grows, you will notice some components are used everywhere: buttons, inputs, modals, cards.
Organize them with a co-located structure:
src/
components/
shared/
Button/
Button.tsx
Button.types.ts
Button.test.tsx
Input/
Input.tsx
Input.types.ts
Input.test.tsx
Modal/
Modal.tsx
Modal.types.ts
Modal.test.tsx
Keeping all related files in one place leads to easier feature development and clear component boundaries.
Simple rule: if you use it in more than one place, it probably belongs in a shared directory.
Step 8: Make Components Predictable
If someone uses your component, they should know what to expect.
<Button disabled>Click</Button>
This should:
- actually disable the button
- look disabled (visual feedback)
- not trigger click handlers
- be announced correctly by screen readers
Sounds obvious, but consistency matters enormously in bigger applications. Your components should be easy for others (and your future self) to read, use, and extend.
Step 9: Type Your Components with TypeScript
In 2026, this is no longer optional. TypeScript has been getting adopted more and more within React projects by professional developers, with adoption rates recently hitting 78% (State of JS 2025). Type safety is the baseline. TypeScript is no longer optional in professional frontend environments. It increases refactoring confidence, clarifies intent, and becomes even more valuable in distributed, edge-driven systems.
In 2026, building a component without TypeScript is like flying blind. By defining explicit types or interfaces for your component's props, you create a contract that every usage of the component must satisfy. This means fewer surprises at runtime -- the compiler will catch, for example, if a required prop is missing or if you pass a string where a number is expected.
Here is what a typed Button component looks like:
interface ButtonProps {
children: React.ReactNode;
variant?: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
disabled?: boolean;
onClick?: () => void;
}
function Button({
children,
variant = "primary",
size = "md",
disabled = false,
onClick,
}: ButtonProps) {
// component logic
}
Types also act as built-in documentation: when someone imports your component, their IDE can instantly show what props it expects, what types those props should be, and maybe even descriptions if you have included JSDoc comments.
Practical rules:
- Avoid using the
anytype, and be as specific as possible with your types. - Use
interfacefor component props so consumers can extend them - Use utility types such as
Partial,Readonly, andPickto map and manipulate your component prop types.
Step 10: Build Accessibility In from Day One
This is a non-negotiable in 2026. The European Accessibility Act is now enforced. The ADA Title II deadline hits in April 2026. Lawsuits are up 37% year over year. If you are a frontend developer in 2026, accessibility is no longer a nice-to-have skill -- it is a legal requirement.
On 05 January 2026 the W3C published an Editor's Draft of WCAG 3.0. The draft moves beyond the page-centric WCAG 2.x model: it explicitly targets modern web applications, interactive components, media/VR, authoring tools and testing tools. WCAG 3.0 is written to cover single-page apps, component libraries and interactive widgets -- not just static documents. Teams building React component systems must expect accessibility expectations to apply at the component and interaction level, not only at full page checkpoints.
What does that mean in practice? Build accessibility-first component libraries by creating reusable components with proper ARIA roles and semantic HTML from the start, ensuring inclusive experiences for all users.
For a Button, at minimum:
<button
type="button"
disabled={disabled}
aria-disabled={disabled}
onClick={!disabled ? onClick : undefined}
>
{children}
</button>
Key principles:
- Use semantic HTML. It is the single most impactful thing you can do for accessibility, costs nothing, and requires no special library.
- Prefer semantic elements; expose ARIA only when native semantics are insufficient; provide explicit props for label/name when necessary.
- Ensure keyboard navigation works (Tab, Enter, Escape)
- Maintain proper color contrast ratios (4.5:1 minimum for normal text)
- Implement a multi-layered testing strategy with tools like
jest-axe,@axe-core/react, andeslint-plugin-jsx-a11y.
Step 11: Do Not Over-Abstract
Not everything needs to be reusable.
If a component is used only once, keep it simple. Trying to make everything reusable slows you down and adds unnecessary complexity.
Design patterns are descriptive, not prescriptive. They can guide you when facing a problem other developers have encountered many times before, but are not a blunt tool for jamming into every scenario.
Focus on reuse where it actually helps.
A Solid Practical Button Example
Here is a version that brings together everything discussed above -- variants, sizes, TypeScript, accessibility, and composition support:
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
}
function Button({
children,
variant = "primary",
size = "md",
disabled = false,
className = "",
...rest
}: ButtonProps) {
const base = "rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variants: Record<string, string> = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
};
const sizes: Record<string, string> = {
sm: "px-2 py-1 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
return (
<button
className={`${base} ${variants[variant]} ${sizes[size]} ${
disabled ? "opacity-50 cursor-not-allowed" : ""
} ${className}`}
disabled={disabled}
aria-disabled={disabled}
{...rest}
>
{children}
</button>
);
}
It is not over-engineered. But it is reusable, type-safe, accessible, and extensible through ...rest and className -- ready for real projects.
Common Mistakes
| Mistake | Why It Hurts |
|---|---|
| Hardcoding values | Kills reuse immediately |
| Too many props | Creates a confusing component API |
| Mixing logic and UI | Makes components hard to test and maintain |
| Skipping TypeScript | No safety net for refactoring or collaboration |
| Ignoring accessibility | Legal risk and poor user experience |
| Overthinking everything | Slows development without adding real value |
Quick Checklist
- [ ] Is it flexible through props?
- [ ] Is it doing only one thing?
- [ ] Is it simple to use?
- [ ] Can I reuse it without modifying it?
- [ ] Is it typed with TypeScript?
- [ ] Is it accessible by default?
- [ ] Is it tested?
My Thoughts
Reusable components are not about being clever. They are about making your code easier to reuse, easier to understand, and easier to maintain.
Building reusable React components is both an art and a science. The common thread is thinking beyond the immediate task of "getting it to work." It is about creating components that are cleanly designed, easy to maintain, and a joy to use in the long run.
The frontend landscape in 2026 demands more than just working code. Design systems and shared component libraries help teams ship consistent, accessible UI faster. They streamline cross-functional collaboration and reduce one-off styling or duplicated patterns. Components with accessible defaults and well-documented tokens provide durable building blocks that adapt to multiple frontend stacks.
If you remember one thing:
Do not try to build reusable components. Build simple components -- then make them reusable when needed.
Keep it simple, type it properly, make it accessible, and evolve as your app grows. That is really it.
Further Reading:
Top comments (0)