React Components: What Makes a Good Component?
A common problem I run into on most projects is overly large components. Based on the component name, you can usually guess what it was originally created for. Over years of maintaining large systems, many teams fall into the pitfall of “just one more prop” or “one small tweak” so it can serve another use case as well.
Over time, this creates dirty, confusing, and bloated components. Components that now have three or more distinct blocks of logic, sometimes growing to hundreds or even thousands of lines.
My background started as a hardcore Java developer and architect, and I still live by the philosophies of separation of concerns, testability, and the single responsibility principle. I have found that applying these principles to any language goes a long way toward keeping code clean. If you take the time to honor them in React, they produce components that are strong, focused, and clear in purpose to the next developer who has to work with them.
So what can we do to follow these principles, and why are they so important?
Single Responsibility Principle
How does single responsibility help when building React components? We are usually building pages, so how do you break that down in a way that still feels practical?
To explain this, I will walk through my standard component tree setup.
Container or Page Component
At the top level, I usually refer to this as a page or container, depending on the context I am working in and client naming standards.
The purpose of this component is setup. That typically includes:
- Resolving parameters from routing
- Determining whether data fetching is required
- Fetching the data if needed
Container components render loading states and then delegate rendering to another component that handles the data results. In most hierarchies, I try to fetch data only once. In some cases, a container may also be responsible for setting up a list of child containers.
Composition Component
I like to delegate all resolved data to a composition component. This component is responsible for taking the data and rendering the appropriate subcomponents.
It is also responsible for any state management needed to support user interaction. This component should not be doing any data fetching or triggering loading side effects. Ideally, it should be a pure function of props and state.
The composition component is also responsible for passing callbacks down to child components so they can notify the parent of user interactions.
Building Block Components
These components are the true building blocks of your UI. They should be as “dumb” as possible.
When state is required, manage it as close to the component as you reasonably can. User interactions should be passed in via props. These components should be small, clearly named, and focused on a single purpose.
Props should be specific and well named. These components should also be easy to test. Knowing when to use children is important here, as it allows for more flexibility and composition without increasing complexity.
Example: A Single Responsibility Component
Below is an example of a building block component with a single responsibility. This component is a selector dedicated to choosing a client from a list of clients.
It manages its own internal state and exposes a callback to notify a parent component of changes. In this example, it fetches shared data using TanStack Query, organizes the data for display, and then passes it to a reusable DropDownSelector component.
The key takeaway is that this component has one clear purpose. It is focused, easy to understand, and easy to test.
Separation of Concerns
Separation of concerns can show up in many ways within a React codebase. This might include:
- Moving logic into custom hooks
- Extracting transforms into standalone utility functions
- Keeping UI components focused purely on rendering data
By making these separations, components become easier to reuse and easier to test. Each building block has a narrower responsibility, which makes it faster to understand and safer to modify. Over time, this naturally leads to a more composable and scalable front end architecture.
Applying Separation of Concerns to Components
One way I commonly apply this is with selector components. I assume the selector is standalone.
It can accept a default value and manage its own internal state. In scenarios where changes need to propagate upward, I expose a callback prop. This keeps the selector focused while still allowing the parent to react to changes.
This approach makes the component more reusable across different contexts without coupling it too tightly to a specific page or workflow.
Testability
Testability is a big one for me. When I look at a component and immediately wonder how I would even begin to test it, that is usually a sign of excessive complexity.
Another red flag is when test setup requires more code than the test itself. Overuse of mocks and deep render trees can make Jest tests slow and brittle, especially when components are doing too much.
The goal is to keep tests as isolated as possible. Smaller, focused components are easier to reason about, easier to mock, and significantly faster to test.
Example: Improving Testability
Here is a simple example of moving logic into a standalone utility function to improve testability. By extracting this logic, the component becomes simpler, and the utility can be tested independently without rendering React at all.
Conclusion
The principles that often get the most attention on the backend are simply good software development practices. They apply just as well to front-end frameworks like React.
By applying single responsibility, separation of concerns, and testability to your React components, you can build systems that are easier to understand, easier to maintain, and easier to test. That leads to a better developer experience and, ultimately, a better product.
About the Author
Adam Utsch
Senior Principal Consultant
Adam is a seasoned software professional with deep experience in development, deployment, and application support. With a strong engineering foundation, they specialize in building scalable solutions and mentoring others in the technologies that drive real impact. Adam is passionate about continuous improvement, collaboration, and staying ahead of the tech curve.