Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
ReactJavaScriptDesign PatternsHigher-Order FunctionsHigher-Order Components

Higher-Order Functions & Higher-Order Components

From JavaScript higher-order functions to React HOCs — map/filter/compose foundations, component factories, with-prefix conventions, withDataFetching, admin permissions, pitfalls, and custom hook alternatives.

Jul 4, 202615 min read

Introduction

A higher-order function (HOF) takes a function as input and/or returns a new function. A higher-order component (HOC) applies the same idea to React — it takes a component and returns an enhanced component. HOCs only make sense once you understand HOFs: both are factories that wrap behavior without rewriting the original.

This guide covers:

  1. Higher-order functions — JavaScript foundation (map, compose, curry)
  2. From HOF to HOC — how the pattern maps to React
  3. HOC core concepts — component factory, input/output shape
  4. Purpose, implementation, and use cases
  5. Movie app & admin dashboard examples
  6. Pitfalls and modern hook alternatives

Note

HOCs were the primary reuse pattern in React 15–16 (Redux connect, React Router v4). Custom hooks replaced most app-level HOCs in 2019+, but HOF thinking still underpins hooks, middleware, and functional utilities everywhere in JavaScript.

Quick index

#Section
1Higher-order functions (HOF)
2From HOF to HOC
3HOC core concepts
4Purpose & benefits
5Implementation details
6Common use cases
7Movie app: withDataFetching
8Practical task: admin dashboard
9Pitfalls & limitations
10Modern alternatives

1. Higher-order functions (HOF)

A higher-order function is a function that operates on other functions — by taking them as arguments, returning them, or both.

plaintext
HOF(fn) → newFn
TraitMeaning
Takes a function as inputmap, filter, addLogging(handler)
Returns a function() => …, curried factories
Abstracts shared behaviorOne wrapper, many call sites
Enables compositionChain small functions into pipelines

Built-in HOFs you already use

js
const nums = [1, 2, 3, 4]
 
// map — HOF: takes a transform function
const doubled = nums.map((n) => n * 2) // [2, 4, 6, 8]
 
// filter — HOF: takes a predicate function
const evens = nums.filter((n) => n % 2 === 0) // [2, 4]
 
// reduce — HOF: takes a reducer function
const sum = nums.reduce((acc, n) => acc + n, 0) // 10

Custom HOF — wrap any function with logging

js
function withLogging(fn) {
  return function (...args) {
    console.log('Calling with', args)
    const result = fn(...args)
    console.log('Returned', result)
    return result
  }
}
 
function add(a, b) {
  return a + b
}
 
const loggedAdd = withLogging(add)
loggedAdd(2, 3) // logs args + result, returns 5

Currying — function that returns a function

js
function multiply(a) {
  return function (b) {
    return a * b
  }
}
 
const double = multiply(2)
const triple = multiply(3)
 
double(5) // 10
triple(5) // 15

Composition — pipe HOFs together

js
const compose =
  (...fns) =>
  (value) =>
    fns.reduceRight((acc, fn) => fn(acc), value)
 
const trim = (s) => s.trim()
const toLower = (s) => s.toLowerCase()
const removeSpaces = (s) => s.replace(/\s+/g, '-')
 
const slugify = compose(removeSpaces, toLower, trim)
slugify('  Hello World  ') // "hello-world"

Tip

If you can write withLogging(fn) in plain JavaScript, you already understand the mechanics of withAuth(Component) in React — the wrapped unit changes from function to component.

Back to index


2. From HOF to HOC

React components are functions (or classes) that return UI. An HOC applies HOF logic to those functions.

HOF (JavaScript)HOC (React)
withLogging(fn)withAuth(Component)
Input: functionInput: component
Output: enhanced functionOutput: enhanced component
Wraps call behaviorWraps render behavior
Currying: (config) => (fn) => newFn(url) => (Component) => Enhanced
plaintext
// HOF mental model
const enhanced = withBehavior(original)
 
// HOC mental model
const EnhancedComponent = withBehavior(OriginalComponent)
js
// HOF — enhance a function
function withTimestamp(fn) {
  return (...args) => {
    console.log(Date.now())
    return fn(...args)
  }
}
 
// HOC — same shape, different input type
function withTimestamp(Component) {
  return function WithTimestamp(props) {
    console.log(Date.now())
    return <Component {...props} />
  }
}

Note

In React, components are first-class values — you can pass them to functions, return them, and store them in variables, exactly like callbacks in HOF patterns.

Back to index


3. HOC core concepts

An HOC is not a component — it is a function that returns a component.

plaintext
withEnhancement(WrappedComponent) → EnhancedComponent
ConceptMeaning
HOF basisFunctions operating on functions — HOCs operate on components
Component factoryProduces new component types from existing ones
Takes component as inputWrapped: ComponentType<P>
Returns enhanced componentAdds props, guards, or side effects before rendering Wrapped
Function that returns a functionOften curried: (url) => (Wrapped) => Enhanced
tsx
import type { ComponentType } from 'react'
 
// Simplest HOC — inject a static prop
function withGreeting<P extends object>(Wrapped: ComponentType<P & { greeting: string }>) {
  return function WithGreeting(props: P) {
    return <Wrapped {...props} greeting="Hello from HOC" />
  }
}
 
function Title({ greeting, name }: { greeting: string; name: string }) {
  return (
    <h1>
      {greeting}, {name}
    </h1>
  )
}
 
export const EnhancedTitle = withGreeting(Title)
// Renders: "Hello from HOC, Kazi" when passed name="Kazi"

Note

HOCs do not modify the original component — they wrap it in a new function component that delegates to Wrapped with extra behavior or props.

Back to index


4. Purpose & benefits

BenefitWhat it means
Sharing logic across componentsOne withAuth protects every route component
Reusability (DRY)Write fetch/loading once, wrap many pages
Centralizing common scenariosSpinner, error boundary, analytics in one place
Abstracting cross-cutting concernsAuth, logging, i18n — not mixed into feature JSX

Before — duplicated auth checks

tsx
function ReportsPage() {
  const session = getSession()
  if (!session) return <p>Sign in required</p>
  return <Reports />
}
 
function SettingsPage() {
  const session = getSession()
  if (!session) return <p>Sign in required</p>
  return <Settings />
}

After — HOC centralizes the guard

tsx
function withAuth<P extends object>(Wrapped: ComponentType<P>) {
  return function Authenticated(props: P) {
    const session = getSession()
    if (!session) return <p>Sign in required</p>
    return <Wrapped {...props} />
  }
}
 
export const ProtectedReports = withAuth(ReportsPage)
export const ProtectedSettings = withAuth(SettingsPage)

Tip

Cross-cutting concerns — logic that touches many unrelated features — are the sweet spot for HOCs (or their hook successors). Feature-specific logic belongs in the component or a dedicated hook.

Back to index


5. Implementation details

Follow these conventions so HOCs behave predictably in TypeScript and DevTools.

RuleWhy
Prefix with withwithAuth, withLoading, withRouter — signals enhancement
Accept Wrapped componentFirst or last argument depending on currying
Spread {...props}Pass through props the wrapper does not own
State in wrapperuseState / useEffect live inside the returned component
Set displayNameDevTools shows withAuth(Dashboard) not Anonymous
tsx
'use client'
 
import { useEffect, useState, type ComponentType } from 'react'
 
function withLoading<P extends object>(Wrapped: ComponentType<P>) {
  function WithLoading(props: P) {
    const [loading, setLoading] = useState(true)
    const [error, setError] = useState<string | null>(null)
 
    useEffect(() => {
      const t = setTimeout(() => setLoading(false), 500)
      return () => clearTimeout(t)
    }, [])
 
    if (loading) return <p>Loading…</p>
    if (error) return <p>Error: {error}</p>
 
    return <Wrapped {...props} />
  }
 
  WithLoading.displayName = `withLoading(${Wrapped.displayName ?? Wrapped.name ?? 'Component'})`
  return WithLoading
}

Curried HOC — configuration + component (HOF curry pattern)

tsx
function withPageView(pageName: string) {
  return function <P extends object>(Wrapped: ComponentType<P>) {
    return function WithPageView(props: P) {
      useEffect(() => {
        analytics.track('page_view', { page: pageName })
      }, [])
      return <Wrapped {...props} />
    }
  }
}
 
// Usage — same currying as multiply(2)(5)
const TrackedDashboard = withPageView('dashboard')(Dashboard)

Warning

Forgetting {...props} breaks wrapped components — they lose their original props. Always spread unless you intentionally filter props.

Back to index


6. Common use cases

Use caseHOC shapeInjected behavior
Loading statewithLoading(Wrapped)Shows spinner until ready
Error handlingwithErrorBoundary(Wrapped)Catches render errors
AuthenticationwithAuth(Wrapped)Redirect or block if logged out
Permission accesswithRole(['admin'])(Wrapped)Hide or deny by role
Data fetchingwithDataFetching(url)(Wrapped)Injects data, loading, error
LoggingwithPageView(name)(Wrapped)Tracks mount / interactions

Auth HOC

tsx
'use client'
 
import type { ComponentType } from 'react'
 
type Session = { userId: string; email: string }
 
function getSession(): Session | null {
  return { userId: '1', email: 'dev@example.com' }
}
 
