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 fieldvalue
: Current field contentisEditing
: Boolean flag to track edit stateinputRef
: 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 fieldtoggleEditForField
: Handles switching fields between view and edit modesrenderField
: 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:
- Using a fixed-width container for field values(
.form-field-value
) - Maintaining consistent font styling across both modes
- 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;
}
I hope that the tutorial was helpful, feel free to reach out for any feedback!