Published on

Advanced React Patterns and Performance Optimization

Advanced React Patterns and Performance Optimization

In the ever-evolving landscape of React development, mastering advanced patterns and performance optimization techniques is crucial for building scalable, maintainable applications. This comprehensive guide explores cutting-edge React patterns and optimization strategies that will elevate your development skills to the next level. 🚀

Table of Contents

Higher-Order Components (HOCs)

Higher-Order Components are a powerful pattern for reusing component logic. They're functions that take a component and return a new component with additional functionality.

Basic HOC Implementation

// withLoading HOC
const withLoading = (WrappedComponent) => {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div className="loading-spinner">Loading...</div>
    }
    return <WrappedComponent {...props} />
  }
}

// Usage
const UserProfile = ({ user }) => (
  <div>
    <h1>{user.name}</h1>
    <p>{user.email}</p>
  </div>
)

const UserProfileWithLoading = withLoading(UserProfile)

// In your component
function App() {
  const [user, setUser] = useState(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    fetchUser().then((userData) => {
      setUser(userData)
      setIsLoading(false)
    })
  }, [])

  return <UserProfileWithLoading isLoading={isLoading} user={user} />
}

Advanced HOC with Error Boundaries

const withErrorBoundary = (WrappedComponent) => {
  return class extends React.Component {
    constructor(props) {
      super(props)
      this.state = { hasError: false, error: null }
    }

    static getDerivedStateFromError(error) {
      return { hasError: true, error }
    }

    componentDidCatch(error, errorInfo) {
      console.error('Error caught by HOC:', error, errorInfo)
      // Log to error reporting service
    }

    render() {
      if (this.state.hasError) {
        return (
          <div className="error-fallback">
            <h2>Something went wrong</h2>
            <button onClick={() => this.setState({ hasError: false, error: null })}>
              Try again
            </button>
          </div>
        )
      }

      return <WrappedComponent {...this.props} />
    }
  }
}

Render Props Pattern

The render props pattern allows components to share code using a prop whose value is a function.

// Data fetcher component using render props
class DataFetcher extends React.Component {
  constructor(props) {
    super(props)
    this.state = { data: null, loading: true, error: null }
  }

  async componentDidMount() {
    try {
      const response = await fetch(this.props.url)
      const data = await response.json()
      this.setState({ data, loading: false })
    } catch (error) {
      this.setState({ error, loading: false })
    }
  }

  render() {
    return this.props.render(this.state)
  }
}

// Usage
function UserList() {
  return (
    <DataFetcher
      url="/api/users"
      render={({ data, loading, error }) => {
        if (loading) return <div>Loading users...</div>
        if (error) return <div>Error: {error.message}</div>
        return (
          <ul>
            {data.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        )
      }}
    />
  )
}

Modern Render Props with Hooks

// Custom hook replacing render props
function useDataFetcher(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null })

  useEffect(() => {
    let isMounted = true

    const fetchData = async () => {
      try {
        const response = await fetch(url)
        const data = await response.json()
        if (isMounted) {
          setState({ data, loading: false, error: null })
        }
      } catch (error) {
        if (isMounted) {
          setState({ data: null, loading: false, error })
        }
      }
    }

    fetchData()

    return () => {
      isMounted = false
    }
  }, [url])

  return state
}

