How To Main Separation of Concerns in React

Barry Michael Doyle - Sep 17 '23 - - Dev Community

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . .