- Write
fetch
requests forGET
andPOST
- Initiate
fetch
requests with theuseEffect
hook - Initiate
fetch
requests from user events - Update state and trigger a re-render after receiving a response to the
fetch
request - Perform Create and Read (CRUD) actions on arrays in state
In labs and technical lessons up to this point, we've seen how to use fetch
in a React application for some common single-page application patterns, such as:
- Requesting data from a server when our application first loads
- Requesting data from a server on a button click
In both of those cases, our workflow in React follows a similar pattern:
- When X event occurs (our application loads, a user clicks a button)
- Make Y fetch request (GET)
- Update Z state (add all items to state)
In this codealong lesson, we'll get more practice following this pattern to
build out our next CRUD action - Create - to work with both our server-side data (the
database; in our case, the db.json
file) as well as our client-side data
(our React state). We'll be working on a shopping list application, using json-server
to create a RESTful API which we
can interact with from React by using fetch and HTTP requests.
To get started, let's install our dependencies:
$ npm install
Then, to run json-server
, we'll be using the server
script in the
package.json
file:
$ npm run server
This will run json-server
on http://localhost:4000.
Before moving ahead, open
http://localhost:4000/items in the browser and
familiarize yourself with the data. What are the important keys on each object?
Leave json-server
running. Open a new terminal, and run React with:
$ npm run dev
View the application in the browser at
http://localhost:5173. We don't have any data to
display yet, but eventually, we'll want to display the list of items from
json-server
in our application and be able to perform CRUD actions on them.
Take some time now to familiarize yourself with the components in the
src/components
folder. Which components are stateful and why? What does our
component hierarchy look like?
Once you've familiarized yourself with the starter code, let's start building out some features!
Our first goal will be to display a list of items from the server when the application first loads. Let's see how this goal fits into this common pattern for working with server-side data in React:
- When X event occurs (our application loads)
- Make Y fetch request (GET /items)
- Update Z state (add all items to state)
With that structure in mind, our first step is to identify which component triggers this event. In this case, the event isn't triggered by a user interacting with a specific DOM element. We want to initiate the fetch request without making our users click a button or anything like that.
So the event we're looking for is a side-effect of a component being
rendered. Which component? Well, we can make that determination by looking at
which state we're trying to update. In our case, it's the items
state which is
held in the ShoppingList
component.
We can call the useEffect
hook in the ShoppingList
component to initiate our
fetch
request. Let's start by using console.log
to ensure that our syntax is
correct, and that we're fetching data from the server:
// src/components/ShoppingList.js
// import useEffect
import { useEffect, useState } from "react";
// ...rest of imports
function ShoppingList() {
const [selectedCategory, setSelectedCategory] = useState("All");
const [items, setItems] = useState([]);
// Add useEffect hook
useEffect(() => {
fetch("http://localhost:4000/items")
.then(r => {
if (r.ok) {
return r.json()
} else {
console.log("fetch request failed")
}
})
.then(items => console.log(items))
.catch(error => console.log(error))
}, []);
// ...rest of component
}
Check your console in the browser — you should see an array of objects representing each item in our shopping list.
Now all that's left to do is to update state, so that React will re-render our
components and use the new data to display our shopping list. Our goal here is
to replace our current items
state, which is an empty array, with the new
array from the server:
// src/components/ShoppingList.js
function ShoppingList() {
const [selectedCategory, setSelectedCategory] = useState("All");
const [items, setItems] = useState([]);
// Update state by passing the array of items to setItems
useEffect(() => {
fetch("http://localhost:4000/items")
.then(r => {
if (r.ok) {
return r.json()
} else {
console.log("fetch request failed")
}
})
.then(items => setItems(items))
.catch(error => console.log(error))
}, []);
// ...rest of component
}
Check your work in the browser and make sure you see the list of items. Which component is responsible for rendering each item from the list of items in state?
To recap:
- When X event occurs
- Use the
useEffect
hook to trigger a side-effect in theShoppingList
component after the component first renders
- Use the
- Make Y fetch request
- Make a
GET
request to/items
to retrieve a list of items
- Make a
- Update Z state
- Replace our current list of items with the new list
Our next goal will be to add a new item to our database on the server when a user submits the form. Once again, let's plan out our steps:
- When X event occurs (a user submits the form)
- Make Y fetch request (POST /items with the new item data)
- Update Z state (add a new item to state)
To tackle the first step, we'll need to identify which component triggers the
event. In this case, the form in question is in the ItemForm
component.
Let's start by handling the form submit
event in this component and access
the data from the form inputs, which are saved in state:
// src/components/ItemForm.js
function ItemForm() {
const [name, setName] = useState("");
const [category, setCategory] = useState("Produce");
// Add function to handle submissions
function handleSubmit(e) {
e.preventDefault();
console.log("name:", name);
console.log("category:", category);
}
return (
// Set up the form to call handleSubmit when the form is submitted
<form className="NewItem" onSubmit={handleSubmit}>
{/** ...form inputs here */}
</form>
);
}
One step down, two to go! Next, we need to determine what data needs to be sent
to the server with our fetch
request. Our goal is to create a new item, and it
should have the same structure as other items on the server. So we'll need to
send an object that looks like this:
{
"name": "Yogurt",
"category": "Dairy",
"isInCart": false
}
Let's create this item in our handleSubmit
function using the data from the
form state:
// src/components/ItemForm.js
function handleSubmit(e) {
e.preventDefault();
const itemData = {
name: name,
category: category,
isInCart: false,
};
console.log(itemData);
}
Check your work in the browser again and make sure you are able to log an item
to the console that has the right key/value pairs. Now, on to the fetch
!
function handleSubmit(e) {
e.preventDefault();
const itemData = {
name: name,
category: category,
isInCart: false,
};
fetch("http://localhost:4000/items", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(itemData),
})
.then(r => {
if (r.ok) {
return r.json()
} else {
console.log("item failed to create")
}
})
.then(newItem => console.log(newItem))
.catch(error => console.log("error"))
}
Recall that to make a POST
request, we must provide additional options along
with the URL when calling fetch
: the method
(HTTP verb), the headers
(specifying that we are sending a JSON string in the request), and the body
(the stringified object we are sending). If you need a refresher on this syntax,
check out the [MDN article on Using Fetch][using fetch].
Try submitting the form once more. You should now see a new item logged to the
console that includes an id
attribute from the server. You can also verify the
object was persisted by refreshing the page in the browser and seeing the new
item at the bottom of the shopping list.
However, our goal isn't to make our users refresh the page to see their newly created item — we want it to show up as soon as it's been persisted. So we have one more step left: updating state.
For this final step, we need to consider:
- Which component owns the state that we're trying to update?
- How can we get the data from the
ItemForm
component to the component that owns state? - How do we correctly update state?
For the first question, we're trying to update state in the ShoppingList
component. Our goal is to display the new item in the list alongside the other
items, and this is the component that is responsible for that part of our
application. Since the ShoppingList
component is a parent component to the
ItemForm
component, we'll need to pass a callback function as a prop so
that the ItemForm
component can send the new item up to the ShoppingList
.
Let's add a handleAddItem
function to ShoppingList
, and pass a reference to
that function as a prop called onAddItem
to the ItemForm
:
// src/components/ShoppingList.js
function ShoppingList() {
const [selectedCategory, setSelectedCategory] = useState("All");
const [items, setItems] = useState([]);
useEffect(() => {
fetch("http://localhost:4000/items")
.then(r => {
if (r.ok) {
return r.json()
} else {
console.log("fetch request failed")
}
})
.then(items => setItems(items))
.catch(error => console.log(error))
}, []);
// add this function!
function handleAddItem(newItem) {
console.log("In ShoppingList:", newItem);
}
function handleCategoryChange(category) {
setSelectedCategory(category);
}
const itemsToDisplay = items.filter((item) => {
if (selectedCategory === "All") return true;
return item.category === selectedCategory;
});
return (
<div className="ShoppingList">
{/* add the onAddItem prop! */}
<ItemForm onAddItem={handleAddItem} />
<Filter
category={selectedCategory}
onCategoryChange={handleCategoryChange}
/>
<ul className="Items">
{itemsToDisplay.map((item) => (
<Item key={item.id} item={item} />
))}
</ul>
</div>
);
}
Then, we can use this prop in the ItemForm
to send the new item up to the
ShoppingList
when we receive a response from the server:
// src/components/ItemForm.js
// destructure the onAddItem prop
function ItemForm({ onAddItem }) {
const [name, setName] = useState("");
const [category, setCategory] = useState("Produce");
function handleSubmit(e) {
e.preventDefault();
const itemData = {
name: name,
category: category,
isInCart: false,
};
fetch("http://localhost:4000/items", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(itemData),
})
.then(r => {
if (r.ok) {
return r.json()
} else {
console.log("item failed to create")
}
})
// call the onAddItem prop with the newItem
.then(newItem => onAddItem(newItem))
.catch(error => console.log(error))
}
// ...rest of component
}
Check your work by submitting the form once more. You should now see the new
item logged to the console, this time from the ShoppingList
component. We're
getting close! For the last step, we need to call setState
with a new array
that has our new item at the end. Recall from our lessons on working with arrays
in state that we can use the spread operator to perform this action:
// src/components/ShoppingList.js
function handleAddItem(newItem) {
setItems([...items, newItem]);
}
Now each time a user submits the form, a new item will be added to our database and will also be added to our client-side state, so that the user will immediately see their item in the list.
Let's recap our steps here:
- When X event occurs
- When a user submits the
ItemForm
, handle the form submit event and access data from the form using state
- When a user submits the
- Make Y fetch request
- Make a
POST
request to/items
, passing the form data in the body of the request, and access the newly created item in the response
- Make a
- Update Z state
- Send the item from the fetch response to the
ShoppingList
component, and set state by creating a new array with our current items from state, plus the new item at the end
- Send the item from the fetch response to the
Synchronizing state between a client-side application and a server-side application is a challenging problem! Thankfully, the general steps to accomplish this in React are the same regardless of what kind of action we are performing:
- When X event occurs
- Make Y fetch request
- Update Z state
Keep these steps in mind any time you're working on a feature involving synchronizing client and server state. Once you have this general framework, you can apply the other things you've learned about React to each step, like handling events, updating state, and passing data between components.