How to Create an Editable Table Component in React
Tables are often employed in web apps to represent data in a structured format. For applications where data can be updated, they're also accompanied by a dedicated form or a modal where users can edit entries.
However, making the table itself editable can enhance user experience by eliminating the need for a separate interface. In this tutorial, we'll see how you can create an editable dynamic table component in React from the ground up.
How to Use the Component
First, let's see how we want to use the component, so we can create the props and layout accordingly. We want to create the following functionality, where the table can be customized via props:
import { EditableTable } from './EditableTable'
import { data } from './data'
const App = () => {
const update = data => {
console.log('Updated table data:', data)
}
return (
<EditableTable
headings={['ID', 'Name', 'Score', 'Age', 'Active']}
rows={data}
callback={update}
/>
)
}
The component is called EditableTable
, which accepts two mandatory and one optional prop:
headings
: This is the only optional prop, which accepts an array of strings. These will be displayed as the table's header. If they're not present, no header will be displayed. The number of items in the array must match the number of properties of each object inside therows
prop.rows
: The data that we want to display. See the example below for the required data format. It must be an array of objects, with any type and number of properties.callback
: A callback function that will be executed each time a change is made in the table. This function will receive the updated data (that is passed to therows
prop) that can be passed forward to an API.
export const data = [
{
id: '6512fb5737b7fd3482d41d90',
name: 'Church Hamilton',
score: 1232,
age: 17,
isActive: false
}
]
To generate random mock data for your application, you can use JSON Generator.
Displaying Tables
Now that we know how we want to call the component, let's create the layout. Create a new file called EditableTable.jsx
and add the following lines of code:
export const EditableTable = ({ headings, rows, callback }) => {
return (
<table>
{!!headings?.length && (
<thead>
<tr>
{headings.map((heading, index) => (
<th key={index}>
{heading}
</th>
))}
</tr>
</thead>
)}
<tbody>...</tbody>
</table>
)
}
Let's break the component down into two parts, as there are quite a few lines involved. First, we want to conditionally render a thead
into our table
element if the headings
prop is present.
We can use a simple map
to loop through the array and display each string inside a th
. Don't forget to pass a key
prop for React. Inside the tbody
, we want to loop through the rows
:
<tbody>
{rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{Object.entries(row).map((entry, columnIndex) => (
<td key={columnIndex}>
{columnIndex === 0
? entry[1]
: <input
type={getInputType(entry[1])}
checked={getInputType(entry[1]) === 'checkbox' ? entry[1] : null}
value={rows[rowIndex][entry[0]]}
placeholder={headings?.[columnIndex]}
/>
}
</td>
))}
</tr>
))}
</tbody>
Here, we need to create a nested loop. First, we need to loop through the rows (each object inside the array), and then loop through the columns (each property inside each object).
To achieve this, we can use Object.entries
inside rows.map
. This returns a multidimensional array, where each array contains the key and value pairs of the object:
This way, we can reference both the key and the value, where entry[0]
represents the key and entry[1]
represents the value. Inside Object.entries
, we have the following logic:
- Lines 6-7: If we're at the 0th index, we know we're processing the ID. This should not be editable by users, so we can display them as is. This step is optional, and depending on your use case, you can omit this part. It assumes that the first element of the object is always the
id
. - Line 8-12: For each property inside the object, we create an
input
field with the following values.type
: To set the correct type for the input, we'll use a helper function that accepts the value of the property.checked
: Using the same helper function, we'll set thechecked
property if the value of the property is a boolean. It means that fortrue
andfalse
values, a checkbox will be created.value
: To set the value of the input, we can reference it usingrows[rowIndex][entry[0]]
, which translates to an index and a key. For example:rows[0]['name']
will place the value of the name property of the first object in the array into the input.placeholder
: For the placeholder, we can use theheadings
prop combined withcolumnIndex
. This means if the input is empty, the placeholder will show the same text as the column's heading.
The getInputType function
To get the proper input type based on the value of the property, add the following helper function in front of the return
statement:
const getInputType = value => {
const map = {
number: 'number',
string: 'text',
boolean: 'checkbox'
}
return map[typeof value]
}
Inside the function, we can define a map where we map the type of the passed value to the input type. This means that the function will return a string based on the passed value. For example:
getInputType('string') -> 'text'
getInputType(123) -> 'number'
getInputType(true) -> 'checkbox'
Updating State
There's only one thing left for us to do, and that is to update the state of the table and call the callback
prop so we can handle the updated data. For this, introduce a new useState
hook and pass the rows
prop as a default value:
import { useState } from 'react'
export const EditableTable = ({ headings, rows, callback }) => {
const [state, setState] = useState(rows)
const getInputType = value => { ... }
return (
<table>
...
<tbody>
{state.map((row, rowIndex) => (
<tr key={rowIndex}>
{Object.entries(row).map((entry, columnIndex) => (
<td key={columnIndex}>
{columnIndex === 0
? entry[1]
: <input
type={getInputType(entry[1])}
checked={getInputType(entry[1]) === 'checkbox' ? entry[1] : null}
value={state[rowIndex][entry[0]]}
placeholder={headings?.[columnIndex]}
/>
}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
Replace rows.map
and the value
attribute on line:21 with state.map
. At this stage, we've injected the static rows
data into the state of the component. To also update the state whenever the input changes, add the following update function to the component and call it in the onChange
prop of the input
:
const update = (event, index, key) => {
const parseValue = {
'checkbox': e => e.target.checked,
'number': e => Number(e.target.value),
'text': e => e.target.value
}
rows[index][key] = parseValue[event.target.type](event)
setState([ ...rows ])
callback(state)
}
// Inside the return, add an onChange prop to the input:
<input
...
onChange={(e) => update(e, rowIndex, entry[0])}
/>
This function excepts three parameters: the event object, rowIndex
, and the key of the property to update. With these three parameters, we can update the correct property inside the state (which is an array of objects):
const rows = [
{
id: '6512fb5737b7fd3482d41d90',
name: 'Church Hamilton',
score: 1232,
age: 17,
isActive": false
}
]
rows[0]['name'] -> 'Church Hamilton'
To grab the correct value from the event object, we need a map called parseValue
. Each of its properties represents a different input type. Based on the input type, it returns its value using the correct reference:
checkbox
: Checkboxes values can be referenced throughe.target.checked
.number
: For numbers, we can typecast them from strings to numbers usingNumber(e.target.value)
.text
: For text-based inputs, we can usee.target.value
.
Notice that each property returns a function that we need to call with the event passed to the update
function: parseValue[event.target.type](event)
To get the type of input, we can use event.target.type
, which will be either checkbox
, number
or text
. Last but not least, we need to call the updater function, as well as the callback
prop and pass it the state.
setState([ ...rows ])
callback(state)
As objects are passed by reference in JavaScript, we need to use the spread operator to create a new array when calling setState
; otherwise, the state will not be updated.
Summary
In summary, creating editable dynamic tables in React can be achieved in less than 100 lines of code with a single useState hook. If you would like to expand upon this solution, check out how you can also export your HTML tables without any extra libraries.
If you would like to learn more about working in React, check out our React projects roadmap, a comprehensive guide that offers inspiration on what to build next and how to approach it. Thank you for reading through, happy coding!
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: