Dwarves
Memo
Type ESC to close search bar

Hook architecture in react

Hooks architecture in React refers to the systematic approach of using hooks to manage state, side effects, and reusable logic across components. Custom hooks are one of the most powerful features, allowing you to encapsulate and reuse complex logic independently of component structure. Custom hooks improve code readability, keep components lean, and make stateful logic portable and composable.

Key concepts in hooks architecture

Separation of concerns with custom hooks

By creating custom hooks, we can isolate specific pieces of logic or state, making components simpler and easier to test. Custom hooks follow the same naming conventions and usage patterns as built-in hooks but encapsulate domain-specific or app-specific logic.

Example: useFetch hook for data fetching

import { useState, useEffect } from 'react'

function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    setLoading(true)
    fetch(url)
      .then((response) => response.json())
      .then((data) => setData(data))
      .catch((error) => setError(error))
      .finally(() => setLoading(false))
  }, [url])

  return { data, loading, error }
}

Usage:

function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`)

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error loading data.</p>

  return <div>{data.name}</div>
}

Benefits:

Encapsulating side effects

Side effects (like fetching data, managing subscriptions, or setting timeouts) often clutter component code. By moving these side effects into custom hooks, we can encapsulate the logic and improve component readability.

Example: useDocumentTitle hook for updating the document title

import { useEffect } from 'react'

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title
  }, [title])
}

Usage:

function HomePage() {
  useDocumentTitle('Home - My App')
  return <div>Welcome to the Home Page</div>
}

Benefits:

Dependency management in hooks

Custom hooks require careful handling of dependencies to avoid bugs, stale data, or unintended behaviors. useEffect, useMemo, and useCallback hooks depend on stable dependencies to function predictably.

Example: Managing dependencies with useMemo

Suppose we need to calculate an expensive value in a hook, which depends on certain props or state. Using useMemo ensures the computation only runs when necessary.

import { useMemo } from 'react'

function useExpensiveCalculation(data) {
  const result = useMemo(() => {
    // Expensive calculation here
    return data.reduce((sum, num) => sum + num, 0)
  }, [data])

  return result
}

Usage:

function Stats({ numbers }) {
  const total = useExpensiveCalculation(numbers)
  return <div>Total: {total}</div>
}

Benefits:

In the future, this step will be handled automatically by React compiler

Combining multiple custom hooks

For more complex scenarios, multiple custom hooks can be combined, keeping components modular and avoiding deeply nested hooks. You can chain hooks to build up increasingly complex functionality without cluttering a single hook.

Example: Using useAuth and useFetch together

// useAuth.js import { useState } from "react";

function useAuth() {
  const [user, setUser] = useState(null)

  const login = (userData) => setUser(userData)
  const logout = () => setUser(null)

  return { user, login, logout }
}

// useUserData.js import useFetch from "./useFetch";

function useUserData(userId) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`)
  return { data, loading, error }
}

// Usage in a component

function Dashboard({ userId }) {
  const { user, login, logout } = useAuth()
  const { data: userData, loading } = useUserData(userId)

  return (
    <div>
      {user ? (
        <div>
          <button onClick={logout}>Logout</button>
          {loading ? <p>Loading user data...</p> : <p>Welcome, {userData.name}</p>}
        </div>
      ) : (
        <button onClick={() => login({ id: userId, name: 'John Doe' })}>Login</button>
      )}
    </div>
  )
}

Benefits:

Best practices for custom hooks

Use clear naming conventions: Name hooks descriptively, starting with use, such as useAuth, useFetchData, or useToggle. This helps with readability and code consistency.

Return only necessary data and functions: Custom hooks should return only the data and functions the component actually needs. This minimizes the hook’s API surface and reduces complexity.

// Better: return only what's needed
function useToggle(initialState = false) {
  const [state, setState] = useState(initialState)
  const toggle = () => setState((prev) => !prev)
  return [state, toggle]
}

Handle edge cases and errors gracefully: Build error handling directly into hooks, where applicable. This keeps components from dealing with low-level error handling, focusing only on displaying relevant information to the user.

function useFetch(url) {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetch(url)
      .then((response) => response.json())
      .then(setData)
      .catch(setError)
  }, [url])

  return { data, error }
}

Encapsulate complex state logic: If you find yourself managing complex state logic (e.g., multiple variables, resetting state), consider using useReducer within the hook.

import { useReducer } from 'react'

function formReducer(state, action) {
  switch (action.type) {
    case 'update':
      return { ...state, [action.field]: action.value }
    case 'reset':
      return action.initialState
    default:
      return state
  }
}

function useForm(initialState) {
  const [state, dispatch] = useReducer(formReducer, initialState)

  const updateField = (field, value) => dispatch({ type: 'update', field, value })
  const resetForm = () => dispatch({ type: 'reset', initialState })

  return [state, updateField, resetForm]
}

Testing custom hooks: Test custom hooks in isolation to ensure they behave as expected under various scenarios. Tools like React testing library’s renderHook make it easy to test hooks directly.

import { renderHook, act } from '@testing-library/react-hooks'
import useToggle from './useToggle'

test('should toggle state', () => {
  const { result } = renderHook(() => useToggle())

  act(() => {
    result.current[1]() // Call toggle function
  })

  expect(result.current[0]).toBe(true) // Assert the toggled state
})

Combining techniques in a custom hook system

Imagine you need a custom hook system for managing user authentication, including login, logout, fetching user data, and handling user permissions. We’ll create modular hooks that interact but remain individually reusable.

  1. useAuth for authentication: Manages login and logout functions and holds user session data.
  2. useUserData for data fetching: Fetches user-specific data from the server.
  3. usePermissions for role-based access: Checks permissions based on the user’s roles.

Combining custom hooks:

// useAuth.js
function useAuth() {
  const [user, setUser] = useState(null)

  const login = (userData) => setUser(userData)
  const logout = () => setUser(null)

  return { user, login, logout }
}

// useUserData.js
import useFetch from './useFetch'

function useUserData(userId) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`)
  return { data, loading, error }
}

// usePermissions.js
function usePermissions(userRoles = []) {
  const hasPermission = (permission) => userRoles.includes(permission)
  return { hasPermission }
}

// Usage in a component
function AdminDashboard({ userId }) {
  const { user, login, logout } = useAuth()
  const { data: userData, loading: dataLoading } = useUserData(userId)
  const { hasPermission } = usePermissions(userData ? userData.roles : [])

  return (
    <div>
      {user ? (
        <div>
          <button onClick={logout}>Logout</button>
          {dataLoading ? <p>Loading user data...</p> : hasPermission('admin') ? <p>Welcome, Admin {userData.name}</p> : <p>Access denied</p>}
        </div>
      ) : (
        <button onClick={() => login({ id: userId, name: 'Jane Doe' })}>Login</button>
      )}
    </div>
  )
}

By modularizing each piece of the authentication system into separate custom hooks, we ensure that each hook is individually testable, reusable, and manageable. This approach keeps the AdminDashboard component focused on rendering, with minimal logic.

Summary

Custom hooks provide a powerful way to architect stateful and reusable logic in React applications. By following best practices and focusing on modularity, you can create hooks that are easy to test, maintain, and scale across complex applications. The approach to combining, organizing, and testing these hooks leads to clean, efficient, and high-quality code.