Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to build a profile page in React

Building a user profile page is a common requirement in React applications, typically featuring user information display, edit mode, and form submission. As the creator of CoreUI with over 10 years of React experience since 2014, I’ve built profile interfaces for countless applications ranging from simple user management to complex enterprise systems. The most effective approach combines useState for managing edit/view modes, controlled inputs for form fields, and optimistic UI updates for smooth user experience. This pattern provides a professional, responsive profile page with minimal complexity.

Create a profile component with view and edit modes using useState.

import { useState } from 'react'

function ProfilePage() {
  const [isEditing, setIsEditing] = useState(false)
  const [profile, setProfile] = useState({
    name: 'John Doe',
    email: '[email protected]',
    bio: 'Software developer'
  })

  return (
    <div className="profile-page">
      <h1>Profile</h1>
      {isEditing ? (
        <EditMode profile={profile} setProfile={setProfile} setIsEditing={setIsEditing} />
      ) : (
        <ViewMode profile={profile} setIsEditing={setIsEditing} />
      )}
    </div>
  )
}

The isEditing state controls whether to show view or edit mode. The profile state holds user data. This pattern separates concerns by using different components for view and edit modes, making the code easier to maintain.

Creating the View Mode Component

Display profile information with an edit button.

function ViewMode({ profile, setIsEditing }) {
  return (
    <div className="view-mode">
      <div className="profile-field">
        <label>Name:</label>
        <p>{profile.name}</p>
      </div>
      <div className="profile-field">
        <label>Email:</label>
        <p>{profile.email}</p>
      </div>
      <div className="profile-field">
        <label>Bio:</label>
        <p>{profile.bio}</p>
      </div>
      <button onClick={() => setIsEditing(true)}>Edit Profile</button>
    </div>
  )
}

The view mode displays profile data in a read-only format. The edit button switches to edit mode by calling setIsEditing(true). This provides a clean separation between viewing and editing functionality.

Creating the Edit Mode Component

Build an editable form with controlled inputs.

import { useState } from 'react'

function EditMode({ profile, setProfile, setIsEditing }) {
  const [formData, setFormData] = useState(profile)

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

  const handleSubmit = (e) => {
    e.preventDefault()
    setProfile(formData)
    setIsEditing(false)
  }

  const handleCancel = () => {
    setFormData(profile)
    setIsEditing(false)
  }

  return (
    <form onSubmit={handleSubmit} className="edit-mode">
      <div className="form-field">
        <label>Name:</label>
        <input
          type="text"
          name="name"
          value={formData.name}
          onChange={handleChange}
        />
      </div>
      <div className="form-field">
        <label>Email:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
      </div>
      <div className="form-field">
        <label>Bio:</label>
        <textarea
          name="bio"
          value={formData.bio}
          onChange={handleChange}
          rows="4"
        />
      </div>
      <div className="button-group">
        <button type="submit">Save Changes</button>
        <button type="button" onClick={handleCancel}>Cancel</button>
      </div>
    </form>
  )
}

The formData state tracks temporary changes before saving. The handleChange function updates form fields. The handleSubmit function saves changes and exits edit mode. The handleCancel function discards changes and returns to view mode. This pattern prevents accidental data loss.

Adding Avatar Upload

Implement profile picture upload with preview.

import { useState } from 'react'

function ProfileWithAvatar() {
  const [profile, setProfile] = useState({
    name: 'John Doe',
    email: '[email protected]',
    avatar: null
  })
  const [previewUrl, setPreviewUrl] = useState(null)

  const handleAvatarChange = (e) => {
    const file = e.target.files[0]
    if (file) {
      setProfile({ ...profile, avatar: file })
      setPreviewUrl(URL.createObjectURL(file))
    }
  }

  return (
    <div className="profile-with-avatar">
      <div className="avatar-section">
        {previewUrl ? (
          <img src={previewUrl} alt="Profile" className="avatar" />
        ) : (
          <div className="avatar-placeholder">No Image</div>
        )}
        <input
          type="file"
          accept="image/*"
          onChange={handleAvatarChange}
        />
      </div>
    </div>
  )
}

The file input allows users to select an image. The URL.createObjectURL() creates a preview URL from the selected file. The preview displays immediately without uploading, providing instant visual feedback.

Adding Form Validation

Validate profile data before saving.

function EditModeWithValidation({ profile, setProfile, setIsEditing }) {
  const [formData, setFormData] = useState(profile)
  const [errors, setErrors] = useState({})

  const validate = () => {
    const newErrors = {}

    if (!formData.name.trim()) {
      newErrors.name = 'Name is required'
    }

    if (!formData.email.trim()) {
      newErrors.email = 'Email is required'
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Email is invalid'
    }

    if (formData.bio.length > 500) {
      newErrors.bio = 'Bio must be less than 500 characters'
    }

    return newErrors
  }

  const handleSubmit = (e) => {
    e.preventDefault()
    const validationErrors = validate()

    if (Object.keys(validationErrors).length === 0) {
      setProfile(formData)
      setIsEditing(false)
    } else {
      setErrors(validationErrors)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div className="form-field">
        <input
          type="text"
          name="name"
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        />
        {errors.name && <span className="error">{errors.name}</span>}
      </div>
      <div className="form-field">
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      <button type="submit">Save Changes</button>
    </form>
  )
}

The validate() function checks all form fields and returns an errors object. If validation passes (no errors), the form submits. Otherwise, error messages display below each invalid field. This prevents saving invalid data.

Fetching Profile Data from API

Load profile data from a backend API on component mount.

import { useState, useEffect } from 'react'

function ProfilePageWithAPI() {
  const [profile, setProfile] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetchProfile()
  }, [])

  const fetchProfile = async () => {
    try {
      const response = await fetch('/api/profile')
      if (!response.ok) throw new Error('Failed to fetch profile')
      const data = await response.json()
      setProfile(data)
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

  const saveProfile = async (updatedProfile) => {
    try {
      const response = await fetch('/api/profile', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updatedProfile)
      })
      if (!response.ok) throw new Error('Failed to save profile')
      const data = await response.json()
      setProfile(data)
    } catch (err) {
      setError(err.message)
    }
  }

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

  return <ProfilePage profile={profile} onSave={saveProfile} />
}

The useEffect hook fetches profile data when the component mounts. The loading state shows a loading indicator. The error state displays error messages. The saveProfile function sends updates to the API. This pattern handles all async operations cleanly.

Best Practice Note

This is the same profile page architecture we use in CoreUI admin templates for user management interfaces. For production applications, consider adding loading states during save operations, success notifications after updates, and confirmation dialogs before discarding changes. Implement debounced autosave for better UX on longer forms. For complex profiles with multiple sections, split the form into tabs or accordion panels. Consider using CoreUI’s form components which provide consistent styling and built-in validation support, reducing development time for professional-looking profile interfaces.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author