export function withAuth<P extends object>(Wrapped: ComponentType<P>) {
  return function WithAuth(props: P) {
    const session = getSession()
    if (!session) return <p>Please sign in to continue.</p>
    return <Wrapped {...props} />
  }
}

Permission HOC

tsx
type Role = 'admin' | 'writer' | 'reader'
 
export function withRole(allowed: Role[]) {
  return function <P extends object>(Wrapped: ComponentType<P>) {
    return function WithRole(props: P) {
      const role = getCurrentUserRole()
      if (!allowed.includes(role)) {
        return <p>You do not have permission to view this page.</p>
      }
      return <Wrapped {...props} />
    }
  }
}
 
const AdminPanel = withRole(['admin'])(AdminPanelView)

Performance

Each HOC adds a component layer in the tree. Stack many HOCs and reconciliation cost grows — another reason hooks won favor for app code.

Back to index


7. Movie app: withDataFetching

Problem: MovieList and MovieAnalytics duplicate the same API call. Solution: a curried HOC injects fetched data as props — the same HOF factory pattern as withLogging(fetchMovies).

Before — duplicate fetch logic

tsx
function MovieList() {
  const [movies, setMovies] = useState([])
  useEffect(() => {
    fetch('/api/movies')
      .then((r) => r.json())
      .then(setMovies)
  }, [])
  return (
    <ul>
      {movies.map((m) => (
        <li key={m.id}>{m.title}</li>
      ))}
    </ul>
  )
}
 
function MovieAnalytics() {
  const [movies, setMovies] = useState([])
  useEffect(() => {
    fetch('/api/movies')
      .then((r) => r.json())
      .then(setMovies)
  }, [])
  return <p>Total movies: {movies.length}</p>
}

After — withDataFetching HOC

tsx
'use client'
 
import { useEffect, useState, type ComponentType } from 'react'
 
type FetchInjectedProps<T> = {
  data: T | null
  loading: boolean
  error: string | null
}
 
export function withDataFetching<T>(url: string) {
  return function <P extends object>(Wrapped: ComponentType<P & FetchInjectedProps<T>>) {
    return function WithDataFetching(props: P) {
      const [data, setData] = useState<T | null>(null)
      const [loading, setLoading] = useState(true)
      const [error, setError] = useState<string | null>(null)
 
      useEffect(() => {
        let cancelled = false
        fetch(url)
          .then((r) => {
            if (!r.ok) throw new Error(`HTTP ${r.status}`)
            return r.json()
          })
          .then((json: T) => {
            if (!cancelled) setData(json)
          })
          .catch((e: Error) => {
            if (!cancelled) setError(e.message)
          })
          .finally(() => {
            if (!cancelled) setLoading(false)
          })
 
        return () => {
          cancelled = true
        }
      }, [])
 
      if (loading) return <p>Loading movies…</p>
      if (error) return <p>Failed to load: {error}</p>
 
      return <Wrapped {...props} data={data} loading={loading} error={error} />
    }
  }
}
tsx
type Movie = { id: string; title: string; rating: number }
 
function MovieListView({ data }: { data: Movie[] | null }) {
  return (
    <ul>
      {(data ?? []).map((m) => (
        <li key={m.id}>
          {m.title} — ★ {m.rating}
        </li>
      ))}
    </ul>
  )
}
 
function MovieAnalyticsView({ data }: { data: Movie[] | null }) {
  const avg = data && data.length ? (data.reduce((s, m) => s + m.rating, 0) / data.length).toFixed(1) : '0'
  return (
    <div>
      <p>Count: {data?.length ?? 0}</p>
      <p>Average rating: {avg}</p>
    </div>
  )
}
 
export const MovieList = withDataFetching<Movie[]>('/api/movies')(MovieListView)
export const MovieAnalytics = withDataFetching<Movie[]>('/api/movies')(MovieAnalyticsView)

Tip

Type injected props separately (FetchInjectedProps<T>) so Wrapped explicitly declares data, loading, and error — TypeScript catches missing props at compile time.

Back to index


8. Practical task: admin dashboard

Build withUserDataAndPermissions — inject user profile and role, conditionally render admin/writer/reader UI.

tsx
'use client'
 
import type { ComponentType } from 'react'
 
type Role = 'admin' | 'writer' | 'reader'
 
type User = { id: string; name: string; role: Role }
 
function getSimulatedUser(): User {
  return { id: 'u1', name: 'Alex Chen', role: 'writer' }
}
 
type PermissionInjectedProps = {
  user: User
  canEdit: boolean
  canPublish: boolean
  canManageUsers: boolean
}
 
