# Drawing the Page from Your Data

Keep what the page shows in an array, write one render function that draws the page from it, and have every listener change the data and call render, so the page and the data never disagree.

Journeyman · JavaScript · Web Development · Updated November 1, 2026

## After this lesson you can

- Write a render function that draws a list on the page from an array of objects
- Change the data or the filter in a listener, then call render, so the page always matches the data

## Key takeaways

- Keep what the page shows in your data, such as an array of recipe objects, and treat the page as a drawing of that data.
- A render function empties the list and draws it again from the data, every time it's called.
- A listener changes the data, then calls render. It never edits the cards by hand, so the page and the data can't disagree.
- A filter only chooses what to draw. Nothing is removed from the data, so choosing All brings every recipe back.
- Drawing again replaces the buttons, so after render, put focus back on the new copy of the button that was pressed.

Weeknight Recipes now has three things that change: which meal is chosen, which recipes have a heart, and which card is open. Let each listener edit the page by hand, and sooner or later they disagree: the heart is red on the card, but the Favorites filter leaves that recipe out; a recipe hidden by one filter never comes back after the next. Bugs like these show up in every page with a list that changes, from a to-do app to a shopping cart, and the way to avoid them is a habit you'll use on every page whose content changes: keep the truth in your data, and draw the page from it.

## The data is the truth; the page is a drawing of it

At a basketball game, the scorekeeper's sheet is the truth, and the big scoreboard shows what's on the sheet. When a team scores, the scorekeeper writes it on the sheet and the board updates from the sheet. Nobody climbs up a ladder to change the board by hand.

Your page works best the same way. The **data** is the sheet: here, an array of recipe objects. The page is the scoreboard. A function that draws the page from the data is called a **render function** ("render" means "draw").

```javascript
const recipes = [
  { id: 'fajitas', name: 'Sheet-Pan Fajitas', meal: 'dinner', minutes: 30, favorite: false },
  { id: 'oats', name: 'Overnight Oats', meal: 'breakfast', minutes: 5, favorite: true },
  { id: 'soup', name: 'Tomato Soup', meal: 'lunch', minutes: 25, favorite: false },
  { id: 'fried-rice', name: 'Veggie Fried Rice', meal: 'dinner', minutes: 20, favorite: false },
];
```

In your textbook, the data a page keeps is often called its **state**, and keeping it in one place is called having a *single source of truth*: when the data and the page could disagree, the data wins.

## One render function

`render` makes the page match the data, starting from nothing every time. `list.replaceChildren()`, with nothing in the parentheses, empties the list, so drawing twice never shows each recipe twice. Then it builds one card per recipe, with `makeCard`, below:

```javascript
const list = document.querySelector('#recipe-list');

function render() {
  list.replaceChildren();
  for (const recipe of recipes) {
    list.appendChild(makeCard(recipe));
  }
}

render();
```

`makeCard` builds one card from one recipe with `createElement` and `appendChild`, from *Manipulating the DOM Structure*. Everything about the card comes from the recipe: its `data-id`, its title, and its heart, with `aria-pressed` from *Keyboard Users and Accessible Controls*:

```javascript
function makeCard(recipe) {
  const card = document.createElement('li');
  card.className = 'recipe-card';
  card.dataset.id = recipe.id;

  const title = document.createElement('h3');
  title.textContent = `${recipe.name}, ${recipe.minutes} min`;
  card.appendChild(title);

  const heart = document.createElement('button');
  heart.className = 'favorite';
  heart.setAttribute('aria-pressed', String(recipe.favorite));
  heart.textContent = recipe.favorite ? '♥ Favorite' : '♡ Favorite';
  card.appendChild(heart);
  return card;
}
```

`className` sets the element's `class` attribute, and `dataset.id = recipe.id` gives it `data-id="fajitas"`. With the four recipes above, the page shows four cards, and Overnight Oats already has a filled heart, because its data says `favorite: true`.

## Change the data, then render

Now a click on a heart. The listener doesn't touch the heart at all. It finds the recipe in the data, changes it, and calls `render`:

```javascript
list.addEventListener('click', (event) => {
  const heart = event.target.closest('.favorite');
  if (!heart) {
    return;
  }
  const id = heart.closest('.recipe-card').dataset.id;
  const recipe = recipes.find((item) => item.id === id);
  recipe.favorite = !recipe.favorite;
  render();
});
```

`render` draws the card again, and the new heart is filled because the data says so. There's no second place to forget: the text, the `aria-pressed` value and the data are always the same, because two of them are drawn from the third.

The listener sits on the list, as in *Bubbling and Event Delegation*. `render` replaces every card and every button inside the list, but the list itself stays, so its one listener keeps working for the new buttons.

