33 | );
34 | }
35 |
36 | export default ShoppingList;
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hooks-fetch-crud-code-along",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@learn-co-curriculum/jest-learn-reporter": "^0.1.0",
7 | "@testing-library/jest-dom": "^5.11.4",
8 | "@testing-library/react": "^11.1.0",
9 | "@testing-library/user-event": "^12.1.10",
10 | "json-server": "^0.16.3",
11 | "mocha": "^8.2.1",
12 | "msw": "^0.24.2",
13 | "react": "^17.0.2",
14 | "react-dom": "^17.0.2",
15 | "react-scripts": "^5.0.1",
16 | "whatwg-fetch": "^3.6.2"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "echo \".results.json\" && react-scripts test --reporters=@learn-co-curriculum/jest-learn-reporter --reporters=default --watchAll",
22 | "eject": "react-scripts eject",
23 | "server": "json-server --watch db.json --port=4000"
24 | },
25 | "eslintConfig": {
26 | "extends": [
27 | "react-app",
28 | "react-app/jest"
29 | ]
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/mocks/handlers.js:
--------------------------------------------------------------------------------
1 | import { rest } from "msw";
2 | import { data } from "./data";
3 |
4 | let items = [...data];
5 | let id = items[items.length - 1].id;
6 |
7 | export function resetData() {
8 | items = [...data];
9 | id = items[items.length - 1].id;
10 | }
11 |
12 | export const handlers = [
13 | rest.get("http://localhost:4000/items", (req, res, ctx) => {
14 | return res(ctx.json(items));
15 | }),
16 | rest.post("http://localhost:4000/items", (req, res, ctx) => {
17 | id++;
18 | const item = { id, ...req.body };
19 | items.push(item);
20 | return res(ctx.json(item));
21 | }),
22 | rest.delete("http://localhost:4000/items/:id", (req, res, ctx) => {
23 | const { id } = req.params;
24 | if (isNaN(parseInt(id))) {
25 | return res(ctx.status(404), ctx.json({ message: "Invalid ID" }));
26 | }
27 | items = items.filter((q) => q.id !== parseInt(id));
28 | return res(ctx.json({}));
29 | }),
30 | rest.patch("http://localhost:4000/items/:id", (req, res, ctx) => {
31 | const { id } = req.params;
32 | if (isNaN(parseInt(id))) {
33 | return res(ctx.status(404), ctx.json({ message: "Invalid ID" }));
34 | }
35 | const itemIndex = items.findIndex((item) => item.id === parseInt(id));
36 | items[itemIndex] = { ...items[itemIndex], ...req.body };
37 | return res(ctx.json(items[itemIndex]));
38 | }),
39 | ];
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # Learn.co Educational Content License
2 |
3 | Copyright (c) 2021 Flatiron School, Inc
4 |
5 | The Flatiron School, Inc. owns this Educational Content. However, the Flatiron
6 | School supports the development and availability of educational materials in the
7 | public domain. Therefore, the Flatiron School grants Users of the Flatiron
8 | Educational Content set forth in this repository certain rights to reuse, build
9 | upon and share such Educational Content subject to the terms of the Educational
10 | Content License set forth [here](http://learn.co/content-license)
11 | (http://learn.co/content-license). You must read carefully the terms and
12 | conditions contained in the Educational Content License as such terms govern
13 | access to and use of the Educational Content.
14 |
15 | Flatiron School is willing to allow you access to and use of the Educational
16 | Content only on the condition that you accept all of the terms and conditions
17 | contained in the Educational Content License set forth
18 | [here](http://learn.co/content-license) (http://learn.co/content-license). By
19 | accessing and/or using the Educational Content, you are agreeing to all of the
20 | terms and conditions contained in the Educational Content License. If you do not
21 | agree to any or all of the terms of the Educational Content License, you are
22 | prohibited from accessing, reviewing or using in any way the Educational
23 | Content.
24 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Learn.co Curriculum
2 |
3 | We're really exited that you're about to contribute to the
4 | [open curriculum](https://learn.co/content-license) on
5 | [Learn.co](https://learn.co). If this is your first time contributing, please
6 | continue reading to learn how to make the most meaningful and useful impact
7 | possible.
8 |
9 | ## Raising an Issue to Encourage a Contribution
10 |
11 | If you notice a problem with the curriculum that you believe needs improvement
12 | but you're unable to make the change yourself, you should raise a Github issue
13 | containing a clear description of the problem. Include relevant snippets of
14 | the content and/or screenshots if applicable. Curriculum owners regularly review
15 | issue lists and your issue will be prioritized and addressed as appropriate.
16 |
17 | ## Submitting a Pull Request to Suggest an Improvement
18 |
19 | If you see an opportunity for improvement and can make the change yourself go
20 | ahead and use a typical git workflow to make it happen:
21 |
22 | - Fork this curriculum repository
23 | - Make the change on your fork, with descriptive commits in the standard format
24 | - Open a Pull Request against this repo
25 |
26 | A curriculum owner will review your change and approve or comment on it in due
27 | course.
28 |
29 | ## Why Contribute?
30 |
31 | Curriculum on Learn is publicly and freely available under Learn's
32 | [Educational Content License](https://learn.co/content-license). By
33 | embracing an open-source contribution model, our goal is for the curriculum
34 | on Learn to become, in time, the best educational content the world has
35 | ever seen.
36 |
37 | We need help from the community of Learners to maintain and improve the
38 | educational content. Everything from fixing typos, to correcting
39 | out-dated information, to improving exposition, to adding better examples,
40 | to fixing tests—all contributions to making the curriculum more effective are
41 | welcome.
42 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --magenta: rgb(210, 51, 210);
3 | --cyan: rgb(98, 230, 230);
4 | --yellow: rgb(237, 237, 32);
5 | --font-color: rgb(33, 33, 33);
6 | --font-color-light: rgb(155, 155, 155);
7 | --background: rgb(255, 255, 255);
8 | --background-light: rgb(230, 230, 230);
9 | }
10 |
11 | * {
12 | margin: 0;
13 | padding: 0;
14 | box-sizing: border-box;
15 | }
16 |
17 | body {
18 | background: rgb(231, 231, 231);
19 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
20 | font-size: 18px;
21 | }
22 |
23 | button,
24 | select,
25 | input {
26 | border: none;
27 | padding: 0.5rem;
28 | border-radius: 4px;
29 | font-size: 1rem;
30 | background: var(--background-light);
31 | }
32 |
33 | span[role="image"] {
34 | color: transparent;
35 | text-shadow: 0 0 0 black;
36 | }
37 |
38 | .App.dark {
39 | --font-color: rgb(255, 255, 255);
40 | --background: rgb(50, 50, 50);
41 | --background-light: rgb(101, 101, 101);
42 | transition: all 0.3s;
43 | }
44 |
45 | .App {
46 | margin: 2rem auto;
47 | padding: 1rem 2rem;
48 | max-width: 700px;
49 | background: var(--background);
50 | color: var(--font-color);
51 | box-shadow: 4px 4px 10px rgba(33, 33, 33, 0.2);
52 | border-radius: 10px;
53 | }
54 |
55 | header {
56 | display: flex;
57 | justify-content: space-between;
58 | align-items: center;
59 | border-top-left-radius: 10px;
60 | border-top-right-radius: 10px;
61 | }
62 |
63 | header h2 {
64 | padding: 1rem 0;
65 | }
66 |
67 | header button {
68 | background: var(--yellow);
69 | }
70 |
71 | .Filter {
72 | display: flex;
73 | justify-content: space-between;
74 | }
75 |
76 | .Filter select,
77 | .Filter input {
78 | width: 45%;
79 | margin: 1rem 0;
80 | background: var(--cyan);
81 | }
82 |
83 | .Items {
84 | list-style: none;
85 | }
86 |
87 | .Items li {
88 | margin: 0.5rem 0;
89 | padding: 0.25rem;
90 | display: grid;
91 | grid-template-columns: 1fr 1fr 160px 100px;
92 | column-gap: 1rem;
93 | align-items: center;
94 | border-radius: 4px;
95 | }
96 |
97 | .Items li.in-cart {
98 | background: var(--background-light);
99 | }
100 |
101 | .Items li.in-cart span {
102 | text-decoration: line-through;
103 | }
104 |
105 | .Items button.add {
106 | background-color: var(--yellow);
107 | }
108 |
109 | .Items button.remove {
110 | background-color: var(--magenta);
111 | color: white;
112 | }
113 |
114 | .Items li .category {
115 | justify-self: end;
116 | color: var(--font-color-light);
117 | font-size: 0.8em;
118 | }
119 |
120 | .NewItem {
121 | border-bottom: 2px solid var(--background-light);
122 | border-top: 2px solid var(--background-light);
123 | padding: 1rem 0;
124 | display: flex;
125 | justify-content: space-between;
126 | align-items: flex-end;
127 | gap: 1rem;
128 | }
129 |
130 | .NewItem button {
131 | background: var(--yellow);
132 | }
133 |
134 | .NewItem label {
135 | font-size: 0.8em;
136 | display: flex;
137 | flex-direction: column;
138 | flex: 1;
139 | }
140 |
--------------------------------------------------------------------------------
/src/__tests__/ShoppingList.test.js:
--------------------------------------------------------------------------------
1 | import "whatwg-fetch";
2 | import "@testing-library/jest-dom";
3 | import {
4 | render,
5 | screen,
6 | fireEvent,
7 | waitForElementToBeRemoved,
8 | } from "@testing-library/react";
9 | import { resetData } from "../mocks/handlers";
10 | import { server } from "../mocks/server";
11 | import ShoppingList from "../components/ShoppingList";
12 |
13 | beforeAll(() => server.listen());
14 | afterEach(() => {
15 | server.resetHandlers();
16 | resetData();
17 | });
18 | afterAll(() => server.close());
19 |
20 | test("displays all the items from the server after the initial render", async () => {
21 | render();
22 |
23 | const yogurt = await screen.findByText(/Yogurt/);
24 | expect(yogurt).toBeInTheDocument();
25 |
26 | const pomegranate = await screen.findByText(/Pomegranate/);
27 | expect(pomegranate).toBeInTheDocument();
28 |
29 | const lettuce = await screen.findByText(/Lettuce/);
30 | expect(lettuce).toBeInTheDocument();
31 | });
32 |
33 | test("adds a new item to the list when the ItemForm is submitted", async () => {
34 | const { rerender } = render();
35 |
36 | const dessertCount = screen.queryAllByText(/Dessert/).length;
37 |
38 | fireEvent.change(screen.queryByLabelText(/Name/), {
39 | target: { value: "Ice Cream" },
40 | });
41 |
42 | fireEvent.change(screen.queryByLabelText(/Category/), {
43 | target: { value: "Dessert" },
44 | });
45 |
46 | fireEvent.submit(screen.queryByText(/Add to List/));
47 |
48 | const iceCream = await screen.findByText(/Ice Cream/);
49 | expect(iceCream).toBeInTheDocument();
50 |
51 | const desserts = await screen.findAllByText(/Dessert/);
52 | expect(desserts.length).toBe(dessertCount + 1);
53 |
54 | // Rerender the component to ensure the item was persisted
55 | rerender();
56 |
57 | const rerenderedIceCream = await screen.findByText(/Ice Cream/);
58 | expect(rerenderedIceCream).toBeInTheDocument();
59 | });
60 |
61 | test("updates the isInCart status of an item when the Add/Remove from Cart button is clicked", async () => {
62 | const { rerender } = render();
63 |
64 | const addButtons = await screen.findAllByText(/Add to Cart/);
65 |
66 | expect(addButtons.length).toBe(3);
67 | expect(screen.queryByText(/Remove From Cart/)).not.toBeInTheDocument();
68 |
69 | fireEvent.click(addButtons[0]);
70 |
71 | const removeButton = await screen.findByText(/Remove From Cart/);
72 | expect(removeButton).toBeInTheDocument();
73 |
74 | // Rerender the component to ensure the item was persisted
75 | rerender();
76 |
77 | const rerenderedAddButtons = await screen.findAllByText(/Add to Cart/);
78 | const rerenderedRemoveButtons = await screen.findAllByText(
79 | /Remove From Cart/
80 | );
81 |
82 | expect(rerenderedAddButtons.length).toBe(2);
83 | expect(rerenderedRemoveButtons.length).toBe(1);
84 | });
85 |
86 | test("removes an item from the list when the delete button is clicked", async () => {
87 | const { rerender } = render();
88 |
89 | const yogurt = await screen.findByText(/Yogurt/);
90 | expect(yogurt).toBeInTheDocument();
91 |
92 | const deleteButtons = await screen.findAllByText(/Delete/);
93 | fireEvent.click(deleteButtons[0]);
94 |
95 | await waitForElementToBeRemoved(() => screen.queryByText(/Yogurt/));
96 |
97 | // Rerender the component to ensure the item was persisted
98 | rerender();
99 |
100 | const rerenderedDeleteButtons = await screen.findAllByText(/Delete/);
101 |
102 | expect(rerenderedDeleteButtons.length).toBe(2);
103 | expect(screen.queryByText(/Yogurt/)).not.toBeInTheDocument();
104 | });
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Fetch CRUD Codealong
2 |
3 | ## Learning Goals
4 |
5 | - Write `fetch` requests for `GET`, `POST`, `PATCH`, and `DELETE`
6 | - Initiate `fetch` requests with the `useEffect` hook
7 | - Initiate `fetch` requests from user events
8 | - Update state and trigger a re-render after receiving a response to the
9 | `fetch` request
10 | - Perform CRUD actions on arrays in state
11 |
12 | ## Introduction
13 |
14 | Up to this point, we've seen how to use `fetch` in a React application for some
15 | common single-page application patterns, such as:
16 |
17 | - Requesting data from a server when our application first loads
18 | - Persisting data to a server when a user submits a form
19 |
20 | In both of those cases, our workflow in React follows a similar pattern:
21 |
22 | - When X event occurs (_our application loads_, _a user submits a form_)
23 | - Make Y fetch request (_GET_, _POST_)
24 | - Update Z state (_add all items to state_, _add a single item to state_)
25 |
26 | In this codealong lesson, we'll get more practice following this pattern to
27 | build out all four CRUD actions to work with both our **server-side** data (the
28 | database; in our case, the `db.json` file) as well as our **client-side** data
29 | (our React state). We'll be revisiting the shopping list application from the
30 | previous module, this time using `json-server` to create a RESTful API which we
31 | can interact with from React.
32 |
33 | ## Instructions
34 |
35 | To get started, let's install our dependencies:
36 |
37 | ```console
38 | $ npm install
39 | ```
40 |
41 | Then, to run `json-server`, we'll be using the `server` script in the
42 | `package.json` file:
43 |
44 | ```console
45 | $ npm run server
46 | ```
47 |
48 | This will run `json-server` on [http://localhost:4000](http://localhost:4000).
49 | Before moving ahead, open
50 | [http://localhost:4000/items](http://localhost:4000/items) in the browser and
51 | familiarize yourself with the data. What are the important keys on each object?
52 |
53 | Leave `json-server` running. Open a new terminal, and run React with:
54 |
55 | ```console
56 | $ npm start
57 | ```
58 |
59 | View the application in the browser at
60 | [http://localhost:3000](http://localhost:3000). We don't have any data to
61 | display yet, but eventually, we'll want to display the list of items from
62 | `json-server` in our application and be able to perform CRUD actions on them.
63 |
64 | Take some time now to familiarize yourself with the components in the
65 | `src/components` folder. Which components are stateful and why? What does our
66 | component hierarchy look like?
67 |
68 | Once you've familiarized yourself with the starter code, let's start building
69 | out some features!
70 |
71 | ### Displaying Items
72 |
73 | Our first goal will be to display a list of items from the server when the
74 | application first loads. Let's see how this goal fits into this common pattern
75 | for working with server-side data in React:
76 |
77 | - When X event occurs (_our application loads_)
78 | - Make Y fetch request (_GET /items_)
79 | - Update Z state (_add all items to state_)
80 |
81 | With that structure in mind, our first step is to **identify which component
82 | triggers this event**. In this case, the event isn't triggered by a user
83 | interacting with a specific DOM element. We want to initiate the fetch request
84 | without making our users click a button or anything like that.
85 |
86 | So the event we're looking for is a **side-effect** of a component being
87 | rendered. Which component? Well, we can make that determination by looking at
88 | which state we're trying to update. In our case, it's the `items` state which is
89 | held in the `ShoppingList` component.
90 |
91 | We can call the `useEffect` hook in the `ShoppingList` component to initiate our
92 | `fetch` request. Let's start by using `console.log` to ensure that our syntax is
93 | correct, and that we're fetching data from the server:
94 |
95 | ```jsx
96 | // src/components/ShoppingList.js
97 |
98 | // import useEffect
99 | import React, { useEffect, useState } from "react";
100 | // ...rest of imports
101 |
102 | function ShoppingList() {
103 | const [selectedCategory, setSelectedCategory] = useState("All");
104 | const [items, setItems] = useState([]);
105 |
106 | // Add useEffect hook
107 | useEffect(() => {
108 | fetch("http://localhost:4000/items")
109 | .then((r) => r.json())
110 | .then((items) => console.log(items));
111 | }, []);
112 |
113 | // ...rest of component
114 | }
115 | ```
116 |
117 | Check your console in the browser — you should see an **array of objects**
118 | representing each item in our shopping list.
119 |
120 | Now all that's left to do is to update state, so that React will re-render our
121 | components and use the new data to display our shopping list. Our goal here is
122 | to replace our current `items` state, which is an empty array, with the new
123 | array from the server:
124 |
125 | ```jsx
126 | // src/components/ShoppingList.js
127 |
128 | function ShoppingList() {
129 | const [selectedCategory, setSelectedCategory] = useState("All");
130 | const [items, setItems] = useState([]);
131 |
132 | // Update state by passing the array of items to setItems
133 | useEffect(() => {
134 | fetch("http://localhost:4000/items")
135 | .then((r) => r.json())
136 | .then((items) => setItems(items));
137 | }, []);
138 |
139 | // ...rest of component
140 | }
141 | ```
142 |
143 | Check your work in the browser and make sure you see the list of items. Which
144 | component is responsible for rendering each item from the list of items in
145 | state?
146 |
147 | To recap:
148 |
149 | - When X event occurs
150 | - Use the `useEffect` hook to trigger a side-effect in the `ShoppingList`
151 | component after the component first renders
152 | - Make Y fetch request
153 | - Make a `GET` request to `/items` to retrieve a list of items
154 | - Update Z state
155 | - Replace our current list of items with the new list
156 |
157 | ### Creating Items
158 |
159 | Our next goal will be to add a new item to our database on the server when a
160 | user submits the form. Once again, let's plan out our steps:
161 |
162 | - When X event occurs (_a user submits the form_)
163 | - Make Y fetch request (_POST /items with the new item data_)
164 | - Update Z state (_add a new item to state_)
165 |
166 | To tackle the first step, we'll need to **identify which component triggers the
167 | event**. In this case, the form in question is in the `ItemForm` component.
168 | Let's start by handling the form `submit` event in this component and access
169 | the data from the form inputs, which are saved in state:
170 |
171 | ```jsx
172 | // src/components/ItemForm.js
173 |
174 | function ItemForm() {
175 | const [name, setName] = useState("");
176 | const [category, setCategory] = useState("Produce");
177 |
178 | // Add function to handle submissions
179 | function handleSubmit(e) {
180 | e.preventDefault();
181 | console.log("name:", name);
182 | console.log("category:", category);
183 | }
184 |
185 | return (
186 | // Set up the form to call handleSubmit when the form is submitted
187 |
190 | );
191 | }
192 | ```
193 |
194 | One step down, two to go! Next, we need to determine what data needs to be sent
195 | to the server with our `fetch` request. Our goal is to create a new item, and it
196 | should have the same structure as other items on the server. So we'll need to
197 | send an object that looks like this:
198 |
199 | ```json
200 | {
201 | "name": "Yogurt",
202 | "category": "Dairy",
203 | "isInCart": false
204 | }
205 | ```
206 |
207 | Let's create this item in our `handleSubmit` function using the data from the
208 | form state:
209 |
210 | ```js
211 | // src/components/ItemForm.js
212 |
213 | function handleSubmit(e) {
214 | e.preventDefault();
215 | const itemData = {
216 | name: name,
217 | category: category,
218 | isInCart: false,
219 | };
220 | console.log(itemData);
221 | }
222 | ```
223 |
224 | Check your work in the browser again and make sure you are able to log an item
225 | to the console that has the right key/value pairs. Now, on to the `fetch`!
226 |
227 | ```js
228 | function handleSubmit(e) {
229 | e.preventDefault();
230 | const itemData = {
231 | name: name,
232 | category: category,
233 | isInCart: false,
234 | };
235 | fetch("http://localhost:4000/items", {
236 | method: "POST",
237 | headers: {
238 | "Content-Type": "application/json",
239 | },
240 | body: JSON.stringify(itemData),
241 | })
242 | .then((r) => r.json())
243 | .then((newItem) => console.log(newItem));
244 | }
245 | ```
246 |
247 | Recall that to make a `POST` request, we must provide additional options along
248 | with the URL when calling `fetch`: the `method` (HTTP verb), the `headers`
249 | (specifying that we are sending a JSON string in the request), and the `body`
250 | (the stringified object we are sending). If you need a refresher on this syntax,
251 | check out the [MDN article on Using Fetch][using fetch].
252 |
253 | Try submitting the form once more. You should now see a new item logged to the
254 | console that includes an `id` attribute from the server. You can also verify the
255 | object was persisted by refreshing the page in the browser and seeing the new
256 | item at the bottom of the shopping list.
257 |
258 | However, our goal isn't to make our users refresh the page to see their newly
259 | created item — we want it to show up as soon as it's been persisted. So we have
260 | one more step left: **updating state**.
261 |
262 | For this final step, we need to consider:
263 |
264 | - Which component owns the state that we're trying to update?
265 | - How can we get the data from the `ItemForm` component to the component that
266 | owns state?
267 | - How do we correctly update state?
268 |
269 | For the first question, we're trying to update state in the `ShoppingList`
270 | component. Our goal is to display the new item in the list alongside the other
271 | items, and this is the component that is responsible for that part of our
272 | application. Since the `ShoppingList` component is a **parent** component to the
273 | `ItemForm` component, we'll need to **pass a callback function as a prop** so
274 | that the `ItemForm` component can send the new item up to the `ShoppingList`.
275 |
276 | Let's add a `handleAddItem` function to `ShoppingList`, and pass a reference to
277 | that function as a prop called `onAddItem` to the `ItemForm`:
278 |
279 | ```jsx
280 | // src/components/ShoppingList.js
281 |
282 | function ShoppingList() {
283 | const [selectedCategory, setSelectedCategory] = useState("All");
284 | const [items, setItems] = useState([]);
285 |
286 | useEffect(() => {
287 | fetch("http://localhost:4000/items")
288 | .then((r) => r.json())
289 | .then((items) => setItems(items));
290 | }, []);
291 |
292 | // add this function!
293 | function handleAddItem(newItem) {
294 | console.log("In ShoppingList:", newItem);
295 | }
296 |
297 | function handleCategoryChange(category) {
298 | setSelectedCategory(category);
299 | }
300 |
301 | const itemsToDisplay = items.filter((item) => {
302 | if (selectedCategory === "All") return true;
303 |
304 | return item.category === selectedCategory;
305 | });
306 |
307 | return (
308 |
321 | );
322 | }
323 | ```
324 |
325 | Then, we can use this prop in the `ItemForm` to send the new item **up** to the
326 | `ShoppingList` when we receive a response from the server:
327 |
328 | ```jsx
329 | // src/components/ItemForm.js
330 |
331 | // destructure the onAddItem prop
332 | function ItemForm({ onAddItem }) {
333 | const [name, setName] = useState("");
334 | const [category, setCategory] = useState("Produce");
335 |
336 | function handleSubmit(e) {
337 | e.preventDefault();
338 | const itemData = {
339 | name: name,
340 | category: category,
341 | isInCart: false,
342 | };
343 | fetch("http://localhost:4000/items", {
344 | method: "POST",
345 | headers: {
346 | "Content-Type": "application/json",
347 | },
348 | body: JSON.stringify(itemData),
349 | })
350 | .then((r) => r.json())
351 | // call the onAddItem prop with the newItem
352 | .then((newItem) => onAddItem(newItem));
353 | }
354 |
355 | // ...rest of component
356 | }
357 | ```
358 |
359 | Check your work by submitting the form once more. You should now see the new
360 | item logged to the console, this time from the `ShoppingList` component. We're
361 | getting close! For the last step, we need to call `setState` with a new array
362 | that has our new item at the end. Recall from our lessons on working with arrays
363 | in state that we can use the spread operator to perform this action:
364 |
365 | ```js
366 | // src/components/ShoppingList.js
367 |
368 | function handleAddItem(newItem) {
369 | setItems([...items, newItem]);
370 | }
371 | ```
372 |
373 | Now each time a user submits the form, a new item will be added to our database
374 | and will also be added to our client-side state, so that the user will
375 | immediately see their item in the list.
376 |
377 | Let's recap our steps here:
378 |
379 | - When X event occurs
380 | - When a user submits the `ItemForm`, handle the form submit event and access
381 | data from the form using state
382 | - Make Y fetch request
383 | - Make a `POST` request to `/items`, passing the form data in the **body** of
384 | the request, and access the newly created item in the response
385 | - Update Z state
386 | - Send the item from the fetch response to the `ShoppingList` component, and
387 | set state by creating a new array with our current items from state, plus
388 | the new item at the end
389 |
390 | **Phew!** This is a good time to **take a break** before proceeding — we've got
391 | a few more steps to cover. Once you're ready and recharged, we'll dive back in.
392 |
393 | ### Updating Items
394 |
395 | For our update action, we'd like to give our users the ability to keep track of which
396 | items from their shopping list they've added to their cart. Once more, we can outline
397 | the basic steps for this action like so:
398 |
399 | - When X event occurs (_a user clicks the Add to Cart button_)
400 | - Make Y fetch request (_PATCH /items_)
401 | - Update Z state (_update the `isInCart` status for the item_)
402 |
403 | From here, we'll again need to **identify which component triggers the event**.
404 | Can you find where the "Add to Cart" button lives in our code? Yep! It's in the
405 | `Item` component. We'll start by adding an event handler for clicks on the
406 | button:
407 |
408 | ```jsx
409 | // src/components/Item.js
410 |
411 | function Item({ item }) {
412 | // Add function to handle button click
413 | function handleAddToCartClick() {
414 | console.log("clicked item:", item);
415 | }
416 |
417 | return (
418 |
430 | );
431 | }
432 | ```
433 |
434 | Check your work by clicking this button for different items — you should see
435 | each item logged to the console. We can access the `item` variable in the
436 | `handleAddToCartClick` function thanks to JavaScript's scope rules.
437 |
438 | Next, let's write out our `PATCH` request:
439 |
440 | ```js
441 | // src/components/Item.js
442 |
443 | function handleAddToCartClick() {
444 | // add fetch request
445 | fetch(`http://localhost:4000/items/${item.id}`, {
446 | method: "PATCH",
447 | headers: {
448 | "Content-Type": "application/json",
449 | },
450 | body: JSON.stringify({
451 | isInCart: !item.isInCart,
452 | }),
453 | })
454 | .then((r) => r.json())
455 | .then((updatedItem) => console.log(updatedItem));
456 | }
457 | ```
458 |
459 | Just like with our `POST` request, we need to specify the `method`, `headers`,
460 | and `body` options for our `PATCH` request as well. We also need to include the
461 | item's ID in the URL so that our server knows which item we're trying to update.
462 |
463 | Since our goal is to let users add or remove items from their cart, we need to
464 | toggle the `isInCart` property of the item on the server (and eventually
465 | client-side as well). So in the body of the request, we send an object with the
466 | key we are updating, along with the new value.
467 |
468 | Check your work by clicking the "Add to Cart" button and see if you receive an
469 | updated item in the response from the server. Then, refresh the page to verify
470 | that the change was persisted server-side.
471 |
472 | Two steps done! Once more, our last step is to update state. Let's think this
473 | through. Right now, what state determines whether or not the item is in our
474 | cart? Where does that state live? Do we need to add new state?
475 |
476 | A reasonable thought here is that we might need to add new state to the `Item`
477 | component to keep track of whether or not the item is in the cart. While we
478 | could theoretically make this approach work, it would be an anti-pattern: we'd
479 | be **duplicating state**, which makes our components harder to work with and
480 | more prone to bugs.
481 |
482 | We already have state in our `ShoppingList` component that tells us which items
483 | are in the cart. So instead of creating new state, our goal is to call
484 | `setItems` in the `ShoppingList` component with a new list of items, where the
485 | `isInCart` state of our updated item matches its state on the server.
486 |
487 | Just like with our `ItemForm` deliverable, let's start by creating a callback
488 | function in the `ShoppingList` component and passing it as a prop to the `Item`
489 | component:
490 |
491 | ```jsx
492 | // src/components/ShoppingList.js
493 |
494 | function ShoppingList() {
495 | const [selectedCategory, setSelectedCategory] = useState("All");
496 | const [items, setItems] = useState([]);
497 |
498 | useEffect(() => {
499 | fetch("http://localhost:4000/items")
500 | .then((r) => r.json())
501 | .then((items) => setItems(items));
502 | }, []);
503 |
504 | // add this callback function
505 | function handleUpdateItem(updatedItem) {
506 | console.log("In ShoppingCart:", updatedItem);
507 | }
508 |
509 | // ...rest of component
510 |
511 | return (
512 |
513 |
514 |
518 |
519 | {/* pass it as a prop to Item */}
520 | {itemsToDisplay.map((item) => (
521 |
522 | ))}
523 |
524 |
525 | );
526 | }
527 | ```
528 |
529 | In the `Item` component, we can destructure the `onUpdateItem` prop and call it
530 | when we have the updated item response from the server:
531 |
532 | ```jsx
533 | // src/components/Item.js
534 |
535 | // Destructure the onUpdateItem prop
536 | function Item({ item, onUpdateItem }) {
537 | function handleAddToCartClick() {
538 | // Call onUpdateItem, passing the data returned from the fetch request
539 | fetch(`http://localhost:4000/items/${item.id}`, {
540 | method: "PATCH",
541 | headers: {
542 | "Content-Type": "application/json",
543 | },
544 | body: JSON.stringify({
545 | isInCart: !item.isInCart,
546 | }),
547 | })
548 | .then((r) => r.json())
549 | .then((updatedItem) => onUpdateItem(updatedItem));
550 | }
551 | // ... rest of component
552 | }
553 | ```
554 |
555 | Check your work by clicking the button once more. You should now see the updated
556 | item logged to the console, this time from the `ShoppingList` component.
557 |
558 | As a last step, we need to call `setState` with a new array that replaces one
559 | item with the new updated item from the server. Recall from our lessons on
560 | working with arrays in state that we can use `.map` to help create this new
561 | array:
562 |
563 | ```js
564 | // src/components/ShoppingList.js
565 |
566 | function handleUpdateItem(updatedItem) {
567 | const updatedItems = items.map((item) => {
568 | if (item.id === updatedItem.id) {
569 | return updatedItem;
570 | } else {
571 | return item;
572 | }
573 | });
574 | setItems(updatedItems);
575 | }
576 | ```
577 |
578 | Clicking the button should now toggle the `isInCart` property of any item in the
579 | list on the server as well as in our React state! To recap:
580 |
581 | - When X event occurs
582 | - When a user clicks the Add to Cart button, handle the button click
583 | - Make Y fetch request
584 | - Make a `PATCH` request to `/items/:id`, using the clicked item's data for
585 | the ID and body of the request, and access the updated item in the response
586 | - Update Z state
587 | - Send the item from the fetch response to the `ShoppingList` component, and
588 | set state by creating a new array which contains the updated item in place
589 | of the old item
590 |
591 | ### Deleting Items
592 |
593 | Last one! For our delete action, we'd like to give our users the ability to
594 | remove items from their shopping list:
595 |
596 | - When X event occurs (_a user clicks the DELETE button_)
597 | - Make Y fetch request (_DELETE /items_)
598 | - Update Z state (_remove the item from the list_)
599 |
600 | From here, we'll again need to **identify which component triggers the event**.
601 | Our delete button is in the `Item` component, so we'll start by adding an event
602 | handler for clicks on the button:
603 |
604 | ```jsx
605 | // src/components/Item.js
606 |
607 | function Item({ item, onUpdateItem }) {
608 | // ...rest of component
609 |
610 | function handleDeleteClick() {
611 | console.log(item);
612 | }
613 |
614 | return (
615 |
623 | );
624 | }
625 | ```
626 |
627 | This step should feel similar to our approach for the update action. Next, let's
628 | write out our `DELETE` request:
629 |
630 | ```js
631 | // src/components/Item.js
632 |
633 | function handleDeleteClick() {
634 | fetch(`http://localhost:4000/items/${item.id}`, {
635 | method: "DELETE",
636 | })
637 | .then((r) => r.json())
638 | .then(() => console.log("deleted!"));
639 | }
640 | ```
641 |
642 | Note that for a `DELETE` request, we must include the ID of the item we're
643 | deleting in the URL. We only need the `method` option — no `body` or `headers`
644 | are needed since we don't have any additional data to send besides the ID.
645 |
646 | You can verify that the item was successfully deleted by clicking the button,
647 | checking that the console message of `"deleted!"` appears, and refreshing the
648 | page to check that the item is no longer on the list.
649 |
650 | Our last step is to update state. Once again, the state that determines which
651 | items are being displayed is the `items` state in the `ShoppingList` component,
652 | so we need to call `setItems` in that component with a new list of items that
653 | **does not contain our deleted item**.
654 |
655 | We'll pass a callback down from `ShoppingList` to `Item`, just like we did for
656 | the update action:
657 |
658 | ```jsx
659 | // src/components/ShoppingList.js
660 |
661 | function ShoppingList() {
662 | const [selectedCategory, setSelectedCategory] = useState("All");
663 | const [items, setItems] = useState([]);
664 |
665 | useEffect(() => {
666 | fetch("http://localhost:4000/items")
667 | .then((r) => r.json())
668 | .then((items) => setItems(items));
669 | }, []);
670 |
671 | // add this callback function
672 | function handleDeleteItem(deletedItem) {
673 | console.log("In ShoppingCart:", deletedItem);
674 | }
675 |
676 | // ...rest of component
677 |
678 | return (
679 |
680 |
681 |
685 |
686 | {/* pass it as a prop to Item */}
687 | {itemsToDisplay.map((item) => (
688 |
694 | ))}
695 |
696 |
697 | );
698 | }
699 | ```
700 |
701 | Call the `onDeleteItem` prop in the `Item` component once the item has been
702 | deleted from the server, and pass up the item that was clicked:
703 |
704 | ```jsx
705 | // src/components/Item.js
706 |
707 | // Deconstruct the onDeleteItem prop
708 | function Item({ item, onUpdateItem, onDeleteItem }) {
709 | function handleDeleteClick() {
710 | // Call onDeleteItem, passing the deleted item
711 | fetch(`http://localhost:4000/items/${item.id}`, {
712 | method: "DELETE",
713 | })
714 | .then((r) => r.json())
715 | .then(() => onDeleteItem(item));
716 | }
717 | // ... rest of component
718 | }
719 | ```
720 |
721 | As a last step, we need to call `setState` with a new array that removes the
722 | deleted item from the list. Recall from our lessons on working with arrays in
723 | state that we can use `.filter` to help create this new array:
724 |
725 | ```js
726 | // src/components/ShoppingList.js
727 | function handleDeleteItem(deletedItem) {
728 | const updatedItems = items.filter((item) => item.id !== deletedItem.id);
729 | setItems(updatedItems);
730 | }
731 | ```
732 |
733 | Clicking the delete button should now delete the item in the list on the server
734 | as well as in our React state! To recap:
735 |
736 | - When X event occurs
737 | - When a user clicks the Delete button, handle the button click
738 | - Make Y fetch request
739 | - Make a `DELETE` request to `/items/:id`, using the clicked item's data for
740 | the ID
741 | - Update Z state
742 | - Send the clicked item to the `ShoppingList` component, and set state by
743 | creating a new array in which the deleted item has been filtered out
744 |
745 | ## Conclusion
746 |
747 | Synchronizing state between a client-side application and a server-side
748 | application is a challenging problem! Thankfully, the general steps to
749 | accomplish this in React are the same regardless of what kind of action we are
750 | performing:
751 |
752 | - When X event occurs
753 | - Make Y fetch request
754 | - Update Z state
755 |
756 | Keep these steps in mind any time you're working on a feature involving
757 | synchronizing client and server state. Once you have this general framework, you
758 | can apply the other things you've learned about React to each step, like
759 | handling events, updating state, and passing data between components.
760 |
761 | ## Resources
762 |
763 | - [MDN: Using Fetch][using fetch]
764 |
765 | [using fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#uploading_json_data
766 |
--------------------------------------------------------------------------------