UI State

The Course
•
5 min read

The interface should explain what is happening

A blank list can mean no search yet, work in progress, no matches, or a failed request. Those are different states. Represent them explicitly rather than inferring them from unrelated booleans. Our model uses status values idle, loading, ready, and error; an empty results array with ready means a successful empty search.

Derive the displayed message from state. Avoid a second variable containing an old message that must be updated in every branch. Render should be a projection of the current state, not another source of application truth.

In HTML, a paragraph with role=status can announce changed feedback without moving keyboard focus. aria-busy on the results region communicates ongoing work. Keep a submit button available if the design permits a newer query to replace an old one. Disable Cancel when no request is pending. Choose the behavior deliberately instead of disabling the entire page.

The console function below is pure and has no DOM dependency. In the browser, assign its returned string to textContent. The complete app additionally creates list rows from results; the same status rule applies. A failure message should offer a useful next action while retaining enough detail for the learner to diagnose the controlled scenario.

Predict and trace

Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.

Predict, then run JavaScript
function statusText(state) {
  if (state.status === "loading") {
    return "Searching…";
  }

  if (state.status === "error") {
    return state.error;
  }

  if (state.status === "ready") {
    return `${state.results.length} result(s)`;
  }

  return "Enter a topic";
}

console.log(statusText({ status: "idle" }));
console.log(statusText({ status: "loading" }));
console.log(statusText({ status: "ready", results: [] }));
console.log(statusText({ status: "error", error: "Try again" }));
Check your prediction
Enter a topic
Searching…
0 result(s)
Try again

Empty results belong to ready, not error. A successful request can legitimately return no matches.

An empty success is different from no response

Model what happened rather than inferring it from the list length. An empty successful result, a request that has not started and a failed request can all have an empty list. A status field carries the missing distinction.

State transitions also decide whether earlier data remains visible while refreshing. That is a user-facing policy, not something an if statement can choose for you. Keep error text scoped to the state that owns it so a later success does not display an old failure.

An empty success is different from no response: trace JavaScript
function message(state) {
  if (state.status === "idle") {
    return "Start a search";
  }

  if (state.status === "loading") {
    return "Loading";
  }

  if (state.status === "error") {
    return state.message;
  }

  return state.items.length === 0 ? "No matches" : "Results ready";
}

console.log(message({ status: "idle", items: [] }));
console.log(message({ status: "ready", items: [] }));
Check your prediction
Start a search
No matches

The same items value supports two distinct messages because the status identifies what happened. An items.length-only renderer would collapse them.

An empty success is different from no response: contrast JavaScript
const before = { status: "error", items: [], message: "Offline" };
const after = { ...before, status: "ready", items: ["Notes"], message: "" };

console.log(after.status);
console.log(after.message === "");
console.log(before.message);
Check your prediction
ready
true
Offline

The success transition clears error state in the new record without mutating the previous one. Rendering only the right branch helps, but clearing incompatible fields also keeps the model understandable.

Try the rule on new inputs

Write a table of idle, loading, ready-empty, ready-nonempty and error messages. Then decide whether a loading state should retain old data, and make the corresponding transition explicit.

Build it yourself

Exercise

UI State: practise

Write canCancel(state), true only for loading. Print answers for loading, ready, and error. Then describe how its inverse sets a button’s disabled property.

Your working space JavaScript
// Keep the rule separate from the DOM assignment.
Hint 1: the idea

The status is the source of truth.

Hint 2: a first step

Use a strict comparison and return the boolean.

Show one solution
function canCancel(state) {
  return state.status === "loading";
}

console.log(canCancel({ status: "loading" }));
console.log(canCancel({ status: "ready" }));
console.log(canCancel({ status: "error" }));
true
false
false

Add an idle case. If you introduce a new status such as refreshing, decide explicitly whether cancellation should be available in that state.

Test a changed requirement

Add an idle case. If you introduce a new status such as refreshing, decide explicitly whether cancellation should be available in that state.

Explain before moving on

Why is an empty array insufficient to identify the UI state? When should rendering move keyboard focus?

Was this article helpful?

Your response is saved on this device.