How to build an inline edit component in React
Tags: react, beginners
Published: 2021-09-22 04:40:57

---
title: How to build an inline edit component in React
published: true
tags: react, beginners
canonical_url: https://www.emgoto.com/react-inline-edit/
cover_image: https://www.emgoto.com/react-inline-edit-dev.png
---

Inline editing allows users to edit content without navigating to a separate edit screen. In this tutorial, we’ll be building an accessible inline edit component in React. Here’s the final product:

 ![GIF showing example of React inline edit component](https://www.emgoto.com/react-inline-edit/inline-edit.gif)

We’ll also learn how to write some unit tests with React Testing Library. Let’s get started!

> This tutorial assumes a basic understanding of React, including hooks.
> 
> If you want to jump straight to the full code, check out the [React inline edit example](https://codepen.io/emgoto/pen/BapGVva) on Codepen.

## Inline editing and accessibility

When creating any React component, keep accessibility in mind. For example, your component should:

- Work with only a keyboard
- Use the correct HTML elements and other attributes to provide the most context to users

One way to approach writing an inline edit component is to have two separate components. One for a “view mode” and one for a “edit mode”:

```jsx
// View mode
<div onClick={startEditing}>Text value</div>

// Edit mode
<input value="Text value" />
```

When a user clicks on the view mode component, it will disappear and the edit mode will appear.

The second approach (and the one we will be implementing below) is to always use an input element. We can use CSS to make it _look_ as though it has begun editing when a user focuses on it.

```jsx
// View and edit mode
<input value="Text value" />
```

By always using an input element, we get behaviours like tabbing and focusing for free. It also makes more explicit what the purpose of the component is.

## Create your inline edit component with an input

Let’s get started by creating a React component that uses the HTML `input` tag:

```jsx
const InlineEdit = ({ value, setValue }) => {
  const onChange = (event) => setValue(event.target.value);

  return (
    <input
      type="text"
      aria-label="Field name"
      value={value}
      onChange={onChange}
    />
  )
}
```

The `aria-label` tells screen reader users the purpose of the input. For instance, if it was the name of a list, you could use "List name".

Then, let's render our new `InlineEdit` component, and pass in a `value` and `setValue` props:

```jsx
const App = () => {
  const [value, setValue] = useState();

  return <InlineEdit value={value} setValue={setValue} />;
}
```

In a real-life app, the `setValue` function would make an endpoint call to store the value in a database somewhere. For this tutorial though, we'll store the value in a `useState` hook.

## Add CSS to make it "click to edit"

We’ll then add some CSS to remove the input styling. This makes it look as though the user needs to click or focus on the input to start editing.

```jsx
input {
  background-color: transparent;
  border: 0;
  padding: 8px;
}
```

We’ll also add some styling to show that the component is editable when a user hovers over it:

```jsx
input:hover {
  background-color: #d3d3d3;
  cursor: pointer;
}
```

## Allow users to save when they press Enter or Escape

If a user clicks away from the input, it will lose focus and return to “view” mode. To keep things keyboard-friendly, we’ll want the escape and enter keys to achieve the same affect.

```jsx
const InlineEdit = ({ value, setValue }) => {
  const onChange = (event) => setValue(event.target.value);

  const onKeyDown = (event) => { if (event.key === "Enter" || event.key === "Escape") { event.target.blur(); } }
  return (
    <input
      type="text"
      aria-label="Field name"
      value={value}
      onChange={onChange}
      onKeyDown={onKeyDown} />
  )
}
```

## Only save on exit

Currently we call the `setValue` prop on each key press. In a real-life situation, where `setValue` was making an endpoint call, it would be making an endpoint call per keypress.

We want to prevent this from happening until a user exits the input.

Let’s create a local state variable called `editingValue`. This is where we’ll store the value of the input when it is in a “editing” phase.

```jsx
const InlineEdit = ({ value, setValue }) => {
  const [editingValue, setEditingValue] = useState(value);
  const onChange = (event) => setEditingValue(event.target.value);
  const onKeyDown = (event) => {
    if (event.key === "Enter" || event.key === "Escape") {
      event.target.blur();
    }
  }

  const onBlur = (event) => { setValue(event.target.value) }
  return (
    <input
      type="text"
      aria-label="Field name"
      value={editingValue} onChange={onChange}
      onKeyDown={onKeyDown}
      onBlur={onBlur} />
  )
}
```

A user exiting the input will call the `onBlur` handler. So we can use this to call `setValue`.

## Adding validation on empty strings

Finally, you don’t want users to be able to save an empty string or spaces as a value. In that case, we’ll cancel the edit and use the original value.

```jsx
const onBlur = (event) => {
  if (event.target.value.trim() === "") {
    setValue(value);
  } else {
    setValue(event.target.value)
  }
}
```

You'll now have a complete single-line inline edit component. Here's the full code:

```jsx
import { useState } from 'react';

const InlineEdit = ({ value, setValue }) => {
  const [editingValue, setEditingValue] = useState(value);
  
  const onChange = (event) => setEditingValue(event.target.value);
  
  const onKeyDown = (event) => {
    if (event.key === "Enter" || event.key === "Escape") {
      event.target.blur();
    }
  }
  
  const onBlur = (event) => {
    if (event.target.value.trim() === "") {
      setEditingValue(value);
    } else {
      setValue(event.target.value)
    }
  }

  return (
    <input
      type="text"
      aria-label="Field name"
      value={editingValue}
      onChange={onChange}
      onKeyDown={onKeyDown}
      onBlur={onBlur}
    />
  );
};

const App = () => {
  const [value, setValue] = useState();

  return <InlineEdit value={value} setValue={setValue} />;
};
```

## Creating a multiline inline edit

If you want your inline edit component to be multiline, we can use the `textarea` element instead:

```jsx
<textarea
  rows={1}
  aria-label="Field name"
  value={editingValue}
  onBlur={onBlur}
  onChange={onChange}
  onKeyDown={onKeyDown}
/>
```

The one difference with textarea is that you pass in a `rows` value. This specifies the height of your textarea.

By default, textareas aren't dynamic. Luckily, over on StackOverflow I found a [solution](https://stackoverflow.com/questions/17772260/textarea-auto-height) to this problem.

If you add the following CSS to your text area:

```jsx
textarea {
  resize: none;
  overflow: hidden;
  min-height: 14px;
  max-height: 100px;
}
```

And then pass in an `onInput` handler, you’ll be able to achieve a “dynamic” look.

```jsx
import { useEffect } from 'react';

const onInput = (event) => {
  if (event.target.scrollHeight > 33) { 
    event.target.style.height = "5px";
    event.target.style.height = (event.target.scrollHeight - 16) + "px";
  }
}

return (
  <textarea
   rows={1}
   aria-label="Field name"
   value={editingValue}
   onBlur={onBlur}
   onChange={onChange}
   onKeyDown={onKeyDown}
   onInput={onInput} />
)
```

Note you may need to fiddle around with some of the values in the `onInput` depending on the height and font size of your text area.

The one other thing you’ll need to add is a focus ring - the blue outline around a focused element. We can do this with some CSS:

```jsx
textarea:focus {
  outline: 5px auto Highlight; /* Firefox */
  outline: 5px auto -webkit-focus-ring-color; /* Chrome, Safari */
}
```

And you’re done! Here's the full code for a multiline inline edit:

```jsx
import { useState, useRef } from 'react';

const MultilineEdit = ({ value, setValue }) => {
  const [editingValue, setEditingValue] = useState(value);

  const onChange = (event) => setEditingValue(event.target.value);

  const onKeyDown = (event) => {
    if (event.key === "Enter" || event.key === "Escape") {
      event.target.blur();
    }
  };

  const onBlur = (event) => {
    if (event.target.value.trim() === "") {
      setEditingValue(value);
    } else {
      setValue(event.target.value);
    }
  };

  const onInput = (target) => {
    if (target.scrollHeight > 33) {
      target.style.height = "5px";
      target.style.height = target.scrollHeight - 16 + "px";
    }
  };

  const textareaRef = useRef();

  useEffect(() => {
    onInput(textareaRef.current);
  }, [onInput, textareaRef]);

  return (
    <textarea
      rows={1}
      aria-label="Field name"
      value={editingValue}
      onBlur={onBlur}
      onChange={onChange}
      onKeyDown={onKeyDown}
      onInput={(event) => onInput(event.target)}
      ref={textareaRef}
    />
  );
};
```

## Ensure your component's functionality with unit tests

Before we finish, let’s write a couple of unit tests to ensure the functionality of our component. We’ll be using React Testing Library:

```sh
npm install --save-dev @testing-library/react @testing-library/user-event
# or
yarn add -D @testing-library/react @testing-library/user-event
```

We can ensure that pressing enter causes the input to lose focus:

```jsx
import { useState } from 'react';
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import InlineEdit from "./Inline-Edit";

const apples = "apples"
const oranges = "oranges"

const TestComponent = () => {
  const [value, setValue] = useState(apples);
  return <InlineEdit value={value} setValue={setValue} />;
}

describe("Inline Edit component", () => {
  test("should save input and lose focus when user presses enter", () => {
    render(<TestComponent />)
    const input = screen.getByRole("textbox");

    userEvent.type(input, `{selectall}${oranges}{enter}`);
    // RTL doesn't properly trigger component's onBlur()
    fireEvent.blur(input); 

    expect(input).not.toHaveFocus();
    expect(input).toHaveValue(oranges);
  });
});
```

If you haven’t used React Testing Library before, let’s break this test down:

- The `render` function will render your component into a container. You can access it using the `screen` variable
- We search for the input component via its aria [role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles), `"textbox"`
- We can use the `userEvent.type()` function to simulate a user typing. If you want to type special keys like space or enter, you can do it with curly braces around it (e.g `{space}` and `{enter}`)

Similarly, we can write two more unit tests:

```jsx
test("should focus when tabbed to", () => {
  render(<TestComponent />);
  const input = screen.getByRole("textbox");

  expect(document.body).toHaveFocus();
  userEvent.tab();

  expect(input).toHaveFocus();
});

test("should reset to last-saved value if input is empty", () => {
  render(<TestComponent />);
  const input = screen.getByRole("textbox");

  userEvent.type(input, "{selectall}{space}{enter}");
  fireEvent.blur(input);

  expect(input).toHaveValue(originalName)
});
```

And finally, we can use a cool library called [jest-axe](https://github.com/nickcolley/jest-axe). You can use it to assert that your component doesn’t have any accessibility violations:

```jsx
import { axe, toHaveNoViolations } from "jest-axe"

expect.extend(toHaveNoViolations)

test("should not have any accessibility violations", async () => {
  const { container } = render(<TestComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
```

If we had forgotten to include an `aria-label`, for instance, then this test would have failed.

* * *

And that's it! Now you should be able to create inline-editable components for your React app, complete with unit tests.