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.
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:
- Higher-order functions — JavaScript foundation (
map,compose, curry) - From HOF to HOC — how the pattern maps to React
- HOC core concepts — component factory, input/output shape
- Purpose, implementation, and use cases
- Movie app & admin dashboard examples
- Pitfalls and modern hook alternatives
Quick index
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.
HOF(fn) → newFn| Trait | Meaning |
|---|---|
| Takes a function as input | map, filter, addLogging(handler) |
| Returns a function | () => …, curried factories |
| Abstracts shared behavior | One wrapper, many call sites |
| Enables composition | Chain small functions into pipelines |
Built-in HOFs you already use
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) // 10Custom HOF — wrap any function with logging
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 5Currying — function that returns a function
function multiply(a) {
return function (b) {
return a * b
}
}
const double = multiply(2)
const triple = multiply(3)
double(5) // 10
triple(5) // 15Composition — pipe HOFs together
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"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: function | Input: component |
| Output: enhanced function | Output: enhanced component |
| Wraps call behavior | Wraps render behavior |
Currying: (config) => (fn) => newFn | (url) => (Component) => Enhanced |
// HOF mental model
const enhanced = withBehavior(original)
// HOC mental model
const EnhancedComponent = withBehavior(OriginalComponent)// 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} />
}
}3. HOC core concepts
An HOC is not a component — it is a function that returns a component.
withEnhancement(WrappedComponent) → EnhancedComponent| Concept | Meaning |
|---|---|
| HOF basis | Functions operating on functions — HOCs operate on components |
| Component factory | Produces new component types from existing ones |
| Takes component as input | Wrapped: ComponentType<P> |
| Returns enhanced component | Adds props, guards, or side effects before rendering Wrapped |
| Function that returns a function | Often curried: (url) => (Wrapped) => Enhanced |
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"4. Purpose & benefits
| Benefit | What it means |
|---|---|
| Sharing logic across components | One withAuth protects every route component |
| Reusability (DRY) | Write fetch/loading once, wrap many pages |
| Centralizing common scenarios | Spinner, error boundary, analytics in one place |
| Abstracting cross-cutting concerns | Auth, logging, i18n — not mixed into feature JSX |
Before — duplicated auth checks
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
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)5. Implementation details
Follow these conventions so HOCs behave predictably in TypeScript and DevTools.
| Rule | Why |
|---|---|
Prefix with with | withAuth, withLoading, withRouter — signals enhancement |
Accept Wrapped component | First or last argument depending on currying |
Spread {...props} | Pass through props the wrapper does not own |
| State in wrapper | useState / useEffect live inside the returned component |
Set displayName | DevTools shows withAuth(Dashboard) not Anonymous |
'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)
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)6. Common use cases
| Use case | HOC shape | Injected behavior |
|---|---|---|
| Loading state | withLoading(Wrapped) | Shows spinner until ready |
| Error handling | withErrorBoundary(Wrapped) | Catches render errors |
| Authentication | withAuth(Wrapped) | Redirect or block if logged out |
| Permission access | withRole(['admin'])(Wrapped) | Hide or deny by role |
| Data fetching | withDataFetching(url)(Wrapped) | Injects data, loading, error |
| Logging | withPageView(name)(Wrapped) | Tracks mount / interactions |
Auth HOC
'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
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)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
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
'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} />
}
}
}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)8. Practical task: admin dashboard
Build withUserDataAndPermissions — inject user profile and role, conditionally render admin/writer/reader UI.
'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} />
}
}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))| Role | canEdit | canPublish | canManageUsers |
|---|---|---|---|
| admin | ✅ | ✅ | ✅ |
| writer | ✅ | ✅ | ❌ |
| reader | ❌ | ❌ | ❌ |
9. Pitfalls & limitations
| Pitfall | Problem | Mitigation |
|---|---|---|
| Composition hell | withA(withB(withC(Component))) — deep nesting | Combine into one HOC or use hooks |
| Harder debugging | Props injected opaquely | displayName, TypeScript injected props |
| Static methods lost | Wrapped.fetchData not on enhanced export | Hoist statics manually (rare today) |
| Ref forwarding breaks | ref stops at wrapper | Wrap with forwardRef |
| Duplicate side effects | Same HOC on siblings = duplicate fetches | Shared cache / hook layer |
// ❌ 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 />
}10. Modern alternatives
| Pattern | Type | Share logic via | Best for |
|---|---|---|---|
| Custom hooks | HOF-style | useAuth() returns state/actions | Application code (primary) |
| Render props | HOF-style | Function child receives state | Libraries, flexible UI |
| HOCs | HOF on components | Wrap component export | Legacy code, memo() |
HOC → custom hook (same HOF idea, different surface)
// 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} />
}Summary
| Layer | Pattern | Wraps |
|---|---|---|
| JavaScript | Higher-order function | Functions (withLogging, map) |
| React | Higher-order component | Components (withAuth, memo) |
| Modern React | Custom hook | Logic 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.
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime