# Week 5

Last week, we made one React component and used it twice by hand. We still wrote each `<InterestCard />` separately in `App.jsx`. This week, we'll put our interests in a list and have React make those same cards from that list.

We'll also add a search field. Last week, state remembered a title for a click. This week, state remembers search text for typing. A merch page will need the same ideas to show a list of products, remember a selected size, and respond to changes in a cart.

By the end of the meeting, your page should display cards from an array and remember the text you type into an input. We'll finish by using that text to filter the cards. If you need more time, finish the filtering before next week; keep the working list and input first.

## Getting started

The complete practice files are included at the end of this lesson.

Open the whole `practice-page` repository in VS Code. Save and commit any finished Week 4 work before starting this lesson. From a terminal at the repository's top level, run:

```sh
git status
git switch -c interests-state
cd react-page
npm run dev
```

The branch starts from your current saved Week 4 work. If `git status` shows unfinished changes you do not recognize, ask for help before switching branches. Open the exact local address printed by the development server.

We'll keep using the same React project. You can compare your work with the complete practice files below. The reference files go in `react-page/src`. Copy the supplied `App.css` into `src` before continuing. It adds spacing and visible keyboard focus for the input and works alongside the existing `styles.css`. Keep last week's `InterestCard.jsx` — we will drive it from the array.

## Objects and arrays

An object groups information about one thing under property names. Last week, the `props` object held a card's title and description. We can make our own object too:

```js
const interest = {
  id: "games",
  title: "Video games",
  description: "I like learning how games are designed."
};
```

The outer braces hold the object. Each property has a name, a colon, and a value. Commas separate the properties. `interest.title` reads the value under `title`, which is `"Video games"`.

The `id` is an identifier: a value that distinguishes this item from the others. We choose it once and keep it the same even if we change the title.

An array is an ordered list. Create `src/interests.js` and add:

```js
export const interests = [
  {
    id: "games",
    title: "Video games",
    description: "I like learning how games are designed."
  },
  {
    id: "photos",
    title: "Photography",
    description: "I like taking pictures around Hoboken."
  }
];
```

The square brackets, `[` and `]`, hold the array. Each item here is an object, and a comma separates the two objects. `interests[0]` gets the first item; array positions start at zero. `interests[0].title` gets that item's title.

`export` lets another file import this value. This is a named export, so we will use the same name inside braces when importing it.

Save `interests.js`. You should have two objects inside one array. The two-item array above keeps the walkthrough short. The supplied reference adds a Cooking interest. Its objects have the same fields. Searching for `games` still keeps only Video games.

## A function that receives and returns a value

A function can receive information when we call it. In this function, `interest` is the parameter: the name used for the input inside the function.

Last week we wrote two `<InterestCard />`s by hand. This week one function returns that same component for whichever object we give it:

```jsx
function renderInterest(interest) {
  return (
    <InterestCard
      key={interest.id}
      title={interest.title}
      description={interest.description}
    />
  );
}
```

If we call `renderInterest(interests[0])`, we give it the first object. That supplied value is called an argument. Inside that call, `interest.title` becomes the card's title prop.

`return` sends a result back to the code that called the function. Here, the result is JSX describing one `InterestCard`. The parentheses let us spread that JSX over several lines. The function does not change the original object.

`InterestCard` still uses an `<h3>` for the title, just like last week. We'll use `key` in the next step. It gives React the item's stable identity when it appears in a list.

## Making the cards from the list

Replace `src/App.jsx` with this small version while we learn the list code. Your earlier profile is still saved in Git. Keep `InterestCard.jsx` in the folder — we import it here.

```jsx
import InterestCard from "./InterestCard.jsx";
import { interests } from "./interests.js";
import "./App.css";

function renderInterest(interest) {
  return (
    <InterestCard
      key={interest.id}
      title={interest.title}
      description={interest.description}
    />
  );
}

export default function App() {
  return (
    <main>
      <h1>Things I like</h1>
      <div className="interest-list">
        {interests.map(renderInterest)}
      </div>
    </main>
  );
}
```

