In this post, I'd like to share a design pattern that has been a game-changer for me and the teams I've worked ever since the introduction of React hooks.
In the software development world, we often hear about the Single Responsibility Principle (SRP). It's a simple yet powerful idea:
"A class should focus on doing one thing only."
But when we dive into building React components, sticking to this principle isn't always straightforward. Luckily, the approach I'm about to share has helped me apply the SRP effectively in my React projects, and I believe it can do the same for you.
Before Implementing Separation of Concerns
Let's start by examining a straightforward form component:
// MyCustomForm.jsx
function MyCustomForm({ initialValues }) {
const [firstName, setFirstName] = useState(initialValues.firstName);
const [lastName, setLastName] = useState(initialValues.lastName);
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
function handleSubmit() {
console.log("Submitting Values", firstName, lastName);
}
return (
<form onSubmit={handleSubmit}>
<input
onChange={handleFirstNameChange}
placeholder="First name"
value={firstName}
/>
<input
onChange={handleLastNameChange}
placeholder="Last name"
value={lastName}
/>
<button type="submit">
Submit
</button>
</form>
)
}
For those familiar with React, this is a basic form component with two input fields and a submit button, all wrapped up with the necessary state and event handlers. While this script isn't flawed, it does have room for enhancement, which we will explore in the following section.
Implementing Separation of Concerns
To take this component to the next level, we're going to decouple the logic from the presentation elements.
First, we craft a custom hook exclusively for the MyCustomForm
component:
// useMyCustomForm.js
function useMyCustomForm({ initialValues }) {
const [firstName, setFirstName] = useState(initialValues.firstName);
const [lastName, setLastName] = useState(initialValues.lastName);
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
function handleSubmit() {
console.log("Submitting Values", firstName, lastName);
}
return {
firstName,
lastName,
handleFirstNameChange,
handleLastNameChange,
handleSubmit,
}
}
With the hook ready, we reintroduce it into the MyCustomForm
component, maintaining the JSX structure while relocating the logic to our new custom hook:
// MyCustomForm.jsx
function MyCustomForm({ initialValues }) {
const {
firstName,
lastName,
handleFirstNameChange,
handleLastNameChange,
handleSubmit,
} = useMyCustomForm({ initialValues });
return (
<form onSubmit={handleSubmit}>
<input
onChange={handleFirstNameChange}
placeholder="First name"
value={firstName}
/>
<input
onChange={handleLastNameChange}
placeholder="Last name"
value={lastName}
/>
<button type="submit">
Submit
</button>
</form>
)
}
This modification fosters a cleaner separation of concerns, with both files working in tandem yet distinctly holding their own roles. You'll notice the JSX remains untouched; we simply transitioned the logic into its dedicated file.
Benefits
Discover why this approach has become my go-to in various projects:
Easy Maintainability and Readability
From the moment you open the new MyCustomForm.jsx
file, the presentational aspect of the component greets you, streamlining comprehension. Imagine wanting to add labels; there’s no need to sift through the component logic to find the JSX — it's readily accessible. Plus, initiating code upon component mounting is as simple as inserting a useEffect
in the useMyCustomForm
hook.
Simplified Testing
Adopting this strategy simplifies the testing process for both the logic and presentation layers of the component. It allows for straightforward testing of the useMyCustomForm
hook in isolation. Moreover, mocking the values for the useMyCustomForm
hook in your MyCustomForm
tests becomes a breeze. And for holistic integration tests, focus on writing tests for MyCustomForm
without altering the useMyCustomForm
values.
Conclusion
While many associate custom hooks with logic reusable across multiple components, they can also shine in singular, specific applications, as showcased here.
What are your thoughts on this component development strategy? Do you see its utility, or deem it not worth the investment? Share your perspective!