For a recent project, my team and I wanted to build a table in our front end (using React) which would hold the data we pulled from the back end (using Ruby and Sinatra to build the database and API paths).
Another post (linked at the end) was extremely helpful in getting the data into our table, but we wanted to be able to edit the table data and have those edits persist using a PATCH request. Given how state works in React and how the React components were organized, this was a task. I thought it would be worth sharing how we incorporated edit capabilities.
Basic Outline of this Post - Jump to What You Need
- Phase 1: Rendering Data to the Table
- how to grab data using a GET fetch request
- how to push data into table rows
- Phase 2: Building an Edit Feature
- how to capture the data you want to edit
- how to create an EditForm component; show/hide as needed
- how to build the PATCH request and submit changes
A Simple Component Map
Here you can see how the component relationships are defined and what the primary responsibility is for each one. We'll use customers data as an example.
Phase 1: Rendering Data to the Table
Getting Your Data Using Fetch
Below you'll see the first step to take - getting your data using a fetch and saving it to state.
// App.js
import React, { useEffect, useState } from "react";
import Customers from "./Customers";
function App() {
// set state
const [customers, setCustomers] = useState([]);
// first data grab
useEffect(() => {
fetch("http://localhost:9292/customers") // your url may look different
.then(resp => resp.json())
.then(data => setCustomers(data)) // set data to state
}, []);
return (
<div>
{/* pass data down to the Customers component where we'll create the table*/}
<Customers customers={customers} />
</div>
);
}
export default App
Pushing Data Into Your Table
Once we have the data passed down to the Customers component, we need to create a table (HTML tag is table) into which the data gets pushed. Inside, you nest a table header (thead) where the column names go (th), as well as a table body (tbody) where the rows of data will go (tr).
// Customers.js
import React from 'react'
import Customer from './Customer'
function Customers({customers}) {
return (
<table>
<thead>
<tr>
<th>Customer ID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Modify Customer</th> // where you'll put the edit button
</tr>
</thead>
<tbody>
{/* iterate through the customers array and render a unique Customer component for each customer object in the array */}
{ customers.map(customer => <Customer key={customer.id} customer={customer} />) }
</tbody>
</table>
)
}
export default Customers
When formatting the Customer component, it is very important that you mind what kinds of HTML tags you use. Since Customer will manifest inside of a table, your top level HTML element needs to be a table row (tr).
// Customer.js
import React from 'react'
// deconstructed props
function Customer({customer:{id, name, email, phone} }) {
return (
<tr key={id}>
<td>{id}</td>
<td>{name}</td>
<td>{email}</td>
<td>{phone}</td>
<td><button>Edit</button></td>
</tr>
)
}
export default Customer
At this point, we successfully have data rendering in a table. Now for the fun part: creating an edit feature!
Phase 2: Building an Edit Feature
In order to edit our table data in a way that persists to the database, we have to build a few new functions across multiple components.
Our concerns include:
- capturing the customer data we want to edit
- creating the EditCustomer component; show/hide as needed
- passing captured customer data to the edit form
- building the PATCH request to submit the changes
- auto-updating the customer data in the browser
Due to the complexity of the task, I'll start with a simple description of each. Then, I'll include fully updated code blocks for each component. It is possible some of the code could be written more efficiently, but I tried to aim more for clarity.
Capture the customer's data
We'll need input fields in which to make changes, but it will need to happen outside of the table. Input tabs cause errors inside of a table, so we need to pass the data captured in Customer up to Customers where it will then get passed back down to the EditCustomer component.
Create the EditCustomer component; conditionally render
Keeping with React best practices, EditCustomer should be a separate component. The component will hold a form, as well as the PATCH request function.
You can conditionally render this component using state. See more about this skill in a previous post of mine here.
Pass captured customer to EditCustomer
Using state defined in Customers, you can save the customer data captured, then pass it down to the edit form. This is doubly helpful when submitting the PATCH request.
Build the PATCH request to ensure persistence
The PATCH request will live in the EditCustomer component and will submit the state that holds the captured customer's data (as well as any changes we've made to it).
Automatically render the updated data
We'll need a function in the same component where customer state is defined (in App), then pass that function down as props to the needed components and dependent functions. This function will automatically render the updated data to the page.
Component Code Blocks
App
import React, { useEffect, useState } from "react";
import Customers from "./Customers";
function App() {
// set state
const [customers, setCustomers] = useState([]);
// first data grab
useEffect(() => {
fetch("http://localhost:9292/customers")
.then((resp) => resp.json())
.then((data) => {
setCustomers(data)
});
}, []);
// update customers on page after edit
function onUpdateCustomer(updatedCustomer) {
const updatedCustomers = customers.map(
customer => {
if (customer.id === updatedCustomer.id) {
return updatedCustomer
} else {return customer}
}
)
setCustomers(updatedCustomers)
}
return (
<div>
<Customers
customers={customers}
onUpdateCustomer={onUpdateCustomer}
/>
</div>
);
}
export default App;
Customers
import React, {useState} from 'react'
import Customer from './Customer'
import EditCustomer from './EditCustomer'
function Customers({customers, onUpdateCustomer}) {
// state for conditional render of edit form
const [isEditing, setIsEditing] = useState(false);
// state for edit form inputs
const [editForm, setEditForm] = useState({
id: "",
name: "",
email: "",
phone: ""
})
// when PATCH request happens; auto-hides the form, pushes changes to display
function handleCustomerUpdate(updatedCustomer) {
setIsEditing(false);
onUpdateCustomer(updatedCustomer);
}
// capture user input in edit form inputs
function handleChange(e) {
setEditForm({
...editForm,
[e.target.name]: e.target.value
})
}
// needed logic for conditional rendering of the form - shows the customer you want when you want them, and hides it when you don't
function changeEditState(customer) {
if (customer.id === editForm.id) {
setIsEditing(isEditing => !isEditing) // hides the form
} else if (isEditing === false) {
setIsEditing(isEditing => !isEditing) // shows the form
}
}
// capture the customer you wish to edit, set to state
function captureEdit(clickedCustomer) {
let filtered = customers.filter(customer => customer.id === clickedCustomer.id)
setEditForm(filtered[0])
}
return (
<div>
{isEditing?
(<EditCustomer
editForm={editForm}
handleChange={handleChange}
handleCustomerUpdate={handleCustomerUpdate}
/>) : null}
<table>
<thead>
<tr>
<th>Customer ID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Modify Customer</th>
</tr>
</thead>
<tbody>
{ customers.map(customer =>
<Customer
key={customer.id}
customer={customer}
captureEdit={captureEdit}
changeEditState={changeEditState}
/>) }
</tbody>
</table>
</div>
)
}
export default Customers
Customer
import React from 'react'
function Customer({customer, customer:{id, name, email, phone}, captureEdit, changeEditState}) {
return (
<tr key={id}>
<td>{id}</td>
<td>{name}</td>
<td>{email}</td>
<td>{phone}</td>
<td>
<button
onClick={() => {
captureEdit(customer);
changeEditState(customer)
}}
>
Edit
</button>
</td>
</tr>
)
}
export default Customer
EditCustomer
import React from 'react'
function EditCustomer({ editForm, handleCustomerUpdate, handleChange }) {
let {id, name, email, phone} = editForm
// PATCH request; calls handleCustomerUpdate to push changes to the page
function handleEditForm(e) {
e.preventDefault();
fetch(`http://localhost:9292/customers/${id}`, {
method: "PATCH",
headers: {
"Content-Type" : "application/json"
},
body: JSON.stringify(editForm),
})
.then(resp => resp.json())
.then(updatedCustomer => {
handleCustomerUpdate(updatedCustomer)})
}
return (
<div>
<h4>Edit Customer</h4>
<form onSubmit={handleEditForm}>
<input type="text" name="name" value={name} onChange={handleChange}/>
<input type="text" name="email" value={email} onChange={handleChange}/>
<input type="text" name="phone" value={phone} onChange={handleChange}/>
<button type="submit">Submit Changes</button>
</form>
</div>
)
}
export default EditCustomer
Conclusion
And there we have it! You can now push data into a table when using React, as well as edit the data in the page.
Reader feedback: Was there a better way? Did I put state in the wrong place? Have some advice to pass down to an early career engineer? Share out in the discussion below!
Did you find this tutorial helpful? Like and follow for more great posts to come!
Top comments (2)
Hi, thank you so much for the tutorial.
I'm having the following issue with the PATCH request:
Web console:
PATCH localhost:5000/customers/1 404 (Not Found)
VM1117:1 Uncaught (in promise) SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
This is how the localhost json file looks like:
[{"CUSTOMER_ID":1,"NAME":"test","EMAIL":"test","PHONE":1111}]
Hello, my guess is that the problem is the brackets around the object. [{"CUSTOMER_ID":1,"NAME":"test","EMAIL":"test","PHONE":1111}] should be {"CUSTOMER_ID":1,"NAME":"test","EMAIL":"test","PHONE":1111} in order to read as a json file.