## Filtering without deleting

A filter is one more piece of data: which meal is chosen. Keep it in a variable, and let `render` decide what to draw:

```javascript
let mealFilter = 'all';

function render() {
  const shown = mealFilter === 'all'
    ? recipes
    : recipes.filter((recipe) => recipe.meal === mealFilter);
  list.replaceChildren();
  for (const recipe of shown) {
    list.appendChild(makeCard(recipe));
  }
  count.textContent = `Showing ${shown.length} recipes`;
  for (const button of filterBar.querySelectorAll('button')) {
    button.setAttribute('aria-pressed', String(button.dataset.meal === mealFilter));
  }
}
```

`filter`, from *map and filter*, makes a new array and leaves `recipes` as it was. Each filter button has a `data-meal` attribute, such as `data-meal="dinner"`, and its listener only sets `mealFilter = button.dataset.meal` and calls `render()`. Choose **Dinner** and the page shows `Showing 2 recipes`: the fajitas and the fried rice. Heart the fajitas, choose **All**, and all four come back, with the fajitas' heart still filled. Nothing was ever deleted; the filter only chose what to draw. The same `render` also marks the chosen filter button with `aria-pressed="true"`, so the filter bar can't disagree with the data either.

## The thinking part can be tested

Which recipes to show is a question with no page in it, so it can be a function of its own that takes data and gives back data. A function like that runs in Node, and tests can check it:

```javascript
function visibleRecipes(recipes, meal) {
  if (meal === 'all') {
    return recipes;
  }
  return recipes.filter((recipe) => recipe.meal === meal);
}

const recipes = [
  { id: 'fajitas', name: 'Sheet-Pan Fajitas', meal: 'dinner' },
  { id: 'oats', name: 'Overnight Oats', meal: 'breakfast' },
  { id: 'fried-rice', name: 'Veggie Fried Rice', meal: 'dinner' },
];

console.log(visibleRecipes(recipes, 'dinner').map((recipe) => recipe.name));
console.log(visibleRecipes(recipes, 'all').length);
console.log(visibleRecipes(recipes, 'snack'));
console.log(recipes.length);
```

```text
[ 'Sheet-Pan Fajitas', 'Veggie Fried Rice' ]
3
[]
3
```

`render` then starts with `const shown = visibleRecipes(recipes, mealFilter);`. This split, the thinking in functions with no page and the page code that only reads, calls and draws, is how the exercise after this lesson is built.

**Try it.** Save the program above as `visible.js` in your `weeknight` folder and run it with `node visible.js`. Add a recipe for lunch, and print `visibleRecipes(recipes, 'lunch')`.

## One catch: putting focus back

`render` throws away the old buttons and makes new ones. For a mouse user, nothing looks different. For a keyboard user, the heart they just pressed with the space bar is gone, and their focus goes with it, back to the page itself: they've lost their place in the list. Put focus back on the new copy of the button, right after `render`:

```javascript
render();
list.querySelector(`[data-id="${id}"] .favorite`).focus();
```

The square brackets in a selector find an element by an attribute: `[data-id="fajitas"] .favorite` is "the heart inside the card whose `data-id` is fajitas". Add this line at the end of the heart listener, after `render()`, and the focus ring stays on the heart that was pressed.

![The Weeknight Recipes page with the Dinner filter pressed, showing two recipe cards, the fajitas card with a filled heart, and the text Showing 2 recipes](https://fsdtrainingmedia.blob.core.windows.net/public/images/2026/10/8a5b5946dc1f46028db30d2674de6e78.webp)

## Check yourself

2 questions to try on the lesson page.

## So what

Data in an array, one `render` function, and listeners that change the data and call `render`: that's the idea that React, Vue and Angular are built on. Those frameworks call `render` for you and keep focus where it was, and now you know what they're doing underneath.

## Key terms

- [Array](https://www.ask-codey.com/glossary/array-javascript-2)
- [filter()](https://www.ask-codey.com/glossary/filter-javascript)
- [createElement()](https://www.ask-codey.com/glossary/createelement)
- Single Source of Truth

## Cheat sheets

- [Drawing the Page from Your Data](https://www.ask-codey.com/cheat-sheets/javascript-rendering-from-data-cheat-sheet)

## Related

- [Bubbling and Event Delegation](https://www.ask-codey.com/lessons/javascript-events-bubbling-and-delegation)
- [map and filter](https://www.ask-codey.com/lessons/javascript-array-methods-map-and-filter)
- [Manipulating the DOM Structure](https://www.ask-codey.com/lessons/working-with-elements-manipulating-the-dom-structure)

Source: https://www.ask-codey.com/lessons/javascript-rendering-from-data