`.map()` is an array method, which means it is a function available on an array. It calls `renderInterest` once for each item and collects the returned cards into a new array. The JSX braces display that result. We pass `renderInterest` without parentheses so `.map()` can call it with each item.

`key={interest.id}` helps React match a rendered card to its data when the list changes. Each item in this list needs a different key. Use the saved ID, not a newly generated random value or the item's current position. Put `key` on the `InterestCard` in the list, not inside the component file.

Save both files. You should see two cards that look like last week's.

Now add a third object to `interests.js` with a different ID, title, and description. Leave `App.jsx` alone. Save and check that a third card appears. Tell the person next to you how one function produced three different cards.

| Job | Last week | This week |
| --- | --- | --- |
| Show two cards | Write two `<InterestCard />`s by hand. | `.map` over the `interests` array. |
| Change a title | Edit the prop in JSX. | Change it in the array (or later, filter which objects show). |
| Remember a value | Click sets title state. | Typing sets query state. |

## Remembering what someone types

Last week, `useState` remembered a title so a click could change a card. This week it remembers search text so typing can change what we show. Same two jobs: React remembers the value between renders, and the setter requests an update.

A render is React calling a component to work out what the page should show. When we update state, React renders the component again using the new value.

Why not write `let query = ""` inside `App`? Each call to `App` would run that declaration again and start with an empty string. Assigning a different string in a handler also would not tell React to call `App` again. State still provides both pieces.

Add this import at the top of `App.jsx`:

```jsx
import { useState } from "react";
```

Call `useState` at the top of `App`, before its `return` — same top-level rule as last week:

```jsx
const [query, setQuery] = useState("");
```

`useState("")` starts with an empty string. It returns an array containing two things: the current value and a function that updates it. The square-bracket syntax gives those two things names in order. Here, `query` is the search text for this render. `setQuery` asks React to remember a value for the next render.

The empty string is the initial value. React does not reset the search to it every time `App` runs. On later renders of this same component, `useState` gives us the state React has remembered.

Keep this call at the top level of the component. Do not put it inside an event handler or an `if` statement.

Inside `App`, after that state line and before `return`, add:

```jsx
function handleSearch(event) {
  setQuery(event.target.value);
}
```

React passes information about the input event to `handleSearch`. `event.target` is the input that changed, and its `value` is the text currently in it. We give that text to `setQuery`.

Below the heading in the JSX, add:

```jsx
<label htmlFor="interest-search">Search interests</label>
<input
  id="interest-search"
  type="search"
  value={query}
  onChange={handleSearch}
/>
<p>You typed: {query}</p>
```

The label stays visible and tells someone what the field is for. JSX uses `htmlFor` to connect it to the input's matching `id`. `type="search"` describes the kind of text field.

`value={query}` makes React state control the field's displayed value. `onChange={handleSearch}` updates that state when someone types. Together these make a controlled input.

`onChange={handleSearch}` passes the function for React to call when the input changes. Defining that function inside `App` does not run its body during rendering. Writing `onChange={handleSearch()}` would call it immediately while calculating the JSX, before there is an input event to handle.

Save and type. The sentence below the field should repeat the text immediately. Clearing the field should clear that text too. Use `setQuery(...)` to update the state; assigning to `query` directly does not ask React to update the page.

## Follow one page update

Start with the empty search and type `g`. Follow the value through the code we just wrote:


