feat: add usememo usecallback shopping list app workshop (#60101)

Co-authored-by: Kolade Chris <65571316+Ksound22@users.noreply.github.com>
Co-authored-by: yoko <25644062+sidemt@users.noreply.github.com>
This commit is contained in:
Kristofer Koishigawa
2025-05-31 01:30:19 +09:00
committed by GitHub
parent 55ad073e72
commit 689cdf2480
34 changed files with 7085 additions and 1 deletions

View File

@@ -3616,7 +3616,12 @@
"In these lecture videos, you will learn about data fetching and memoization in React."
]
},
"ffpt": { "title": "279", "intro": [] },
"workshop-shopping-list-app": {
"title": "Build a Shopping List App",
"intro": [
"In this workshop, you'll use the <code>useMemo()</code> and <code>useCallback()</code> hooks in React to build a simple shopping list app. You'll learn more about state and the lifecycle of React components, and how to use memoization to reduce re-renders and make your apps more efficient."
]
},
"lab-currency-converter": {
"title": "Build a Currency Converter",
"intro": [

View File

@@ -0,0 +1,9 @@
---
title: Introduction to the Build a Shopping List App Workshop
block: workshop-shopping-list-app
superBlock: full-stack-developer
---
## Introduction to the Build a Shopping List App Workshop
In this workshop, you'll use the `useMemo()` and `useCallback()` hooks in React to build a simple shopping list app. You'll learn more about state and the lifecycle of React components, and how to use memoization to reduce re-renders and make your apps more efficient.

View File

@@ -0,0 +1,133 @@
{
"name": "Build a Shopping List App",
"blockType": "workshop",
"blockLayout": "challenge-grid",
"isUpcomingChange": false,
"usesMultifileEditor": true,
"hasEditableBoundaries": true,
"dashedName": "workshop-shopping-list-app",
"superBlock": "full-stack-developer",
"challengeOrder": [
{
"id": "67f63e58ce657ae05908d6c8",
"title": "Step 1"
},
{
"id": "67f647f3ce657ae05908d6c9",
"title": "Step 2"
},
{
"id": "67fcd561ae787aba05a9d917",
"title": "Step 3"
},
{
"id": "67fcd83e40fa32ca2c6af073",
"title": "Step 4"
},
{
"id": "67fcdbea6714f8df73b83c86",
"title": "Step 5"
},
{
"id": "67fcde1677d474ebe61b4f2e",
"title": "Step 6"
},
{
"id": "67fcdef983296ff197e9a1cf",
"title": "Step 7"
},
{
"id": "67fe3fe8910ff7e2106ff340",
"title": "Step 8"
},
{
"id": "67fe42e8e6bbaaf3f128004c",
"title": "Step 9"
},
{
"id": "67fe45cc404a5604ea3a1eb9",
"title": "Step 10"
},
{
"id": "67fe49ea92b9fc1c09b6e314",
"title": "Step 11"
},
{
"id": "67fe4adca39a9b224b72d642",
"title": "Step 12"
},
{
"id": "67fe4fa5e660453e884d6710",
"title": "Step 13"
},
{
"id": "67fe512b066c4a479f82d6bd",
"title": "Step 14"
},
{
"id": "67fe53c21e2f8956dcaef1c5",
"title": "Step 15"
},
{
"id": "67fe5810b61de16e891dd02f",
"title": "Step 16"
},
{
"id": "6808baa8f8dcaf4f50a7acaa",
"title": "Step 17"
},
{
"id": "6808bcac6bd35f568a19f21a",
"title": "Step 18"
},
{
"id": "67fe5f1eaea66710b08c03f1",
"title": "Step 19"
},
{
"id": "67fe626cc464f1238514cd8b",
"title": "Step 20"
},
{
"id": "67fe66d2490f143c510038bf",
"title": "Step 21"
},
{
"id": "67fe694ebcafe94ab7451a8f",
"title": "Step 22"
},
{
"id": "67ff7500255008598207a70c",
"title": "Step 23"
},
{
"id": "67ff838da1cb2927ae18a3f5",
"title": "Step 24"
},
{
"id": "67ff89b745907449e0e5bae4",
"title": "Step 25"
},
{
"id": "67ff7e6d04d17507e8923f46",
"title": "Step 26"
},
{
"id": "67ff8defd6747d621c4b838c",
"title": "Step 27"
},
{
"id": "67ff7f5515db250dc015f128",
"title": "Step 28"
},
{
"id": "67ff8059bac7f0146857c2b1",
"title": "Step 29"
},
{
"id": "67ff8eacd3d965670c9121f9",
"title": "Step 30"
}
],
"helpCategory": "JavaScript"
}

View File

@@ -0,0 +1,166 @@
---
id: 67f63e58ce657ae05908d6c8
title: Step 1
challengeType: 0
dashedName: step-1
demoType: onLoad
---
# --description--
In this workshop, you'll build a simple shopping list app to practice using the `useMemo()` and `useCallback()` hooks in React. You'll learn how to use these hooks to optimize the performance of your app by memoizing potentially expensive calculations and functions.
All the basic HTML and CSS you'll need has been provided for you, along with the basic structure of a React component named `ShoppingList`.
Within the `ShoppingList` component, return an empty pair of parentheses for now.
# --hints--
You should use the `export` keyword to export the `ShoppingList` component.
```js
assert.match(code, /export\s+(const|let|var|function)\s+ShoppingList\s*(=\s*)?\(\)\s*(=>\s*)?\{\s*/);
```
You should return an empty pair of round parentheses inside the `ShoppingList` function.
```js
assert.match(code, /export\s+(const|let|var|function)\s+ShoppingList\s*(=\s*)?\(\)\s*(=>\s*)?\{\s*return\s*\(\s*\)\s*;?\s*\}/)
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
--fcc-editable-region--
const { useState } = React;
export const ShoppingList = () => {
};
--fcc-editable-region--
```

View File

@@ -0,0 +1,187 @@
---
id: 67f647f3ce657ae05908d6c9
title: Step 2
challengeType: 0
dashedName: step-2
---
# --description--
Within the `return` statement of the `ShoppingList` component, add a `div` element with a `className` of `container`.
Inside the `div`, nest an `h1` element with the text `Shopping List`, and below that, nest an empty `form` element.
# --hints--
Your `ShoppingList` component should return a `div` element with a `className` of `container`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
assert.equal(testElem.firstElementChild?.tagName, 'DIV');
assert.isTrue(
[...testElem.firstElementChild.classList].includes('container')
);
}
```
Your `div` should contain an `h1` element with the text `Shopping List`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const h1 = testElem.querySelector('h1');
assert.exists(h1);
assert.equal(h1.textContent.toLowerCase().trim(), 'shopping list');
}
```
Your `div` should also contain an empty `form` element.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const form = testElem.querySelector('form');
assert.exists(form);
assert.equal(form.tagName, 'FORM');
}
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
--fcc-editable-region--
const { useState } = React;
export const ShoppingList = () => {
return (
);
};
--fcc-editable-region--
```

View File

@@ -0,0 +1,226 @@
---
id: 67fcd561ae787aba05a9d917
title: Step 3
challengeType: 0
dashedName: step-3
---
# --description--
Within the `form` element, add a `label` element with the text `Search for an item:`. Next, give the `label` an `htmlFor` attribute set to `search`. This will associate the label with an `input` element you'll add next.
Below the `label`, add an `input` element with the `type` and `id` attributes set to `search`. Also, give the `input` a placeholder of `Search...`, and an `aria-describedby` attribute set to `search-description`.
Finally, add a `p` element below the `input`, and give it the text `Type to filter the list below:` and an `id` of `search-description`. This will provide additional context for screen readers.
# --hints--
Your `form` element should contain a `label` element with the text `Search for an item:`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const label = testElem.querySelector('form label');
assert.exists(label);
assert.equal(label.textContent.toLowerCase().trim(), 'search for an item:');
}
```
Your `label` should have an `htmlFor` attribute set to `search`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const label = testElem.querySelector('form label');
assert.equal(label.getAttribute('for'), 'search');
}
```
Your `form` element should contain an `input` element with the `type` and `id` attributes set to `search`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const input = testElem.querySelector('form input');
assert.exists(input);
assert.equal(input.getAttribute('type'), 'search');
assert.equal(input.getAttribute('id'), 'search');
}
```
Your `input` should have a placeholder of `Search...` and an `aria-describedby` attribute set to `search-description`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const input = testElem.querySelector('form input');
assert.equal(input.getAttribute('placeholder').toLowerCase().trim(), 'search...');
assert.equal(input.getAttribute('aria-describedby'), 'search-description');
}
```
Your `form` element should contain a `p` element with the text `Type to filter the list below:`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const p = testElem.querySelector('form p');
assert.exists(p);
assert.equal(p.textContent.toLowerCase().trim(), 'type to filter the list below:');
}
```
Your `p` element should have an `id` of `search-description`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const p = testElem.querySelector('form p');
assert.equal(p.getAttribute('id'), 'search-description');
}
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
export const ShoppingList = () => {
return (
<div className="container">
<h1>Shopping List</h1>
<form>
--fcc-editable-region--
--fcc-editable-region--
</form>
</div>
);
};
```

View File

@@ -0,0 +1,193 @@
---
id: 67fcd83e40fa32ca2c6af073
title: Step 4
challengeType: 0
dashedName: step-4
---
# --description--
Now that you have the basic structure of the app, it's time to add some items and get the search functionality working.
Create an array and assign it to a variable named `items`. Within the array, add the strings `Apples`, `Bananas`, `Strawberries`, `Blueberries`, `Mangoes`, `Pineapple`, `Lettuce`, `Broccoli`, `Paper Towels`, and `Dish Soap`. This will be the list of items that will be displayed in the app.
# --hints--
`items` is an array.
```js
assert.match(code, /(const|let|var)\s+items\s*=\s*\[/i);
```
Your `items` array should contain the strings `Apples`, `Bananas`, `Strawberries`, `Blueberries`, `Mangoes`, `Pineapple`, `Lettuce`, `Broccoli`, `Paper Towels`, and `Dish Soap`.
```js
const expected = [
"Apples", "Bananas", "Strawberries", "Blueberries", "Mangoes",
"Pineapple", "Lettuce", "Broccoli", "Paper Towels", "Dish Soap"
];
const itemArrContentsString = code.match(/(?:const|let|var)\s+items\s*=\s*\[\s*([\s\S]*?)\s*\]/)?.[1] ?? null;
const itemArr = itemArrContentsString ? itemArrContentsString.split(",").map(item => item.replace(/['"]/g, "")) : [];
const normalizeArray = (arr) => arr.map((str) => str.toLowerCase().replace(/\W/g, "")).filter(Boolean);
const normalizedItemArr = normalizeArray(itemArr);
const normalizedExpected = normalizeArray(expected);
assert.isTrue(
normalizedItemArr.length === normalizedExpected.length &&
normalizedItemArr.every((item) => normalizedExpected.includes(item))
);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
--fcc-editable-region--
--fcc-editable-region--
export const ShoppingList = () => {
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
/>
<p id="search-description">Type to filter the list below:</p>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,201 @@
---
id: 67fcdbea6714f8df73b83c86
title: Step 5
challengeType: 0
dashedName: step-5
---
# --description--
Next, you need to create a state variable, a setter function, and set the initial state with the `useState()` hook to store the user's input.
As a reminder, here's how to create a state variable named `age`, a setter function to update it named `setAge`, and initialize `age` to the number `0` with the `useState()` hook:
```js
const [age, setAge] = useState(0);
```
At the top of the `ShoppingList` component, use the `useState()` hook to create a state variable named `query` and a setter function to update it named `setQuery`. Initialize `query` to an empty string. This will be used to store the user's search input.
# --hints--
You should create a state variable named `query` and a setter function named `setQuery`.
```js
assert.match(code, /(const|let|var)\s+\[\s*query\s*,\s*setQuery\s*\]\s*=\s*useState\(/);
```
The initial value of `query` should be an empty string.
```js
const queryValue = code.match(/(?:const|let|var)\s+\[\s*query\s*,\s*setQuery\s*\]\s*=\s*useState\(\s*(['"])(.*?)\1\s*\)/)?.[2] ?? null;
assert.typeOf(queryValue, "string");
assert.lengthOf(queryValue, 0);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
--fcc-editable-region--
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
/>
<p id="search-description">Type to filter the list below:</p>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,206 @@
---
id: 67fcde1677d474ebe61b4f2e
title: Step 6
challengeType: 0
dashedName: step-6
---
# --description--
In the input element, add a `value` attribute and set it to the `query` state variable. This will allow the input field to display the current value of `query`. Also, remember that you need to use curly braces (`{}`) to embed JavaScript expressions like `query` in JSX.
Then, add an `onChange` event handler to the input element. Pass it an anonymous function that takes `e` as an argument, which is the event object. Inside the anonymous function, call `setQuery()` and pass it `e.target.value`. This will update the `query` state variable with the current value of the input field whenever the user types in it.
# --hints--
You should add a `value` attribute to the input element and set it to the `query` state variable.
```js
assert.match(code, /<input[^>]*value={\s*query\s*}[^>]*\/?>/);
```
Your `onChange` event handler should be a function that takes `e` as an argument.
```js
const input = document.querySelector('input');
const key = Object.keys(input).find(key => key.startsWith("__reactProps"));
const reactInput = input[key];
assert.match(reactInput.onChange.toString(), /onChange\s*\(\s*e\s*\)/);
```
Your `onChange` event handler should call `setQuery()` with `e.target.value`.
```js
const input = document.querySelector('input');
const key = Object.keys(input).find(key => key.startsWith("__reactProps"));
const reactInput = input[key];
assert.match(reactInput.onChange.toString(), /setQuery\s*\(\s*e\.target\.value\s*\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
--fcc-editable-region--
--fcc-editable-region--
/>
<p id="search-description">Type to filter the list below:</p>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,206 @@
---
id: 67fcdef983296ff197e9a1cf
title: Step 7
challengeType: 0
dashedName: step-7
---
# --description--
Create a variable named `filteredItems` and assign it the result of filtering the `items` array with the `filter()` method. Pass an anonymous function to the `filter()` method that takes `item` as an argument. Inside the anonymous function, just return `item` for now.
# --hints--
You should create a variable named `filteredItems`.
```js
assert.match(code, /(const|let|var)\s+filteredItems\s*=/);
```
You should use the `filter()` method on the `items` array.
```js
assert.match(code, /items\.filter\s*\(/);
```
Your `filter()` method should take an anonymous function that takes `item` as an argument and returns `item`.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item;?\s*}\s*\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
--fcc-editable-region--
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,228 @@
---
id: 67fe3fe8910ff7e2106ff340
title: Step 8
challengeType: 0
dashedName: step-8
---
# --description--
While you're not really filtering anything yet, and `filteredItems` matches the `items` array, it would be nice to see the list of items on the page.
First, add a `ul` element below the `p` element in the `form`. Inside the unordered list, use the `map()` method to iterate over the `filteredItems` array, and pass an anonymous function to it. The anonymous function should take `item` as an argument, and return an `li` element for each item. Each `li` should display the `item` text.
# --hints--
Your `form` element should contain a `ul` element.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
assert.exists(testElem.querySelector('form ul'));
}
```
You should use the `map()` method on the `filteredItems` array within the `ul` element.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /React.createElement\("ul", null, filteredItems.map\(function\s*\(/);
```
Your `map()` method should take an anonymous function that takes `item` as an argument.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /React.createElement\("ul", null, filteredItems.map\(function\s*\(\s*item\s*\)\s*{/);
```
The anonymous function for `map()` should return an `li` element with the `item` text for each element in the `filteredItems` array.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /React.createElement\("ul", null, filteredItems.map\(function\s*\(\s*item\s*\)\s*{\s*.+React.createElement\("li", null, item\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const filteredItems = items.filter((item) => item);
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
--fcc-editable-region--
--fcc-editable-region--
</form>
</div>
);
};
```

View File

@@ -0,0 +1,203 @@
---
id: 67fe42e8e6bbaaf3f128004c
title: Step 9
challengeType: 0
dashedName: step-9
---
# --description--
If you check the console, you'll see a warning about a missing `key` prop. This is because React needs a unique key for each element in a list to help it identify which items have changed, are added, or are removed. It is also important for performance reasons.
To fix this, add a `key` prop to the `li` element inside the `map()` method. Set the `key` to the value of `item`. Generally, you should use a unique identifier like an id as the `key`, but since the items are all unique strings, you can use the string itself in this case.
# --hints--
The `key` prop for your `li` element should be set to the value of `item`.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /React.createElement\("li", {\s*key:\s+item\s*}, item\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const filteredItems = items.filter((item) => item);
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
--fcc-editable-region--
<li>
--fcc-editable-region--
{item}
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,206 @@
---
id: 67fe45cc404a5604ea3a1eb9
title: Step 10
challengeType: 0
dashedName: step-10
---
# --description--
Now it's time to finish the search functionality.
The search input should be case-insensitive, and should also match partial strings. For example, if a user types in `app`, it should match both `Apples` and `Pineapples`. This is because the lowercase version of `app` is a substring of the lowercase version of both `Apples` and `Pineapples`.
First, inside the `filter()` method, use the `toLowerCase()` method to convert `item` into a lowercase string.
# --hints--
You should use the `toLowerCase()` method on `item` within the `filter()` method.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\);?\s*}\s*\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
--fcc-editable-region--
const filteredItems = items.filter((item) => item);
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
{item}
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,224 @@
---
id: 67fe49ea92b9fc1c09b6e314
title: Step 11
challengeType: 0
dashedName: step-11
---
# --description--
Next, use the `includes()` method to check if the lowercase version of `item` includes the lowercase version of `query` as a substring.
For example, here's how to use the `toLowerCase()` and `includes()` methods to check if the string `freeCodeCamp` includes `code`:
```js
const str1 = "freeCodeCamp";
const str2 = "code";
str1.toLowerCase().includes(str2.toLowerCase()); // true
```
Chain the `includes()` method to the lowercase `item` and check if it includes the lowercase `query`. Remember to lowercase `query` as well.
# --hints--
You should chain the `includes()` method to the lowercase `item`.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\).includes\(\s*/);
```
You should check if the lowercase `item` string includes the lowercase `query` as a substring.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /filteredItems\s*=\s*items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\)\.includes\(\s*query\.toLowerCase\(\s*\)\s*\);?\s*}\s*\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
--fcc-editable-region--
const filteredItems = items.filter((item) => item.toLowerCase());
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
{item}
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,214 @@
---
id: 67fe4adca39a9b224b72d642
title: Step 12
challengeType: 0
dashedName: step-12
---
# --description--
Now the search input should work as expected, and filter the shopping list based on the user's input.
Next, we'll add a way to check off items in the list so users can keep track of what they've already purchased.
To do this, we'll need another state variable and a setter function. Use the `useState` hook to create a state variable named `selectedItems` and a setter function named `setSelectedItems`. Initialize `selectedItems` as an empty array. This array will store the items the user has selected, and we'll apply some styling to those selected items later.
# --hints--
You should create a state variable named `selectedItems` and a setter function named `setSelectedItems`.
```js
assert.match(code, /(const|let)\s+\[\s*selectedItems\s*,\s*setSelectedItems\s*\]\s*=\s*useState\(/);
```
The initial value of `selectedItems` should be an empty array.
```js
const selectedItemsValue = code.match(/(?:const|let)\s+\[\s*selectedItems\s*,\s*setSelectedItems\s*\]\s*=\s*useState\(\s*(\[\s*(.*?)\s*\])\s*\)/)?.[1] ?? null;
assert.isTrue(
selectedItemsValue &&
selectedItemsValue?.replace(/\s/g, "") === "[]"
);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
--fcc-editable-region--
--fcc-editable-region--
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
{item}
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,219 @@
---
id: 67fe4fa5e660453e884d6710
title: Step 13
challengeType: 0
dashedName: step-13
---
# --description--
Create a function named `toggleItem` that takes `item` as an argument.
Inside the function, just call the `setSelectedItems()` function for now. We'll come back a bit later to implement the logic to toggle selected items.
# --hints--
You should have a function named `toggleItem` that takes `item` as an argument.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /function\s+toggleItem\s*\(\s*item\s*\)\s*{/);
```
You should call the `setSelectedItems()` function inside the `toggleItem()` function.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /function\s+toggleItem\s*\(\s*item\s*\)\s*{(.|\n)*setSelectedItems\s*\(\s*\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
--fcc-editable-region--
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
{item}
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,240 @@
---
id: 67fe512b066c4a479f82d6bd
title: Step 14
challengeType: 0
dashedName: step-14
---
# --description--
Within the `li` element, above the `item` text, add an `input` element with the `type` attribute set to `checkbox`. Also, set its `onChange` attribute to an anonymous function that calls `toggleItem()` with `item` as an argument.
Finally, wrap the `input` and the `item` text in a `label` element for better accessibility.
# --hints--
You should add another `input` element with the `type` attribute set to `checkbox`.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const checkbox = testElem.querySelector('input[type="checkbox"]');
assert.exists(checkbox);
}
```
Your new `input` should have an `onChange` attribute set to an anonymous function that calls `toggleItem()` with `item` as an argument.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const checkbox = testElem.querySelector('input[type="checkbox"]');
const key = checkbox && Object.keys(checkbox).find(key => key.startsWith("__reactProps"));
const reactCheckbox = checkbox && checkbox[key];
const onChangeString = reactCheckbox && reactCheckbox?.onChange?.toString();
assert.match(onChangeString, /function onChange\s*\(\s*\)\s*{\s*(return)?\s+toggleItem\s*\(\s*item\s*\);?\s*\}/);
}
```
Your new `input` and the `item` text should be wrapped in a `label` element.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const label = testElem.querySelector('li label');
const key = label && Object.keys(label)?.find(key => key.startsWith("__reactProps"));
const reactLabel = label && label[key];
const reactLabelChildren = reactLabel?.children || [];
assert.lengthOf(reactLabelChildren, 2);
assert.isTrue(reactLabelChildren.some((item => item.type === 'input')));
assert.isTrue(reactLabelChildren.some((item => typeof item === 'string')));
}
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems();
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
--fcc-editable-region--
<li key={item}>
{item}
</li>
--fcc-editable-region--
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,231 @@
---
id: 67fe53c21e2f8956dcaef1c5
title: Step 15
challengeType: 0
dashedName: step-15
---
# --description--
Back to the `toggleItem()` function.
If you recall from a past lecture, you can compare past and present states in React, and use that to determine an upcoming state. In this case, you'll use the past and present states to either add or remove items from the `selectedItems` array.
First, call the `setSelectedItems()` function, and pass it an anonymous function as an argument. This anonymous function should take `prev` as an argument, which is the previous state of `selectedItems`.
Inside the anonymous function, use the `includes` method to check if `prev` includes `item` and return the result.
# --hints--
You should pass an anonymous function that takes `prev` as an argument to the `setSelectedItems()` function.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /function\s*toggleItem\s*\(\s*item\s*\)\s*{(.|\n)*setSelectedItems\s*\(function\s*\(\s*prev\s*\)\s*{/);
```
Use the `includes()` method inside the anonymous function to check if `prev` includes `item`, and return the result of that check.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /function\s*\(\s*prev\s*\)\s*{\s*return\s+prev\.includes\s*\(\s*item\s*\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
--fcc-editable-region--
const toggleItem = (item) => {
setSelectedItems();
};
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
/>
{item}
</label>
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,216 @@
---
id: 67fe5810b61de16e891dd02f
title: Step 16
challengeType: 0
dashedName: step-16
---
# --description--
Rather than just returning `prev.includes(item)`, return a ternary operator with `prev.includes(item)` as the condition. Just return `null` for both cases for now.
# --hints--
Return a ternary operator that checks if `prev.includes(item)` is true. Return `null` for both the truthy and falsy cases.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /setSelectedItems\s*\(function\s*\(\s*prev\s*\)\s*{\s*return\s+prev\.includes\s*\(\s*item\s*\)\s*\?\s*null\s*:\s*null;?\s*}/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
--fcc-editable-region--
prev.includes(item)
--fcc-editable-region--
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
/>
{item}
</label>
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,247 @@
---
id: 67fe5f1eaea66710b08c03f1
title: Step 19
challengeType: 0
dashedName: step-19
---
# --description--
Now you'll work on some logic to cross off any selected items on the list.
First off, since everything inside of this `filteredItems.map()` call is being implicitly returned, wrap all the code within it in curly braces, and return the `li` element explicitly.
For example, here's how you would do this with a simple component that renders a list of names:
```jsx
const names = ["Abbey", "Beau", "Quincy"];
const NameList = () => {
return (
<ul>
{names.map((name) => {
return (
<li key={name}>{name}</li>
);
})}
</ul>
);
};
```
# --hints--
You should wrap the code within the `filteredItems.map()` call in curly braces.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(code, /{\s*filteredItems\.map\(\s*(function)?\s*\(?\s*item\s*\)?\s*\)\s*(=>)?\s*{(.|\n)*}\s*\);?\s*}/);
```
You should return the `li` element explicitly.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(code, /{\s*filteredItems\.map\(\s*(function)?\s*\(?\s*item\s*\)?\s*\)\s*(=>)?\s*{\s*return\s*\(\s*(.|\n)*}\s*\);?\s*}/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
--fcc-editable-region--
<ul>
{filteredItems.map((item) =>
<li key={item}>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
/>
{item}
</label>
</li>
)}
</ul>
--fcc-editable-region--
</form>
</div>
);
};
```

View File

@@ -0,0 +1,259 @@
---
id: 67fe626cc464f1238514cd8b
title: Step 20
challengeType: 0
dashedName: step-20
---
# --description--
Before the `return` statement within the `filteredItems.map()` call, create a variable called `isChecked` and use the `includes()` method to check if the current `item` is in the `selectedItems` array.
Then, add a `style` prop to the `li` element, and use a ternary operator to set the `textDecoration` property to `line-through` if `isChecked` is true, or `none` if it is false. Remember that you need to wrap the ternary operator in curly braces since it's a JavaScript expression.
For example, here's how you can use a ternary operator to conditionally set the color of a paragraph in a simple component:
```jsx
const MyComponent = () => {
const isActive = true;
return (
<p style={{ color: isActive ? "green" : "black"}}>This text is conditionally styled.</p>
);
};
```
# --hints--
You should create a variable called `isChecked` which uses the `includes()` method on the `selectedItems` array to check if the current `item` is in the array.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*isChecked\s*=\s*selectedItems\.includes\s*\(\s*item\s*\)/);
```
You should add a `style` prop to the `li` element.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const li = testElem.querySelector('li');
const key = li && Object.keys(li)?.find(key => key.startsWith("__reactProps"));
const reactLi = li && li[key];
assert.exists(reactLi.style);
}
```
Your `style` prop should include a ternary operator that sets the `textDecoration` property to `line-through` if `isChecked` is true, or `none` if it is false.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /"li"\s*,\s*{(.|\n)*style:\s*{\s*textDecoration\s*:\s*isChecked\s*\?\s*("|')line-through("|')\s*:\s*("|')none("|')/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
--fcc-editable-region--
return (
<li
key={item}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
/>
{item}
</label>
</li>
);
--fcc-editable-region--
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,235 @@
---
id: 67fe66d2490f143c510038bf
title: Step 21
challengeType: 0
dashedName: step-21
---
# --description--
Then, in the checkbox `input` element, add a `checked` attribute and set it to `isChecked`. While it's not strictly necessary to set the `checked` attribute, it's a good practice to ensure that each checkbox reflects the current state of the `selectedItems` array.
# --hints--
You should add a `checked` attribute to the checkbox `input` element.
```js
async () => {
const testElem = await __helpers.prepTestComponent(window.index.ShoppingList);
const checkbox = testElem.querySelector('input[type="checkbox"]');
const key = checkbox && Object.keys(checkbox)?.find(key => key.startsWith("__reactProps"));
const reactCheckbox = checkbox && checkbox[key];
assert.exists(reactCheckbox.checked);
}
```
You should set the `checked` attribute to `isChecked`.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /"input"\s*,\s*{(.|\n)*checked:\s*isChecked/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
--fcc-editable-region--
<input
type="checkbox"
onChange={() => toggleItem(item)}
/>
--fcc-editable-region--
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,230 @@
---
id: 67fe694ebcafe94ab7451a8f
title: Step 22
challengeType: 0
dashedName: step-22
---
# --description--
Your app works! Go ahead and test it out.
However, it's not as efficient as it could be. While React is already very performant, there are some cases where it can be better to cache the results of potentially expensive calculations, or to ensure that functions are not recreated on every render.
We'll improve the performance of your app over the following steps. But first, let's add some logging so you can see the lifecycle of this component more clearly.
Above `filteredItems`, add a `console.log()` statement that logs the string `Filtering items...` to the console.
# --hints--
You should add a `console.log()` statement above `filteredItems` that logs the string `Filtering items...` to the console.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /console\.log\s*\(\s*("|'|`)\s*Filtering\s+items\s*\.\.\.\s*("|'|`)\s*\);?/i);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
--fcc-editable-region--
--fcc-editable-region--
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,225 @@
---
id: 67ff7500255008598207a70c
title: Step 23
challengeType: 0
dashedName: step-23
---
# --description--
Now you will see `Filtering items...` in the console every time you type in the search bar and check or uncheck an item. This is because those actions update the state of the component, which causes it to re-render.
This isn't usually a problem. But if you have a lot of items in the list, or if you're fetching a lot of data from an API and manipulating it, your app can feel slow.
To improve performance, you can use the `useMemo()` hook to memoize, or in other words, cache, the result of the filtering operation. Then, React will only re-run the filtering operation when its dependency changes, rather than on every render.
First, destructure the `useMemo()` hook from React at the top of your file.
# --hints--
You should destructure `useMemo` from `React` at the top of the file. Make sure you aren't removing the `useState` hook you're already using.
```js
assert.match(code, /(const|let|var)\s*{[^}]*\buse(State|Memo)\b[^}]*\buse(Memo|State)\b[^}]*}\s*=\s*React/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
--fcc-editable-region--
const { useState } = React;
--fcc-editable-region--
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
console.log("Filtering items...");
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,227 @@
---
id: 67ff7e6d04d17507e8923f46
title: Step 26
challengeType: 0
dashedName: step-26
---
# --description--
Another thing React does each time it re-renders a component is recreate the functions inside of it. Here, every time you check or uncheck an item, the `toggleItem()` function is recreated. This is not a problem in most cases, but can lead to performance issues in larger apps.
Let's add some logging to track this.
Above the `ShoppingList` component, use `let` to create a variable named `prevToggleItem` and set it to `null`. You'll use this to track the function definition of `toggleItem()` across renders.
# --hints--
You should use `let` to create a variable named `prevToggleItem` and set it to `null`.
```js
assert.match(code, /let\s+prevToggleItem\s*=\s*null/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState, useMemo } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
--fcc-editable-region--
--fcc-editable-region--
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = useMemo(() => {
console.log("Filtering items...");
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,242 @@
---
id: 67ff7f5515db250dc015f128
title: Step 28
challengeType: 0
dashedName: step-28
---
# --description--
Now that you can see when the `toggleItem()` function definition is first set or changes, you should also log when the function is the same across renders.
Add an `else` statement to the `if` statement you just wrote. Inside the `else` block, log the string `Current toggleItem function` to the console.
# --hints--
You should add an `else` statement to the `if` statement you wrote in the last step.
```js
assert.match(code, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)\s*{.*}\s*else\s*{/si);
```
You should log the string `Current toggleItem function` to the console inside the `else` statement.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx")?.innerText || '';
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList?.toString() || '';
console.log(shoppingListString);
assert.match(shoppingListString, /else\s*{\s*console\.log\s*\(\s*("|')\s*Current\s+toggleItem\s+function\s*("|')\s*\);?/si);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState, useMemo } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
let prevToggleItem = null;
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = useMemo(() => {
console.log("Filtering items...");
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
--fcc-editable-region--
if (prevToggleItem !== toggleItem) {
console.log("New toggleItem function");
prevToggleItem = toggleItem;
}
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,234 @@
---
id: 67ff8059bac7f0146857c2b1
title: Step 29
challengeType: 0
dashedName: step-29
---
# --description--
If you use the app now, you'll see that the `toggleItem()` function is recreated every time you check or uncheck an item, or type in the search bar.
To improve performance, you can use the `useCallback()` hook to memoize or cache the `toggleItem()` function. Then React will only recreate the function when its dependencies change.
First, destructure the `useCallback()` hook from React at the top of your file.
# --hints--
You should destructure `useCallback` from `React` at the top of the file. Make sure you aren't removing any other hooks you're already using.
```js
assert.match(code, /(const|let|var)\s*{[^}]*\buse(State|Memo|Callback)\b[^}]*\buse(State|Memo|Callback)\b[^}]*\buse(State|Memo|Callback)\b[^}]*}\s*=\s*React/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
--fcc-editable-region--
const { useState, useMemo } = React;
--fcc-editable-region--
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
let prevToggleItem = null;
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = useMemo(() => {
console.log("Filtering items...");
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
if (prevToggleItem !== toggleItem) {
console.log("New toggleItem function");
prevToggleItem = toggleItem;
} else {
console.log("Current toggleItem function");
}
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,268 @@
---
id: 67ff838da1cb2927ae18a3f5
title: Step 24
challengeType: 0
dashedName: step-24
---
# --description--
`useMemo()` takes two arguments: a function that returns the value you want to memoize, and an array of dependencies. The memoized value will only be recomputed when one or more of its dependencies change.
Here's the basic syntax for `useMemo()`:
```js
const memoizedValue = useMemo(() => {
// Some expensive calculation
return value;
}, [dependency1, dependency2]);
```
Set `filteredItems` equal to `useMemo()`, and pass it an anonymous function with curly braces. Inside the curly braces, use a `return` statement to explicitly return your existing filtering logic. Also, add `query` as the only dependency in the dependencies array. This ensures that the filtering operation is only re-run when `query` changes.
# --hints--
You should set `filteredItems` equal to calling the `useMemo()` hook.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(/);
```
You should pass an anonymous function to `useMemo()` with curly braces.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{/);
```
You should use a `return` statement to explicitly return your existing filtering logic.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{\s*return\s+items\.filter\s*\(function\s*\(\s*item\s*\)\s*{\s*return\s+item\.toLowerCase\(\s*\).includes\(\s*query\.toLowerCase\(\s*\)\s*\);?\s*}\s*\);?\s*}/);
```
You should add `query` as the only dependency in the dependencies array.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{(.|\n)*},\s*\[\s*query\s*\]\s*\);?/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState, useMemo } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
console.log("Filtering items...");
--fcc-editable-region--
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
--fcc-editable-region--
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,226 @@
---
id: 67ff89b745907449e0e5bae4
title: Step 25
challengeType: 0
dashedName: step-25
---
# --description--
Finally, move your `console.log()` statement inside the `useMemo()` callback function, just above the `return` statement.
# --hints--
You should move your `Filtering items...` log before the `return` statement inside the `useMemo()` callback function.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*filteredItems\s*=\s*useMemo\s*\(\s*function\s*\(\s*\)\s*{\s*console\.log\s*\(\s*("|'|`)\s*Filtering\s+items\s*\.\.\.\s*("|'|`)\s*\);?\s*return\s+items\.filter/i);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState, useMemo } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
--fcc-editable-region--
console.log("Filtering items...");
const filteredItems = useMemo(() => {
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
--fcc-editable-region--
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,242 @@
---
id: 67ff8defd6747d621c4b838c
title: Step 27
challengeType: 0
dashedName: step-27
---
# --description--
Next, write an `if` statement to check if `prevToggleItem` is not strictly equal to `toggleItem`. If they are not equal, log the string `New toggleItem function` to the console. Then, set `prevToggleItem` equal to `toggleItem`.
# --hints--
You should write an `if` statement to check if `prevToggleItem` is not strictly equal to `toggleItem`.
```js
assert.match(code, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)/);
```
You should log the string `New toggleItem function` to the console inside the `if` statement if the condition is met.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx")?.innerText || '';
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList?.toString() || '';
assert.match(shoppingListString, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)\s*{.*console\.log\s*\(\s*("|')\s*New\s+toggleItem\s+function\s*("|')\s*\);?/si);
```
You should set `prevToggleItem` equal to `toggleItem` inside the `if` statement if the condition is met.
```js
assert.match(code, /if\s*\(\s*prevToggleItem\s*!==\s*toggleItem\s*\)\s*{.*prevToggleItem\s*=\s*toggleItem;?\s*/si);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState, useMemo } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
let prevToggleItem = null;
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = useMemo(() => {
console.log("Filtering items...");
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
--fcc-editable-region--
--fcc-editable-region--
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,493 @@
---
id: 67ff8eacd3d965670c9121f9
title: Step 30
challengeType: 0
dashedName: step-30
---
# --description--
The `useCallback()` hook takes two arguments: a function and an array of dependencies. The function will only be recreated if one of the dependencies changes.
Here's the basic syntax for `useCallback()`:
```js
const memoizedCallback = useCallback(() => {
// Your function logic
return value;
}, [dependency1, dependency2]);
```
Set `toggleItem` equal to `useCallback()`, and pass it an anonymous function that takes `item` as an argument. Move your existing `toggleItem()` function logic inside the anonymous function, where `setSelectedItems` is called. Finally, add `setSelectedItems` as a dependency in the dependencies array.
After that, you're done optimizing! Your app should still work the same way, but you'll only see your console logs when the `query` state changes or when the `toggleItem()` function is recreated.
# --hints--
You should set `toggleItem` equal to calling the `useCallback()` hook.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(/);
```
You should pass an anonymous function that takes `item` as an argument to `useCallback()`.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(\s*function\s*\(\s*item\s*\)\s*{/);
```
You should move your existing `toggleItem()` function logic, starting with the call to `setSelectedItems()`, inside the anonymous function.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(\s*function\s*\(\s*item\s*\)\s*{\s*(return)?\s+setSelectedItems\s*\(\s*function\s*\(prev\)\s*{\s*return\s+prev\.includes\(item\)\s*\?\s*prev\.filter\(function\s*\(i\)\s*\{\s*return\s+i\s!==\sitem;\s*\}\s*\)\s*:\s*\[\].concat\(_toConsumableArray\(prev\),\s+\[item\]\);\s*}\);?/);
```
You should add `setSelectedItems` as the only dependency in the dependencies array.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /var\s*toggleItem\s*=\s*useCallback\s*\(\s*function\s*\(\s*item\s*\)\s*{.*},\s*\[\s*setSelectedItems\s*\]\s*\);?/si);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState, useMemo, useCallback } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
let prevToggleItem = null;
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = useMemo(() => {
console.log("Filtering items...");
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
--fcc-editable-region--
const toggleItem = (item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
};
--fcc-editable-region--
if (prevToggleItem !== toggleItem) {
console.log("New toggleItem function");
prevToggleItem = toggleItem;
} else {
console.log("Current toggleItem function");
}
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```
# --solutions--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState, useMemo, useCallback } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
let prevToggleItem = null;
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = useMemo(() => {
console.log("Filtering items...");
return items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
const toggleItem = useCallback(
(item) => {
setSelectedItems((prev) =>
prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
);
},
[setSelectedItems]
);
if (prevToggleItem !== toggleItem) {
console.log("New toggleItem function");
prevToggleItem = toggleItem;
} else {
console.log("Current toggleItem function");
}
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) => {
const isChecked = selectedItems.includes(item);
return (
<li
key={item}
style={{ textDecoration: isChecked ? "line-through" : "none" }}
>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
checked={isChecked}
/>
{item}
</label>
</li>
);
})}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,229 @@
---
id: 6808baa8f8dcaf4f50a7acaa
title: Step 17
challengeType: 0
dashedName: step-17
---
# --description--
For the truthy condition, if `prev` includes `item`, return a filtered array with `item` removed.
Chain the `filter()` method to `prev` and pass it an anonymous function that takes `i` as an argument. Inside the function, check that `i` is not strictly equal to `item`. This will return a new array with all items except `item`.
# --hints--
You should chain the `filter()` method to `prev` and pass it an anonymous function that takes `i` as an argument.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /prev\.includes\s*\(\s*item\s*\)\s*\?\s*prev\.filter\s*\(function\s*\(\s*i\s*\)\s*{/);
```
Inside the anonymous `filter()` function, you should check that `i` is not strictly equal to `item`. Remember to return the result of that check.
```js
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const shoppingListString = exports.ShoppingList.toString();
assert.match(shoppingListString, /prev\.includes\s*\(\s*item\s*\)\s*\?\s*prev\.filter\s*\(function\s*\(\s*i\s*\)\s*{\s*return\s+i\s*!==\s*item;?\s*}\s*\)/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
--fcc-editable-region--
prev.includes(item) ? null : null
--fcc-editable-region--
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
/>
{item}
</label>
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -0,0 +1,211 @@
---
id: 6808bcac6bd35f568a19f21a
title: Step 18
challengeType: 0
dashedName: step-18
---
# --description--
For the falsy condition, use the spread operator to return a copy of the `prev` array with `item` appended to the end. This will add `item` to the `selectedItems` array.
# --hints--
You should use the spread operator to clone the `prev` array with `item` appended to the end.
```js
assert.match(code, /\:\s*\[\s*\.\.\.prev\s*\,\s*item\s*\]/);
```
# --seed--
## --seed-contents--
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping List</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
src="index.jsx"
></script>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app-container"></main>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { ShoppingList } from "./index.jsx";
const appContainer = document.getElementById("app-container");
const root = ReactDOM.createRoot(appContainer);
root.render(<ShoppingList />);
</script>
</body>
</html>
```
```css
:root {
--dark-grey: #1b1b32;
--light-grey: #f5f6f7;
--dark-orange: #f89808;
--yellow: #f1be32;
--golden-yellow: #feac32;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: var(--dark-grey);
background-color: var(--dark-grey);
}
main,
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background-color: var(--light-grey);
width: 90%;
margin: 20px;
padding: 10px;
}
h1 {
color: var(--dark-grey);
}
form {
text-align: center;
}
button {
cursor: pointer;
}
button {
cursor: pointer;
width: 100px;
margin: 3px;
color: var(--dark-grey);
background-color: var(--golden-yellow);
background-image: linear-gradient(#fecc4c, #ffac33);
border-color: var(--golden-yellow);
border-width: 3px;
}
button:hover {
background-image: linear-gradient(#ffcc4c, #f89808);
}
input {
color: var(--dark-grey);
margin-left: 5px;
padding: 3px;
}
li {
text-align: left;
margin: 10px 0;
}
.selected-item {
font-weight: bold;
}
@media (min-width: 425px) {
.container {
width: 400px;
}
}
```
```jsx
const { useState } = React;
const items = [
"Apples",
"Bananas",
"Strawberries",
"Blueberries",
"Mangoes",
"Pineapple",
"Lettuce",
"Broccoli",
"Paper Towels",
"Dish Soap",
];
export const ShoppingList = () => {
const [query, setQuery] = useState("");
const [selectedItems, setSelectedItems] = useState([]);
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
const toggleItem = (item) => {
setSelectedItems((prev) =>
--fcc-editable-region--
prev.includes(item) ? prev.filter((i) => i !== item) : null
--fcc-editable-region--
);
};
return (
<div className="container">
<h1>Shopping List</h1>
<form>
<label htmlFor="search">Search for an item:</label>
<input
id="search"
type="search"
placeholder="Search..."
aria-describedby="search-description"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p id="search-description">Type to filter the list below:</p>
<ul>
{filteredItems.map((item) =>
<li key={item}>
<label>
<input
type="checkbox"
onChange={() => toggleItem(item)}
/>
{item}
</label>
</li>
)}
</ul>
</form>
</div>
);
};
```

View File

@@ -1078,6 +1078,9 @@
{
"dashedName": "lecture-working-with-data-fetching-and-memoization-in-react"
},
{
"dashedName": "workshop-shopping-list-app"
},
{
"dashedName": "lab-currency-converter"
},