Single responsibility in react

04/02/2023 Wassim Nassour

The single responsibility principle is created by Robert C. Martin , and the main goal and idea behind it is that every class, module, component, and hook …, should have only one responsibility/purpose, and the reason for making a module or component… have single responsibility will help us in a lot of things

  • reduce the number of bugs
  • improve our development experience
  • avoid unexpected bugs
  • writing test will be extremely fast and easy because our code has only one responsibility
  • if we have a component that renders a list of users I see no reason, that component should hold information about users
    as well, when our requirement in a component or hook changes the surface of changes and affected code will be small, opposite if we have conflicted code a component does a lot of things that can lead to some side effects or changes
    writing tests will be easy too, and this principle can help us also when writing TDD cuz you wrote a small component that requires less test, also building our component, and hooks independently, and composing them together, Reuse will become an obvious citizen of your systems.
    I will write an example of a component that doesn’t use this principle and I will apply it to see the difference and how it can help us
    this is our component called Users fetches users from the backend listing all of them and filter
    import axios from 'axios'
    import { useEffect, useMemo, useState } from 'react'
    
    const api = 'https://jsonplaceholder.typicode.com/users'
    
    interface User {
      id: string
      name: string
      username: 'Bret'
      email: string
      address: Object
      phone: string
      website: string
      company: {
        name: string
        catchPhrase: string
        bs: string
      }
    }
    
    export function Users() {
      const [users, setUsers] = useState<User[]>([])
      const [searchKeyword, setSearchKeyword] = useState('')
    
      const fetchProducts = async () => {
        const response = await axios.get(api)
        if (response && response.data) setUsers(response.data)
      }
    
      useEffect(() => {
        fetchProducts()
      }, [])
    
      const filterUsers = useMemo(
        () =>
          users.filter((user: User) => user.name.toLowerCase().includes(searchKeyword?.toLowerCase())),
        [users, searchKeyword]
      )
    
      return (
        <div className="flex flex-col h-full w-full">
          <div className="flex flex-col justify-center items-center">
            <input
              className="border"
              placeholder="search for user"
              onChange={e => setSearchKeyword(e.target.value)}
            />
          </div>
          <div className="h-full flex mt-10  flex-wrap justify-center">
            {filterUsers?.map(user => (
              <div className="max-w-md mr-2 mb-3 p-2 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
                <div className="flex justify-end px-4 pt-4">
                  <div
                    id="dropdown"
                    className="z-10 hidden text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700"
                  >
                    <ul className="py-2" aria-labelledby="dropdownButton">
                      <li>
                        <a
                          href="#"
                          className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
                        >
                          Edit
                        </a>
                      </li>
                      <li>
                        <a
                          href="#"
                          className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
                        >
                          Export Data
                        </a>
                      </li>
                      <li>
                        <a
                          href="#"
                          className="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
                        >
                          Delete
                        </a>
                      </li>
                    </ul>
                  </div>
                </div>
                <div className="flex flex-col items-center pb-10">
                  <img
                    className="w-24 h-24 mb-3 rounded-full shadow-lg"
                    src="https://i.pravatar.cc/300"
                    alt={user?.id}
                  />
                  <h5 className="mb-1 text-xl font-medium text-gray-900 dark:text-white">
                    {user?.name}
                  </h5>
                  <span className="text-sm text-gray-500 dark:text-gray-400">
                    {user?.company?.name}
                  </span>
                  <div className="flex mt-4 space-x-3 md:mt-6">
                    <a
                      href="#"
                      className="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
                    >
                      Add friend
                    </a>
                    <a
                      href="#"
                      className="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-gray-900 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-700"
                    >
                      Message
                    </a>
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      )
    }
    
    our Users Components have more than one responsibility, fetch users, list users, and search for user, let’s use the SRP principle to clean and organize this code based on the responsibilities
    this is our code after using the principle, created a Component for listing users and move our fetching from the backend logic to separate hook, do the same with the search separate search logic from the search component
    import { useState } from 'react'
    import { User } from '../types/user'
    import { useFetchUsers, useSearchUser } from './hooks'
    import { UserCard } from './UserCard'
    import { SearchUserComponent } from './SearchUser'
    
    export function Users() {
      const [searchKeyword, setSearchKeyword] = useState('')
      const { users } = useFetchUsers()
      const { filterUsers } = useSearchUser({ searchKeyword, users })
    
      return (
        <div className="flex flex-col h-full w-full">
          <SearchUserComponent setSearchKeyword={setSearchKeyword} />
          <div className="h-full flex mt-10  flex-wrap justify-center">
            {filterUsers?.map((user: User) => (
              <UserCard user={user} />
            ))}
          </div>
        </div>
      )
    }
    
    useFetchUsers fetch all users
    import axios from 'axios'
    import { useEffect, useState } from 'react'
    
    import { User } from '../../types/user'
    
    const api = 'https://jsonplaceholder.typicode.com/users'
    
    export const useFetchUsers = () => {
      const [users, setUsers] = useState<User[]>([])
    
      const fetchProducts = async () => {
        const response = await axios.get(api)
        if (response && response.data) setUsers(response.data)
      }
    
      useEffect(() => {
        fetchProducts()
      }, [])
    
      return {
        users
      }
    }
    
    useFilterUsers hook for search for specific user
    import { useMemo } from 'react'
    import { User } from '../../types/user'
    
    interface Params {
      users: User[]
      searchKeyword: string
    }
    
    export const useSearchUser = ({ searchKeyword, users }: Params) => {
      const filterUsers = useMemo(
        () =>
          users.filter((user: User) => user.name.toLowerCase().includes(searchKeyword?.toLowerCase())),
        [users, searchKeyword]
      )
    
      return {
        filterUsers
      }
    }
    
    UserCard component display only the user information only
    import { User } from '../../types/user'
    
    interface Props {
      user: User
    }
    
    export const UserCard = ({ user }: Props) => (
      <div className="max-w-md mr-2 mb-3 p-2 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
        <div className="flex justify-end px-4 pt-4">
          <div
            id="dropdown"
            className="z-10 hidden text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700"
          >
            <ul className="py-2" aria-labelledby="dropdownButton">
              <li>
                <a
                  href="#"
                  className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
                >
                  Edit
                </a>
              </li>
              <li>
                <a
                  href="#"
                  className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
                >
                  Export Data
                </a>
              </li>
              <li>
                <a
                  href="#"
                  className="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
                >
                  Delete
                </a>
              </li>
            </ul>
          </div>
        </div>
        <div className="flex flex-col items-center pb-10">
          <img
            className="w-24 h-24 mb-3 rounded-full shadow-lg"
            src="https://i.pravatar.cc/300"
            alt={user?.id}
          />
          <h5 className="mb-1 text-xl font-medium text-gray-900 dark:text-white">{user?.name}</h5>
          <span className="text-sm text-gray-500 dark:text-gray-400">{user?.company?.name}</span>
          <div className="flex mt-4 space-x-3 md:mt-6">
            <a
              href="#"
              className="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
            >
              Add friend
            </a>
            <a
              href="#"
              className="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-gray-900 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-700"
            >
              Message
            </a>
          </div>
        </div>
      </div>
    )
    
    search input component
    interface Props {
      setSearchKeyword: (keyword: string) => void
    }
    
    export const SearchUserComponent = ({ setSearchKeyword }: Props) => (
      <div className="flex flex-col justify-center items-center">
        <input
          className="border"
          placeholder="search for user"
          onChange={e => setSearchKeyword(e.target.value)}
        />
      </div>
    )
    
    we separate the code based on the responsibility , and this as i said before will be very help full in reading code , finding\bugs quickly write test , also writing TDD with this approach will be easy
    Check the code in the sandbox

    Created by @Wassim built with @NextJs deployed in @Vercel