![Typing g calls handleSearch and setQuery. React renders App, compares the result with the last one, commits the You typed paragraph text, and the browser paints it. The same input keeps focus.](https://wiki.sitblueprint.com/uploads/images/gallery/2026-10/react-type-update.png)

1. The browser changes the input's text. React calls `handleSearch` for that change, and `event.target.value` contains `"g"`.
2. The handler calls `setQuery("g")`. This requests new state; it does not edit the paragraph itself.
3. React calls `App` again. `useState` now supplies `"g"` as `query`. The JSX describes the input with that value and a paragraph saying "You typed: g".
4. React commits the needed DOM changes. The paragraph's text changes. The same input element stays on the page with keyboard focus, so you can keep typing.
5. The browser paints the result. The document has stayed open throughout. We have not navigated to a new page or reloaded the HTML file.

The cards have not changed yet because this version still reads the same list. A render can update one part of the page and leave another alone.

## Which value does the handler read?

Let's say the search starts empty and you type `g`. The input now contains `g`, so the event gives us that new text. But what does `query` contain while the handler runs?

Temporarily replace `handleSearch` with this version and open the browser's Console:

```jsx
function handleSearch(event) {
  const nextQuery = event.target.value;
  setQuery(nextQuery);
  console.log("This render's query:", query);
  console.log("The input's new text:", nextQuery);
}
```

Start with an empty field and type `g` once. The first log shows the empty string and the second shows `g`. `setQuery` requested another render; it did not rewrite `query` inside this handler. React calls `App` again with the new state, and that render's `query` is `g`. Each render gets its own values, which is why a `const` declaration can give `query` different text on the next render.

Remove the two `console.log` lines after checking them. Keep `nextQuery` and `setQuery(nextQuery)` as your handler. The supplied reference uses that version.

## Showing matching interests

We can use the search text to choose which objects become cards. This is the next part of the same exercise, so keep the working input before making this change.

Add this inside `App`, below `handleSearch` and above `return`:

```jsx
function matchesSearch(interest) {
  return interest.title.toLowerCase().includes(query.toLowerCase());
}

const filteredInterests = interests.filter(matchesSearch);
```

`.toLowerCase()` returns a lowercase version of a string. `.includes(...)` checks whether a string contains the supplied text and returns `true` or `false`. These are Boolean values: yes or no answers. Comparing lowercase versions lets `games` and `GAMES` find the same title.

`.filter()` calls `matchesSearch` for each interest. It returns a new array containing the items whose answer was `true`. It keeps the original array intact. An empty query matches every title.

In the JSX, replace `interests.map(renderInterest)` with:

```jsx
{filteredInterests.map(renderInterest)}
```

Now `filter` chooses the objects, then `map` makes their cards. We calculate `filteredInterests` from the current `query` each time React renders `App`. It does not need separate state.

Save. Try `games`, `GAMES`, and a word that is in none of the titles. When `query` reaches `"games"`, that render keeps the Video games object and leaves out titles that do not match. The JSX describes the remaining cards, and the commit removes the other cards from the DOM. The existing Video games card can stay because its type and key still match.

When nothing matches, an empty page is not much help. Below the list, add:

```jsx
{filteredInterests.length === 0 && (
  <p>No interests match. Clear the search to see them all.</p>
)}
```

`.length` is the number of items in the array. `===` compares values; here it asks whether that count is exactly zero. `&&` means “and.” In this JSX expression, the paragraph appears only when the comparison is true. This is conditional rendering: choosing what to display based on a condition.


![With query games, filter keeps the Video games object and leaves out Photography. map turns that object into an InterestCard with key games. The original interests array still contains both objects.](https://wiki.sitblueprint.com/uploads/images/gallery/2026-10/search-filter-map.png)

With `query` set to `"games"`, the filter keeps the Video games object. The map then describes one card. Clearing the input lets both objects pass the filter again; neither object was deleted.

Save and check: a matching search shrinks the list, a missing word shows the no-match message, and clearing the field restores every card.

## When something goes wrong

| What you see | What to check |
| --- | --- |
| Only one card appears after adding an object | Confirm the new object is inside the array's square brackets and separated from the previous object by a comma. |
| `.map is not a function` | Check that `interests` is the array, not one object such as `interests[0]`. |
| The list is blank with no syntax error | Make sure `renderInterest` returns its JSX. A function with no `return` does not give `.map()` a card. |
| React warns about keys | Give each object its own ID and keep `key={interest.id}` on the `InterestCard` in the list. |
| The input will not change | Check that `onChange` names `handleSearch` and that the handler passes the input's new text to `setQuery`. |
| React reports too many renders | Pass `handleSearch` to the event prop. Do not call a state setter while rendering the JSX. |
| Search always finds nothing | Compare the property names, then clear the query and try a title that appears in your data. |

## What's next

Next week, the interest data will come from an HTTP response instead of a local import. Keep today's working list and search so you can compare imported data with data the browser loads.

The complete practice files are on this page. Use them to compare a file that is not working.


## Resources

Everything needed for today's code and explanations is above. These are extra examples or references to return to after the exercise.

- [MDN: JavaScript object basics](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Object_basics)
- [MDN: Arrays](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Arrays)
- [MDN: Functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions)
- [MDN: Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
- [MDN: Array.filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)
- [React: Rendering Lists](https://react.dev/learn/rendering-lists)
- [React: State, a component's memory](https://react.dev/learn/state-a-components-memory)
- [React: Controlling an input with state](https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable)
- [React: Responding to events](https://react.dev/learn/responding-to-events)
- [React: Conditional Rendering](https://react.dev/learn/conditional-rendering)

## Complete practice files

Copy these files into the paths shown below. They are the same files used by the lesson. Keep the filenames and imports together. Keep last week's `InterestCard.jsx` (or copy the version below if you need it).

### `src/InterestCard.jsx`

```jsx
export default function InterestCard(props) {
  return (
    <article className="interest-card">
      <h3>{props.title}</h3>
      <p>{props.description}</p>
    </article>
  );
}
```

### `src/App.jsx`

```jsx
import { useState } from "react";
import InterestCard from "./InterestCard.jsx";
import { interests } from "./interests.js";
import "./App.css";

function renderInterest(interest) {
  return (
    <InterestCard
      key={interest.id}
      title={interest.title}
      description={interest.description}
    />
  );
}

export default function App() {
  const [query, setQuery] = useState("");

  function handleSearch(event) {
    const nextQuery = event.target.value;
    setQuery(nextQuery);
  }

  function handleClear() {
    setQuery("");
  }

  function matchesSearch(interest) {
    return interest.title.toLowerCase().includes(query.toLowerCase());
  }

  const filteredInterests = interests.filter(matchesSearch);

  return (
    <main>
      <h1>Things I like</h1>
      <p>This page reads each interest from an array.</p>

      <label htmlFor="interest-search">Search interests</label>
      <input
        id="interest-search"
        type="search"
        value={query}
        onChange={handleSearch}
        placeholder="Try photography"
      />
      <button type="button" onClick={handleClear}>
        Clear search
      </button>

      <p role="status">Interests shown: {filteredInterests.length}</p>
      <div className="interest-list">
        {filteredInterests.map(renderInterest)}
      </div>
      {filteredInterests.length === 0 && (
        <p>No interests match. Clear the search to see them all.</p>
      )}
    </main>
  );
}
```

### `src/interests.js`

```js
export const interests = [
  {
    id: "games",
    title: "Video games",
    description: "I like learning how games are designed.",
  },
  {
    id: "photos",
    title: "Photography",
    description: "I like taking pictures around Hoboken.",
  },
  {
    id: "cooking",
    title: "Cooking",
    description: "I like trying a new recipe each week.",
  },
];
```

### `src/App.css`

```css
.interest-card h3 {
  font-size: 1.15rem;
  margin: 0;
}

label {
  display: block;
  font-weight: 600;
  margin-bottom: 8px;
}

input {
  width: 100%;
  font: inherit;
  color: inherit;
  padding: 12px;
  border: 1px solid #64748b;
  border-radius: 8px;
}

input:focus-visible {
  outline: 3px solid #1d4ed8;
  outline-offset: 3px;
}

button {
  margin-top: 12px;
  font: inherit;
  padding: 8px 14px;
  border: 1px solid #64748b;
  border-radius: 8px;
  background: #f8fafc;
  cursor: pointer;
}

button:focus-visible {
  outline: 3px solid #1d4ed8;
  outline-offset: 3px;
}
```