// Usage
function UserList() {
  const { data, loading, error } = useDataFetcher('/api/users')

  if (loading) return <div>Loading users...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Compound Components

Compound components work together to form a complete UI component, providing flexibility and reusability.

// Tabs compound component
const TabsContext = React.createContext()

function Tabs({ children, defaultActiveKey }) {
  const [activeKey, setActiveKey] = useState(defaultActiveKey)

  return (
    <TabsContext.Provider value={{ activeKey, setActiveKey }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  )
}

function TabList({ children }) {
  return <div className="tab-list">{children}</div>
}

function Tab({ eventKey, children }) {
  const { activeKey, setActiveKey } = useContext(TabsContext)
  const isActive = activeKey === eventKey

  return (
    <button className={`tab ${isActive ? 'active' : ''}`} onClick={() => setActiveKey(eventKey)}>
      {children}
    </button>
  )
}

function TabPanels({ children }) {
  return <div className="tab-panels">{children}</div>
}

function TabPanel({ eventKey, children }) {
  const { activeKey } = useContext(TabsContext)

  if (activeKey !== eventKey) return null

  return <div className="tab-panel">{children}</div>
}

// Compound component assignment
Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panels = TabPanels
Tabs.Panel = TabPanel

// Usage
function App() {
  return (
    <Tabs defaultActiveKey="tab1">
      <Tabs.List>
        <Tabs.Tab eventKey="tab1">Tab 1</Tabs.Tab>
        <Tabs.Tab eventKey="tab2">Tab 2</Tabs.Tab>
        <Tabs.Tab eventKey="tab3">Tab 3</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panels>
        <Tabs.Panel eventKey="tab1">Content for Tab 1</Tabs.Panel>
        <Tabs.Panel eventKey="tab2">Content for Tab 2</Tabs.Panel>
        <Tabs.Panel eventKey="tab3">Content for Tab 3</Tabs.Panel>
      </Tabs.Panels>
    </Tabs>
  )
}

Custom Hooks Patterns

Advanced State Management Hook

// useReducerWithMiddleware hook
function useReducerWithMiddleware(reducer, initialState, middleware = []) {
  const [state, dispatch] = useReducer(reducer, initialState)

  const enhancedDispatch = useCallback(
    (action) => {
      // Apply middleware
      const chain = middleware.map((mw) => mw(state))
      const composedDispatch = chain.reduce((acc, mw) => mw(acc), dispatch)
      return composedDispatch(action)
    },
    [middleware, state]
  )

  return [state, enhancedDispatch]
}

// Logging middleware
const loggingMiddleware = (state) => (next) => (action) => {
  console.log('Previous state:', state)
  console.log('Action:', action)
  const result = next(action)
  console.log('Next state:', state)
  return result
}

// Usage
function Counter() {
  const [state, dispatch] = useReducerWithMiddleware(counterReducer, { count: 0 }, [
    loggingMiddleware,
  ])

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
    </div>
  )
}

Async State Management Hook

// useAsyncState hook
function useAsyncState(asyncFunction, dependencies = []) {
  const [state, setState] = useState({
    data: null,
    loading: false,
    error: null,
  })

  const execute = useCallback(async (...args) => {
    setState({ data: null, loading: true, error: null })
    try {
      const result = await asyncFunction(...args)
      setState({ data: result, loading: false, error: null })
      return result
    } catch (error) {
      setState({ data: null, loading: false, error })
      throw error
    }
  }, dependencies)

  return { ...state, execute }
}

// Usage
function UserProfile({ userId }) {
  const {
    data: user,
    loading,
    error,
    execute,
  } = useAsyncState(async (id) => {
    const response = await fetch(`/api/users/${id}`)
    return response.json()
  }, [])

  useEffect(() => {
    if (userId) {
      execute(userId)
    }
  }, [userId, execute])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  if (!user) return <div>No user found</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

Performance Optimization Techniques

React.memo and useMemo

// Expensive component that should be memoized
const ExpensiveComponent = React.memo(
  ({ data, onUpdate }) => {
    const processedData = useMemo(() => {
      // Expensive computation
      return data.map((item) => ({
        ...item,
        processed: heavyComputation(item),
      }))
    }, [data])

    return (
      <div>
        {processedData.map((item) => (
          <div key={item.id} onClick={() => onUpdate(item.id)}>
            {item.name}: {item.processed}
          </div>
        ))}
      </div>
    )
  },
  (prevProps, nextProps) => {
    // Custom comparison function
    return (
      prevProps.data.length === nextProps.data.length &&
      prevProps.data.every((item, index) => item.id === nextProps.data[index].id)
    )
  }
)

useCallback for Event Handlers

function TodoList({ todos, onToggle, onDelete }) {
  // Memoize callbacks to prevent unnecessary re-renders
  const handleToggle = useCallback(
    (id) => {
      onToggle(id)
    },
    [onToggle]
  )

  const handleDelete = useCallback(
    (id) => {
      onDelete(id)
    },
    [onDelete]
  )

  return (
    <div>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} onDelete={handleDelete} />
      ))}
    </div>
  )
}

const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  return (
    <div>
      <span onClick={() => onToggle(todo.id)}>
        {todo.completed ? '✓' : '○'} {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  )
})

Virtual Scrolling

// Simple virtual scrolling implementation
function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0)

  const visibleStart = Math.floor(scrollTop / itemHeight)
  const visibleEnd = Math.min(
    visibleStart + Math.ceil(containerHeight / itemHeight),
    items.length - 1
  )

  const visibleItems = items.slice(visibleStart, visibleEnd + 1)
  const offsetY = visibleStart * itemHeight

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div key={visibleStart + index} style={{ height: itemHeight }}>
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

Advanced State Management

Context with Reducer Pattern

// Advanced context pattern with reducer
const AppStateContext = createContext()
const AppDispatchContext = createContext()

function appReducer(state, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload }
    case 'SET_THEME':
      return { ...state, theme: action.payload }
    case 'ADD_NOTIFICATION':
      return {
        ...state,
        notifications: [...state.notifications, action.payload],
      }
    case 'REMOVE_NOTIFICATION':
      return {
        ...state,
        notifications: state.notifications.filter((n) => n.id !== action.payload),
      }
    default:
      throw new Error(`Unhandled action type: ${action.type}`)
  }
}

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, {
    user: null,
    theme: 'light',
    notifications: [],
  })

  return (
    <AppStateContext.Provider value={state}>
      <AppDispatchContext.Provider value={dispatch}>{children}</AppDispatchContext.Provider>
    </AppStateContext.Provider>
  )
}

// Custom hooks for consuming context
function useAppState() {
  const context = useContext(AppStateContext)
  if (!context) {
    throw new Error('useAppState must be used within AppProvider')
  }
  return context
}

function useAppDispatch() {
  const context = useContext(AppDispatchContext)
  if (!context) {
    throw new Error('useAppDispatch must be used within AppProvider')
  }
  return context
}

Best Practices and Anti-patterns

✅ Best Practices

  1. Use TypeScript for better type safety
  2. Implement proper error boundaries
  3. Optimize bundle size with code splitting
  4. Use React DevTools Profiler for performance analysis
  5. Implement proper accessibility (a11y)

❌ Anti-patterns to Avoid

  1. Mutating state directly
  2. Using array indices as keys
  3. Not cleaning up effects
  4. Over-using useEffect
  5. Prop drilling without context

Code Splitting Example

// Lazy loading with Suspense
const LazyComponent = React.lazy(() => import('./LazyComponent'))

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  )
}

Conclusion

Mastering these advanced React patterns and optimization techniques will significantly improve your application's performance, maintainability, and user experience. Remember to:

  • Choose the right pattern for your specific use case
  • Profile your application to identify performance bottlenecks
  • Keep components small and focused
  • Use TypeScript for better development experience
  • Always consider accessibility in your implementations

By implementing these patterns thoughtfully, you'll build React applications that are not only performant but also maintainable and scalable. Happy coding! 🚀