function resolvePermissions(role: Role) {
  return {
    canEdit: role === 'admin' || role === 'writer',
    canPublish: role === 'admin' || role === 'writer',
    canManageUsers: role === 'admin',
  }
}
 
export function withUserDataAndPermissions<P extends object>(Wrapped: ComponentType<P & PermissionInjectedProps>) {
  return function WithUserDataAndPermissions(props: P) {
    const user = getSimulatedUser()
    const permissions = resolvePermissions(user.role)
    return <Wrapped {...props} user={user} {...permissions} />
  }
}
tsx
function AdminDashboard({ user, canEdit, canPublish, canManageUsers }: PermissionInjectedProps) {
  return (
    <section>
      <header>
        <h1>Dashboard</h1>
        <p>
          Signed in as {user.name} ({user.role})
        </p>
      </header>
      <nav className="flex gap-2">
        <button type="button">View posts</button>
        {canEdit && <button type="button">Edit draft</button>}
        {canPublish && <button type="button">Publish</button>}
        {canManageUsers && <button type="button">Manage users</button>}
      </nav>
      {user.role === 'reader' && (
        <p className="text-sm text-neutral-600">Read-only access — contact admin to upgrade.</p>
      )}
    </section>
  )
}
 
export const Dashboard = withUserDataAndPermissions(AdminDashboard)
export const ProtectedDashboard = withAuth(withUserDataAndPermissions(AdminDashboard))
RolecanEditcanPublishcanManageUsers
admin✅✅✅
writer✅✅❌
reader❌❌❌

Interview Answer

"What's the relationship between higher-order functions and HOCs?"

A HOC is a higher-order function where the input and output types are React components instead of plain functions. withAuth(Dashboard) has the same factory shape as withLogging(add) — wrap once, reuse everywhere. Custom hooks are the modern way to share the logic without wrapping the component export.

Back to index


9. Pitfalls & limitations

PitfallProblemMitigation
Composition hellwithA(withB(withC(Component))) — deep nestingCombine into one HOC or use hooks
Harder debuggingProps injected opaquelydisplayName, TypeScript injected props
Static methods lostWrapped.fetchData not on enhanced exportHoist statics manually (rare today)
Ref forwarding breaksref stops at wrapperWrap with forwardRef
Duplicate side effectsSame HOC on siblings = duplicate fetchesShared cache / hook layer
tsx
// ❌ DevTools: WithLogging(WithAuth(WithLoading(Dashboard)))
export default withMountLog('dashboard')(withAuth(withLoading(withPageView('dashboard')(Dashboard))))
 
// ✅ Flatten — hooks are HOFs you call inside the component
function Dashboard() {
  usePageView('dashboard')
  const session = useSession()
  if (!session) return <SignInPrompt />
  return <DashboardView />
}

Warning

Composition hell is the top reason teams migrated from HOCs to hooks. If you stack more than two HOCs on one export, refactor.

Back to index


10. Modern alternatives

PatternTypeShare logic viaBest for
Custom hooksHOF-styleuseAuth() returns state/actionsApplication code (primary)
Render propsHOF-styleFunction child receives stateLibraries, flexible UI
HOCsHOF on componentsWrap component exportLegacy code, memo()

HOC → custom hook (same HOF idea, different surface)

tsx
// HOC — function(Component) → Component
const ProtectedReports = withAuth(withDataFetching<Report[]>('/api/reports')(ReportsView))
 
// Custom hook — function() → state (HOF returning behavior, not a component)
function useReports() {
  const session = useSession()
  const { data, loading, error } = useFetch<Report[]>('/api/reports')
  return { session, data, loading, error }
}
 
function ReportsPage() {
  const { session, data, loading, error } = useReports()
  if (!session) return <SignInPrompt />
  if (loading) return <Spinner />
  if (error) return <ErrorMessage error={error} />
  return <ReportsView data={data} />
}

Note

React's built-in memo() is technically an HOC — a higher-order function that takes a component and returns a memoized component. Custom hooks like useFetch are higher-order behavior without extra wrapper components.

Tip

Production checklist

  • Learn HOFs first — map, custom wrappers, curry, compose
  • HOCs: with prefix, displayName, spread {...props}
  • Type injected props explicitly in TypeScript
  • Avoid stacking more than two HOCs — prefer hooks
  • Hooks share the same DRY goal with fewer DevTools layers

Back to index


Summary

LayerPatternWraps
JavaScriptHigher-order functionFunctions (withLogging, map)
ReactHigher-order componentComponents (withAuth, memo)
Modern ReactCustom hookLogic only — no wrapper component

Higher-order functions are the foundation; HOCs apply that factory pattern to React components. Understand both, then default to custom hooks for new application code.

Next reads: Render Props Pattern, Container-Presenter Pattern, and React Design Patterns.

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime