Next.js Client Components — When to Use Them

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Next.js Client Components — When to Use Them

Client Components handle interactivity. They run in the browser where users can interact with them using React hooks and browser APIs.

What Are Client Components?

Client Components are marked with the 'use client' directive. They:

  • Run entirely in the browser
  • Can use React hooks like useState, useEffect
  • Can access browser APIs like localStorage, geolocation
  • Handle user interactions and real-time updates
// components/counter.tsx
'use client'

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

When to Use Client Components

Use Client Components when you need:

1. State Management

'use client'

export function SearchForm() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <Results items={results} />
    </div>
  )
}

2. Browser APIs

'use client'

import { useEffect, useState } from 'react'

export function LocationTracker() {
  const [location, setLocation] = useState(null)

  useEffect(() => {
    navigator.geolocation.getCurrentPosition((pos) => {
      setLocation(pos.coords)
    })
  }, [])

  return <div>Lat: {location?.latitude}, Lng: {location?.longitude}</div>
}

3. Event Listeners

'use client'

import { useEffect } from 'react'

export function WindowSize() {
  useEffect(() => {
    function handleResize() {
      console.log(`Window size: ${window.innerWidth}x${window.innerHeight}`)
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return <div>Watch console for size changes</div>
}

Hooks in Client Components

All React hooks work in Client Components:

'use client'

import { useState, useEffect, useCallback, useMemo } from 'react'

export function AdvancedForm() {
  const [formData, setFormData] = useState({ name: '', email: '' })
  const [isSubmitting, setIsSubmitting] = useState(false)

  const isValid = useMemo(() => {
    return formData.name.length > 0 && formData.email.includes('@')
  }, [formData])

  const handleSubmit = useCallback(async (e) => {
    e.preventDefault()
    setIsSubmitting(true)

    try {
      await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(formData)
      })
    } finally {
      setIsSubmitting(false)
    }
  }, [formData])

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
      />
      <button disabled={!isValid || isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  )
}

Context in Client Components

Use React Context for state sharing:

'use client'

import { createContext, useContext, useState } from 'react'

const ThemeContext = createContext()

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  return useContext(ThemeContext)
}

Effects and Lifecycle

useEffect handles side effects:

'use client'

import { useEffect, useState } from 'react'

export function PostList() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const res = await fetch('/api/posts')
        const data = await res.json()
        setPosts(data)
      } finally {
        setLoading(false)
      }
    }

    fetchPosts()
  }, [])

  if (loading) return <div>Loading...</div>
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}

Event Handling

Client Components handle user interactions:

'use client'

export function InteractiveButton() {
  const handleClick = () => alert('Button clicked!')
  const handleMouseEnter = () => console.log('Mouse entered')
  const handleChange = (e) => console.log('Input:', e.target.value)

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <button onMouseEnter={handleMouseEnter}>Hover me</button>
      <input onChange={handleChange} />
    </div>
  )
}

Performance Optimization

Use memoization to prevent unnecessary re-renders:

'use client'

import { memo, useMemo, useCallback } from 'react'

const ExpensiveComponent = memo(({ data }) => {
  return <div>{/* Expensive render */}</div>
})

export function ParentComponent() {
  const memoizedData = useMemo(() => computeData(), [])
  const memoizedCallback = useCallback(() => handleAction(), [])

  return (
    <div>
      <ExpensiveComponent data={memoizedData} />
      <button onClick={memoizedCallback}>Action</button>
    </div>
  )
}

Real-World Example: Form with Validation

'use client'

import { useState, useCallback } from 'react'

export function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  })
  const [errors, setErrors] = useState({})

  const handleChange = useCallback((e) => {
    const { name, value } = e.target
    setFormData(prev => ({ ...prev, [name]: value }))
  }, [])

  const handleSubmit = useCallback(async (e) => {
    e.preventDefault()

    const newErrors = {}
    if (!formData.username) newErrors.username = 'Required'
    if (!formData.email.includes('@')) newErrors.email = 'Invalid email'
    if (formData.password.length &lt; 8) newErrors.password = 'Min 8 chars'

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors)
      return
    }

    // Submit form
    await fetch('/api/register', {
      method: 'POST',
      body: JSON.stringify(formData)
    })
  }, [formData])

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={formData.username}
        onChange={handleChange}
      />
      {errors.username && <span>{errors.username}</span>}

      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
      />
      {errors.email && <span>{errors.email}</span>}

      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
      />
      {errors.password && <span>{errors.password}</span>}

      <button type="submit">Register</button>
    </form>
  )
}

FAQ

Q: When should I use Client Components vs Server Components? A: Server Components by default. Use Client Components only for interactivity, state, or browser APIs.

Q: Can I use hooks in a Server Component? A: No, hooks are React features that only work in Client Components.

Q: Should I wrap my entire app in 'use client'? A: No, that defeats the benefits of Server Components. Use 'use client' granularly for components that need it.


Master Client Components and you'll build responsive, interactive experiences efficiently.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro