Building a Simple Inline Edit Form with React

Creating intuitive inline editing functionality is a common requirement in modern web applications. In this tutorial, we’ll build a clean, performant implementation using only React – no additional libraries required. While we’ll focus on text input fields, the approach can be easily extended to other input types like textareas and dropdowns.

Here is the final result:

The article is split into 2 main parts – behavior and styling. I will begin with the behavior.

Understanding the Data Structure

When implementing inline editing, it’s crucial to start with a well-structured data format. While real-world applications typically fetch data from APIs in varying formats, we’ll use a structure that efficiently supports our UI requirements. Each editable field in our implementation contains:

  • label: Display name for the field
  • value: Current field content
  • isEditing: Boolean flag to track edit state
  • inputRef: Reference for managing focus(will be explained later)
{
  name: { label: 'Full name', value: 'Chuck Norris', isEditing: false, inputRef: useRef(null) },
  address: { label: 'Address', value: '1600 Freedom Avenue NW', isEditing: false, inputRef: useRef(null) },
  phone: { label: 'Phone', value: '+1-212-456-7890', isEditing: false, inputRef: useRef(null) },
}

Core Implementation

The heart of our implementation revolves around three key functions:

  • renderButton: Creates the edit toggle button for each field
  • toggleEditForField: Handles switching fields between view and edit modes
  • renderField: Conditionally renders either an input or span based on edit state

These functions work together within a form component that iterates through our field data, displaying each field with its corresponding label and edit button.

function App() {
  const [fields, updateFields] = useState({
    name: { label: 'Full name', value: 'Chuck Norris', isEditing: false },
    address: { label: 'Address', value: '1600 Freedom Avenue NW', isEditing: false },
    phone: { label: 'Phone', value: '+1-212-456-7890', isEditing: false },
  });

  const toggleEditForField = (fieldKey) => {
    const isSelectedFieldInEditingMode = fields[fieldKey].isEditing;
    // Invert the current value of isEditing for the changed field
    updateFields((fields) => ({
      ...fields,
      [fieldKey]: { ...fields[fieldKey], isEditing: !isSelectedFieldInEditingMode },
    }));
  };

  const handleFieldValueChange = (e, fieldKey) => {
    const { value } = e.target;
    updateFields((fields) => ({
      ...fields,
      [fieldKey]: { ...fields[fieldKey], value },
    }));
  };

  const renderField = (key) => {
    const field = fields[key];
    return field.isEditing ? (
      <input type="text" value={field.value} onChange={(e) => handleFieldValueChange(e, key)}/>
    ) : (
      <span className="form-field-value-text">{field.value}</span>
    );
  };

  const renderButton = (key) => {
    const isInEditMode = fields[key].isEditing ? 'Save' : 'Edit';
    return (
      <button className="btn" onClick={() => toggleEditForField(key)} type="button">
        {isInEditMode}
      </button>
    );
  };

  return (
    <div>
      <h1>Your profile</h1>
      <form className="form-container">
        {Object.entries(fields).map(([key, field]) => (
          <div className="form-field" key={key}>
            <div className="form-field-label">{field.label}:</div>
            <div className="form-field-value">{renderField(key)}</div>
            <div className="form-field-actions">{renderButton(key)}</div>
          </div>
        ))}
      </form>
    </div>
  );
}

export default App;

Enhancing the User Experience with Auto-Focus

There are multiple ways to implement auto-focus, but I’ll use the one, that to me, is the easiest to implement and doesn’t require any “hacks”. First, we need to add an inputRef property to each field object using useRef(null) as a default value:

  const [fields, updateFields] = useState({
    name: { label: 'Full name', value: 'Chuck Norris', isEditing: false, inputRef: useRef(null) },
    address: { label: 'Address', value: '1600 Freedom Avenue NW', isEditing: false, inputRef: useRef(null) },
    phone: { label: 'Phone', value: '+1-212-456-7890', isEditing: false, inputRef: useRef(null) },
  });

Then we will attach this ref to each input field in the renderField function:

  const renderField = (key) => {
    const field = fields[key];
    return field.isEditing ? (
      <input type="text" value={field.value} onChange={(e) => handleFieldValueChange(e, key)} ref={field.inputRef} />
    ) : (
      <span className="form-field-value-text">{field.value}</span>
    );
  };

Then we add the lastEditedFieldKey state variable tracking the most recently edited field:

const [lastEditedFieldKey, setLastEditedFieldKey] = useState(null);

Lastly, inside a useEffect we automatically focus the appropriate input when edit mode is toggled:

  useEffect(() => {
    // When a new field is set to isEditing, focus on its input
    if (lastEditedFieldKey && fields[lastEditedFieldKey].inputRef.current) {
      fields[lastEditedFieldKey].inputRef.current.focus();
    }
  }, [lastEditedFieldKey, fields]);

Here is the final result:

import { useEffect, useRef, useState } from 'react';
import './App.css';

function App() {
  const [fields, updateFields] = useState({
    name: { label: 'Full name', value: 'Chuck Norris', isEditing: false, inputRef: useRef(null) },
    address: { label: 'Address', value: '1600 Freedom Avenue NW', isEditing: false, inputRef: useRef(null) },
    phone: { label: 'Phone', value: '+1-212-456-7890', isEditing: false, inputRef: useRef(null) },
  });
  const [lastEditedFieldKey, setLastEditedFieldKey] = useState(null);

  useEffect(() => {
    // When a new field is set to isEditing, focus on its input
    if (lastEditedFieldKey && fields[lastEditedFieldKey].inputRef.current) {
      fields[lastEditedFieldKey].inputRef.current.focus();
    }
  }, [lastEditedFieldKey, fields]);

  const toggleEditForField = (fieldKey) => {
    const isSelectedFieldInEditingMode = fields[fieldKey].isEditing;
    // If the value of the field is being edited, set it as the last edited field so that its input is focused
    if (!isSelectedFieldInEditingMode) {
      setLastEditedFieldKey(fieldKey);
    }

    // Invert the current value of isEditing for the changed field
    updateFields((fields) => ({
      ...fields,
      [fieldKey]: { ...fields[fieldKey], isEditing: !isSelectedFieldInEditingMode },
    }));
  };

  const handleFieldValueChange = (e, fieldKey) => {
    const { value } = e.target;
    updateFields((fields) => ({
      ...fields,
      [fieldKey]: { ...fields[fieldKey], value },
    }));
  };

  const renderField = (key) => {
    const field = fields[key];
    return field.isEditing ? (
      <input type="text" value={field.value} onChange={(e) => handleFieldValueChange(e, key)} ref={field.inputRef} />
    ) : (
      <span className="form-field-value-text">{field.value}</span>
    );
  };

  const renderButton = (key) => {
    const isInEditMode = fields[key].isEditing ? 'Save' : 'Edit';
    return (
      <button className="btn" onClick={() => toggleEditForField(key)} type="button">
        {isInEditMode}
      </button>
    );
  };

  return (
    <div>
      <h1>Your profile</h1>
      <form className="form-container">
        {Object.entries(fields).map(([key, field]) => (
          <div className="form-field" key={key}>
            <div className="form-field-label">{field.label}:</div>
            <div className="form-field-value">{renderField(key)}</div>
            <div className="form-field-actions">{renderButton(key)}</div>
          </div>
        ))}
      </form>
    </div>
  );
}

export default App;

Styling for Seamless Transitions

One of the most important aspects of inline editing is maintaining visual consistency between edit and view modes. Poor styling can lead to jarring content shifts that disrupt the user experience. Our styling approach focuses on:

  1. Using a fixed-width container for field values(.form-field-value)
  2. Maintaining consistent font styling across both modes
  3. Adding transparent borders to span elements to match input heights
/* The form element */
.form-container {
    border:1px solid #ccc;
    border-radius: 1rem;
    padding: 1.5rem;
}

/* Wrapper element for each form field */
.form-field {
    display: flex;
    margin: 0.5rem 0;
    height: 35px;
    border-bottom: 1px solid #e0e0e0;
    padding: 0.5rem 0;
}

/* Field label */
.form-field-label {
    display: flex;
    font-weight: bold;
    align-items: center;
    margin-right: 1rem;
    width: 80px;
}

/* Wrapper element for the text and input value */
.form-field-value {
    margin-right: 1rem;
    width: 300px;
    text-align: left;
    display: flex;
}

/* Field's toggle button */
.form-field-actions {
    display: flex;
    align-items: center;
}

/* Field value(in non-edit mode) */
.form-field-value-text {
    display: flex;
    align-items: center;
    padding:0.5rem;
    font-size:1rem;
    border: 1px solid transparent;
}

/* Field value input(edit mode) */
input {
    display: inline-block;
    width: 300px;
    font-size:1rem;
    padding:0.5rem;
    border: 1px solid #e7e7e7;
    border-radius: 0.5rem;
}

/* Reset font styles for input and span so that the text looks the same */
.form-field-value-text,
input[type="text"] {
    font-family: inherit;
    font-size: inherit;
    line-height: inherit;
    box-sizing: border-box;
}

/* The toggle button styling */
.btn {
    width: 65px;
    padding: 0.3rem 0;
    border: none;
    border-radius: 0.5rem;
    background-color: #242424;
    color: white;
    cursor: pointer;
}

Link to GitHub repo.

I hope that the tutorial was helpful, feel free to reach out for any feedback!