Webbo3 Level 2 · JavaScript Development · Day 9 of 20
Working With Real Data
Search, filter, sort and page through a real API response without the interface ever lying to the person using it.
By the end of today you can
- Fetch once and re-render many times from data you already hold
- Sort text the way people expect with
localeCompare, including a tiebreak - Search without firing a request on every keystroke, using a debounce
- Page through a long list, and filter before you page
- Reshape an awkward API response into the shape your code wants
- Show loading, error, empty and list states that each mean something
Yesterday you got data onto the page. Today you make it usable, which is a different job.
Two hundred results with no search is not a feature, it is a wall. And every control you add is a chance to tell the user something untrue: an empty list that looks broken, a spinner that never stops, a search that fires forty requests while somebody types their name.
1. One Fetch, Many Renders
Definition
Fetch once, render many means you fetch the data into one array when the page loads, then search, filter and sort that array locally every time the user changes something. You do not go back to the network for every letter typed.
Real-Life Example
A librarian photocopies the class list once in the morning. When a teacher asks "who is from Lagos?", the librarian looks at the copy on the desk. They do not walk to the records office for every question.
How It Works
- Load: fetch into one array, once.
- Derive: a function works out which items to show from the search, sort and page.
- Render: draw what that function returns, every time something changes.
This is your Day 5 project's shape again, now with the array coming from an API.
Local filtering is right while the whole list fits comfortably in memory: hundreds of records, not hundreds of thousands. Past that, you send the search to the API and let the database do it. The signal that you have crossed the line is the page becoming slow to type in.
Picture the photocopied class list on the librarian's desk. Every question is answered from the copy; the records office is visited once.
2. Sorting Real Data
Definition
a.localeCompare(b) compares two pieces of text the way a person would, and gives back a negative number, zero or a positive number, exactly what sort needs.
Real-Life Example
Arranging a class list. A person puts "ade" and "Ade" next to each other. A computer comparing raw character codes puts every capital letter before every small one, so "Zainab" lands before "ade".
Types of Sort You Will Need
- Text, in the order a person expects.
- Numbers, with a second rule for ties.
- Dates, compared as dates rather than as text.
Type 1: Text
[...names] sorts a copy (Day 3), so the original list keeps its order.
Type 2: Numbers With a Tiebreak
When two scores are equal, b.score - a.score is 0, which is falsy, so || moves on to the name comparison. That is a tiebreak in one line.
Type 3: Dates
Subtracting two Date objects gives the milliseconds between them, which sort can use directly.
Picture two people arranging the same register: one reads the names like a person, the other reads raw character codes. localeCompare makes the computer read like the person.
3. Debouncing: Stop Firing on Every Keystroke
Definition
Debouncing waits until someone stops typing for a moment, and only then does the work. Each new keystroke cancels the plan made by the previous one.
Real-Life Example
A lift door. Every time someone steps in, the door waits a little longer before closing. It only closes once nobody has come in for a few seconds. It does not close and reopen for every person.
Why You Need It
An input listener runs on every character. Typing "Chidinma" fires it eight times. If each one hits the network, you send eight requests for one answer, and they can come back out of order, so the screen ends up showing the result for "Chidinm".
How It Works
- Each call runs
clearTimeout(timer), which cancels the call planned by the last keystroke. - Then it plans a fresh call with
setTimeout. - Only the last keystroke in a burst survives the pause.
debounce takes a function and gives back a new function (functions can return functions). ...args passes along whatever it was called with.
The same function in a real search box, with real typing:
Debounce waits for a pause: right for search boxes and window resizing. Throttle runs at most once every so many milliseconds: right for scroll position and dragging. "When they stop" is debounce; "regularly while it happens" is throttle.
Picture the lift door that keeps waiting while people step in, and closes only once the stream of people stops.
4. Paging
Definition
Paging shows a long list a fixed number of items at a time. Page 1 is items 1 to 10, page 2 is 11 to 20, and so on.
Real-Life Example
A textbook does not print all 400 exercises on one sheet. It splits them into pages, tells you how many pages there are, and you turn to the one you need.
How It Works
| Piece | Code | Why |
|---|---|---|
| Where a page starts | (page - 1) * size | Page 1 starts at index 0 |
| The items on it | items.slice(start, start + size) | slice copies a section and never fails past the end |
| How many pages | Math.ceil(total / size) | Rounds UP, so the last few items get a page |
Array.from({ length: 23 }, (_, i) => ...) builds a list of 23 items. The _ is a parameter we do not need, named so on purpose; {C("i")} is the position.
Common Mistake: Paging Before Filtering
Paging first cuts five records out of the whole list and then filters only those five, so page 1 shows almost nothing. Narrow first, cut into pages second, and reset to page 1 whenever the search or filter changes.
Picture tearing out page 1 of the textbook before highlighting the questions on your topic, instead of highlighting first and then reading the highlighted ones in order.
5. Reshaping an Awkward Response
Definition
Normalising means converting the data an API sends into the shape your code wants, once, as soon as it arrives. The rest of your code then never has to know the API was awkward.
Real-Life Example
A shop receives stock in the supplier's boxes, with the supplier's labels. At the back door, staff unpack it onto the shop's own shelves with the shop's own price tags. Nobody on the shop floor ever sees the supplier's boxes.
Three real problems fixed in one map: the name was split in two, the course was null for a student with no course, and the score arrived as text, so arithmetic on it would have joined strings instead of adding numbers (Day 1).
Picture unpacking at the back door: supplier boxes in, shop shelves out, and the shop floor never sees a supplier label.
6. The Four States, Properly This Time
Definition
Every screen that shows fetched data is always in exactly one of four states: loading, error, empty or showing the list. Your render function decides which, in that order, every time.
Real-Life Example
A departures board at an airport: "Updating" while it refreshes, "Information unavailable" if it loses its feed, "No more departures today" late at night, and otherwise the flights. It is never blank, and it never shows old flights as if they were current.
| If | Show | Never show |
|---|---|---|
| The request is in flight | Loading, and disable the button | A blank screen |
| It failed | What went wrong in plain words, and a Retry | [object Object], or a stack trace |
| It worked with zero items | "No results for "xyz"" and a way to clear | An empty area with no explanation |
| It worked with items | The list | A spinner still turning |
A search that matched nothing has worked perfectly. Say so in those terms, name what was searched for so the user can spot their own typo, and give them a way back. An empty list dressed up as a failure teaches people not to trust your app.
Picture the departures board: four honest messages, never blank, never stale.
Do This Now
Build a browser for a real public API that returns at least fifty records. Good choices: https://restcountries.com/v3.1/region/africa or https://api.github.com/users/NAME/repos.
- Fetch once on load into one array.
- Normalise the response into your own shape in one map, before anything else touches it.
- A search box, debounced at about 300ms, filtering locally.
- A sort control with at least two options, one of them text using localeCompare.
- Paging at ten per page, filtering first and resetting to page 1 whenever the search changes.
- All four states: loading, error with retry, empty with the search quoted back, and the list.
Break the URL on purpose and confirm the error state appears. Search for zzzz and confirm the empty state appears. In DevTools, Network tab, throttle to Slow 3G and watch your loading state for a few seconds. If you have not seen all three yourself, you have not built them.
Checkpoint
Q1. Why filter before paging rather than after?
Paging first cuts ten records out of the unfiltered list and then filters those ten, so pages come back nearly empty. Narrow the list first, then cut it into pages.
Q2. What does clearTimeout do inside a debounce?
It cancels the call planned by the previous keystroke. Every new character throws away the pending call and plans a fresh one, so only the last one in a burst ever runs.
Q3. Why does ["ade", "Zainab"].sort() put Zainab first?
The default sort compares character codes, and every capital letter has a lower code than every small one. Use localeCompare for anything a person will read.
Q4. Zero results came back. Is that an error?
No. The request worked and the honest answer was "nothing matches". Show an empty state naming what was searched for, not an error.
Tick Before You Move On
- ☐ I fetch once and filter locally, not on every keystroke
- ☐ I sort text with localeCompare and wrote a tiebreak with ||
- ☐ My search is debounced, and I proved it by counting the calls
- ☐ I filter before paging, and a new search resets the page to 1
- ☐ The API response is normalised in one place before anything else uses it
- ☐ I have seen all four states on my own screen, including on Slow 3G
Quick Recap
Fetch once into one array; search, sort and page that array locally. localeCompare sorts text the way people read it, and || writes a tiebreak in one line. Debounce waits for a pause; throttle runs at a steady rate. Filter first, page second, and reset the page when the filter changes. Normalise an awkward response once, at the edge. Loading, error, empty and list are four different screens, and all four are yours to design.
Final Scenario
A student opens your country explorer on a slow connection, types "nig", pauses, and finds nothing because they meant "Nigeria" and typed "nig" into a list sorted and paged in the wrong order.
With today's structure it goes differently: one fetch, a loading message while it arrives, a single search when they pause, results filtered before they are paged, and if nothing matches, a message that shows them exactly what they typed.
Nothing on the screen is ever false. That is what makes real data feel usable.
Tomorrow, Day 10: Git, GitHub and your first group project, and the workflow that lets five people work on one codebase without destroying each other's work.