Web Development using JavaScript/React

Chapter 8
Routing, Forms, and User Experience

Bachelors Program in IT
Web Development using JavaScript/React   •   Bachelor

Routing, Forms, and User Experience

Introduction

A single-page application (SPA) loads an initial document and then updates its interface through JavaScript rather than requesting a complete HTML document for every interaction. This model can make applications feel fast and app-like, but it also introduces design responsibilities that traditional multi-page websites often handle automatically. Developers must manage browser history, URLs, nested views, authentication boundaries, form state, validation, loading feedback, errors, and responsive interaction.

Routing and forms are therefore not isolated implementation details. They are part of the application’s interaction architecture. A route communicates location and application state; a form converts user intent into validated data; feedback communicates whether an operation is progressing, successful, incomplete, or unsuccessful. Together, these mechanisms determine whether an application is understandable, accessible, trustworthy, and efficient.

This chapter examines routing and form design in React applications, with emphasis on nested and protected routes, controlled forms, validation, navigation patterns, responsive interaction, and usable loading and empty states.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain the role of client-side routing in a React SPA;
  • design nested routes and routes that require authentication or authorization;
  • distinguish authentication from authorization;
  • implement controlled forms with appropriate validation and error feedback;
  • select suitable navigation patterns for different user tasks;
  • design loading, empty, success, and error states;
  • apply accessibility, security, and responsive-design principles to routing and forms; and
  • evaluate a user flow using usability and maintainability criteria.

1. Routing as Application Structure

A URL is more than an address. In a well-designed application it represents a reproducible view of application state. For example:

  • /products identifies a collection view;
  • /products/42 identifies one product;
  • /products/42/edit identifies an editing task;
  • /search?q=react&page=2 identifies a filtered and paginated result; and
  • /account/settings identifies a settings section inside an account area.

A client-side router maps URL patterns to React components. When a user navigates internally, the router changes the displayed component and usually uses the History API to update the address without a full page reload. Direct visits and browser refreshes still require the server to return the SPA entry document for application routes; otherwise, a URL such as /products/42 may produce a server-level 404 before React runs.

1.1 Core routing concepts

Important routing terms include:

  • Route pattern: A URL pattern associated with a view, such as /users/:userId.
  • Route parameter: A variable segment extracted from the path, such as userId.
  • Query parameter: Optional information after ?, such as ?sort=price.
  • Nested route: A route rendered inside a parent route’s layout or outlet.
  • Index route: The default child view for a parent path.
  • Navigation: A transition from one location to another.
  • Redirect: An instruction to move the user to a different location.
  • History entry: A browser-history record created by navigation.
  • Deep link: A URL that opens a specific view rather than only the application home page.

A router normally distinguishes navigation caused by an in-app link from navigation caused by a browser reload. The former can preserve application state and avoid unnecessary network requests, while the latter reconstructs the application from the URL and server response.

1.2 Declarative routing

Modern React routing libraries commonly express route relationships declaratively. A simplified example using React Router concepts is:

import { Routes, Route } from "react-router-dom";

function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="products" element={<ProductsLayout />}>
        <Route index element={<ProductList />} />
        <Route path=":productId" element={<ProductDetails />} />
        <Route path=":productId/edit" element={<EditProduct />} />
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

The parent ProductsLayout commonly renders an Outlet, which is the position where the selected child route appears:

function ProductsLayout() {
  return (
    <section>
      <h1>Products</h1>
      <ProductNavigation />
      <Outlet />
    </section>
  );
}

This structure communicates the information architecture directly in code. It also enables shared layout elements such as breadcrumbs, tabs, side navigation, and contextual actions.

1.3 Nested routing and information architecture

Nested routes are useful when a parent context remains relevant while the child content changes. An account page might contain /account/profile, /account/security, and /account/billing. The account layout can provide navigation and a heading, while each child route manages its own content.

Nested routes should reflect meaningful conceptual relationships, not merely component composition. Excessive nesting can create confusing URLs, complicated authorization rules, and difficult back-button behavior. A useful test is whether a user would expect the child view to remain inside the parent context when navigating directly to its URL.

1.4 Navigation and browser history

A normal internal link should usually create a history entry so that the browser Back button reverses the user’s movement. Programmatic navigation that replaces the current entry is appropriate when the previous location should not be revisited, such as after a login redirect or when normalizing a malformed URL.

import { Link, useNavigate } from "react-router-dom";

function SaveButton() {
  const navigate = useNavigate();

  async function save() {
    await persistData();
    navigate("/dashboard", { replace: true });
  }

  return <button onClick={save}>Save</button>;
}

Use links for user-initiated movement and buttons for actions. Making a button look like a link, or using a link to trigger a mutation, weakens semantics and can impair keyboard and assistive-technology use.

2. Protected Routing and Access Control

A protected route is a route whose content or operation is available only under defined access conditions. Two conditions must be distinguished:

  • Authentication: establishing who the user is.
  • Authorization: determining what that authenticated user may do.

A user may be authenticated but not authorized to edit an invoice, view another department’s records, or access an administrator screen.

A client-side guard improves user experience by preventing unauthorized screens from being displayed, but it is not a security boundary. The server or API must independently verify identity and permissions for every protected operation. A malicious user can modify JavaScript, call an API directly, or construct a URL manually.

A conceptual guard might be written as follows:

function RequireAuth({ children }) {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) return <FullPageSpinner label="Checking your session" />;
  if (!user) {
    return <Navigate to="/login" replace state={{ from: location }} />;
  }
  return children;
}

After successful login, the application can return the user to the original location, provided that the destination is validated as an internal route. Redirect data must not be accepted blindly from an untrusted URL parameter; otherwise, the application may create an open-redirect vulnerability.

Role or permission checks should be explicit:

function RequirePermission({ permission, children }) {
  const { user } = useAuth();

  if (!user?.permissions.includes(permission)) {
    return <Forbidden />;
  }
  return children;
}

Protected routes should also consider session expiry, loading failures, and permission changes during a session. A clear 401 response generally indicates that authentication is absent or invalid, while 403 indicates that the identity is known but lacks permission. The exact handling depends on the API contract, but the distinction should be preserved in the user experience.

3. Forms as State, Validation, and Communication

A form is an interaction protocol, not merely a group of input elements. It collects user intent, constrains data, communicates requirements, submits an operation, and reports its result.

3.1 Controlled components

In a controlled form, React state is the authoritative representation of an input’s value. The input receives its value from state and reports changes through an event handler.

function ProfileForm() {
  const [values, setValues] = useState({
    name: "",
    email: ""
  });

  function handleChange(event) {
    const { name, value } = event.target;
    setValues(current => ({ ...current, [name]: value }));
  }

  return (
    <form>
      <label htmlFor="name">Name</label>
      <input
        id="name"
        name="name"
        value={values.name}
        onChange={handleChange}
      />

      <label htmlFor="email">Email</label>
      <input
        id="email"
        name="email"
        type="email"
        value={values.email}
        onChange={handleChange}
      />
    </form>
  );
}

Controlled inputs make it straightforward to display character counts, enable or disable actions, derive previews, reset values, and validate as the user interacts. They require careful initialization: an input should not unexpectedly switch between an undefined value and a string. For example, use value={values.name ?? ""} when data may initially be absent.

Uncontrolled inputs store their current value in the DOM and are read through a reference or form submission. They can be appropriate for simple forms, file inputs, or performance-sensitive cases. Neither approach is universally superior; the choice should follow the form’s complexity and interaction requirements.

3.2 Validation layers

Validation should occur at several layers:

  1. Semantic HTML: Use the correct input type, required, minLength, maxLength, and related attributes.
  2. Client-side validation: Give immediate, understandable feedback and prevent avoidable requests.
  3. Server-side validation: Enforce business rules and security because the client is untrusted.
  4. Persistence validation: Protect database and domain invariants.

Client-side validation is primarily a usability feature. It cannot establish trust. For example, a browser may check that an email resembles an email address, but the server must still validate ownership, uniqueness, permissions, and acceptable content.

Validation should answer three questions: what is wrong, where is it wrong, and how can it be corrected? “Invalid input” is less useful than “Enter a valid university email address, for example [email protected].”

A form can maintain values, errors, touched fields, submission status, and a general server message:

const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [status, setStatus] = useState("idle");

function validate(values) {
  const next = {};
  if (!values.title.trim()) next.title = "Title is required.";
  if (values.title.length > 80) next.title = "Use 80 characters or fewer.";
  return next;
}

Validation timing matters. Validating every field immediately can make a form feel hostile, especially when a user has not yet had an opportunity to type. A common strategy is to validate on blur after the field has been visited, validate again on submit, and clear or update the error as the user corrects the value.

3.3 Accessible error feedback

Each error should be associated with its field. The input can use aria-invalid="true" and aria-describedby to identify the message. A summary at the top is useful after submission, particularly when errors occur far below the current viewport.

<label htmlFor="title">Title</label>
<input
  id="title"
  name="title"
  value={values.title}
  onChange={handleChange}
  aria-invalid={Boolean(errors.title)}
  aria-describedby={errors.title ? "title-error" : undefined}
/>
{errors.title && (
  <p id="title-error" role="alert">{errors.title}</p>
)}

Color should not be the only error signal. Combine text, icons where appropriate, field association, and visible focus. Error messages should remain stable enough for users to understand what changed and should not disappear before they can read them.

3.4 Submission state and duplicate actions

A submission has states such as idle, editing, validating, submitting, succeeded, and failed. During submission, the application should prevent accidental duplicate requests while preserving enough context for the user to understand progress.

async function handleSubmit(event) {
  event.preventDefault();
  const nextErrors = validate(values);
  setErrors(nextErrors);
  if (Object.keys(nextErrors).length > 0) return;

  setStatus("submitting");
  try {
    await saveProfile(values);
    setStatus("success");
  } catch (error) {
    setStatus("error");
    setFormMessage("We could not save your profile. Try again.");
  }
}

Disabling a submit button can be useful, but it should not remove all possible recovery. If a request can take time, communicate that the operation is in progress and preserve entered data unless there is a compelling reason to clear it.

4. Navigation Patterns and Responsive Interaction

Different tasks call for different navigation patterns.

PatternAppropriate useMain risk
Global navigationMoving among major application areasToo many top-level choices
BreadcrumbsShowing hierarchy in deep contentPoor value for flat structures
TabsSwitching related views at the same levelTabs used for unrelated tasks
SidebarFrequent navigation in workspacesInadequate mobile adaptation
PaginationMoving through bounded result setsLosing filters or scroll context
“Load more”Extending a result listDifficult return-to-position behavior
Modal or dialogFocused, interruptive taskHidden context and accessibility errors
Wizard or stepperSequential, multi-stage taskPreventing legitimate review or backtracking

Navigation should preserve user orientation. A page title, selected navigation state, breadcrumbs, and meaningful URL all help answer “Where am I?” Links should have descriptive names rather than repeated “Click here” labels.

Responsive interaction is more than shrinking desktop layouts. A sidebar may become a drawer; a table may become stacked cards; a multi-column form may become a single column; and hover-only actions must become available through touch and keyboard. Tap targets should be sufficiently large and separated to reduce accidental activation, while focus indicators must remain visible for keyboard users.

When a route changes, focus management deserves explicit attention. A screen-reader user may otherwise remain focused on the navigation link while the main content silently changes. After navigation, focus can move to the new page heading or a main landmark, provided that this is done consistently and does not disrupt users who did not request it.

5. Loading, Empty, Error, and Success States

A polished interface designs for states beyond the successful response.

Loading states

Use a progress indicator when the duration is uncertain or substantial. For small, predictable operations, a button label such as “Saving…” may be sufficient. Skeleton screens can preserve layout and reduce perceived instability, but they should not imitate content so closely that users mistake placeholders for real data.

Avoid indefinite spinners. If an operation exceeds a reasonable threshold, explain what is happening and offer a recovery path, such as retrying or returning to the previous screen.

Empty states

An empty state should distinguish among:

  • no data has ever been created;
  • a search or filter produced no matches;
  • data is unavailable because of permissions; and
  • data failed to load.

These cases require different messages and actions. “No projects yet—Create your first project” is different from “No projects match these filters—Clear filters.”

Error states

Errors should be specific, actionable, and proportionate. A field error belongs near its field. A failed page request may need a retry action. A system failure may require a support reference while avoiding disclosure of sensitive internal details. Technical logs can contain diagnostic information; user-facing messages should contain only what the user needs.

Success states

Success feedback confirms completion and often explains the next available action. A temporary toast can be appropriate for a minor background save, while a prominent confirmation page may be necessary for a purchase, account deletion, or other consequential action. Do not rely on color or animation alone to communicate success.

6. Worked Application: A Protected Project Workspace

The following is an illustrative case. A university department is building a project-management SPA. Authenticated users can view projects. Members can create tasks, project owners can edit project settings, and visitors must be redirected to login.

Background and problem

The initial prototype has three weaknesses: every screen is rendered through a single component, forms submit without visible validation, and users see a blank area while project data loads. On mobile devices, the sidebar covers the form and the Back button behaves unpredictably.

Analysis

The team identifies four separate concerns:

  1. route structure and deep links;
  2. access control and permission failures;
  3. form state and server validation; and
  4. feedback for loading, empty, and failed states.

They define the following route model:

/projects
/projects/:projectId
/projects/:projectId/tasks/new
/projects/:projectId/settings

The project layout contains the project title, breadcrumbs, navigation, and an outlet for child views. The settings route is protected by an owner permission check. The API independently checks the same permission.

Approach and solution

The project route fetches data using the projectId parameter. While the request is pending, it displays a skeleton with the expected title and navigation structure. If the project does not exist, it displays a not-found state; if the request fails, it displays a retry action.

The new-task form uses controlled state. Its required fields are title and due date. Client validation checks that the title is nonempty and that the date has the expected format. The server additionally checks membership, project status, and permissible date rules. Server field errors are mapped back to the corresponding controls; an unknown failure is presented as a form-level message.

After successful creation, the application navigates to the task details route using a normal history entry when users may reasonably return to the form, or replaces the entry when the form was an intermediate redirect. The decision is based on the intended Back-button experience rather than convenience for the developer.

On narrow screens, the sidebar becomes a collapsible navigation drawer. Opening and closing the drawer is keyboard accessible, focus is managed, and the main content remains readable when the drawer is closed. A responsive table is replaced with task cards that preserve task title, status, owner, and due date.

Outcome

Users can bookmark a project view, refresh it, and return to the same context. Unauthorized users receive a clear forbidden message rather than a broken settings screen. Form errors are understandable and associated with their controls. Loading and empty states explain what users can do next, and mobile users can complete the same task without horizontal scrolling.

Learning points

The case demonstrates that routing, forms, and user experience must be designed together. A route defines context; authorization defines permissible actions; the form represents intent; and feedback determines whether the system’s behavior is intelligible. Treating these as unrelated features would reproduce the original weaknesses.

7. Quality, Security, and Governance Considerations

Accessibility

Use semantic headings, landmarks, labels, keyboard-operable controls, visible focus, sufficient contrast, and meaningful status announcements. Test with keyboard-only navigation and at least one screen reader. Ensure that dialogs trap focus appropriately, return focus when closed, and can be dismissed according to their purpose.

Security and privacy

Never treat hidden routes or disabled buttons as authorization. Enforce permissions on the server, protect session tokens according to the chosen authentication architecture, and avoid placing sensitive information in URLs because URLs may appear in history, logs, browser synchronization, and analytics. Sanitize or safely render user-generated content to reduce cross-site scripting risk. Avoid exposing detailed account-existence messages when that would enable enumeration.

Data integrity and race conditions

Users may submit a form twice, navigate away during a request, or edit stale data. APIs should use idempotency strategies where repeated operations would be harmful, and interfaces should handle cancellation or stale responses. Optimistic updates can make an interface feel fast, but they require rollback or reconciliation when the server rejects the change.

Testing

Useful tests include:

  • route rendering for valid, unknown, and parameterized URLs;
  • redirect behavior for unauthenticated and unauthorized users;
  • preservation of intended return locations;
  • validation for boundary and malformed inputs;
  • mapping of server errors to fields;
  • keyboard and screen-reader interaction;
  • loading, empty, success, and failure states; and
  • responsive behavior at representative viewport sizes.

End-to-end tests are especially valuable for complete flows such as login, protected navigation, form submission, and recovery from a failed request. Unit tests remain useful for pure validation functions and route-specific utilities.

8. Contemporary Practice and Critical Evaluation

Frameworks and routing libraries increasingly support data loaders, deferred content, route-level error boundaries, and server rendering. These features can improve performance and reduce duplicated fetching logic, but they do not remove the need for sound information architecture. A fast route that has ambiguous permissions or poor error recovery remains a poor experience.

React applications may also use form libraries that provide registration, validation, touched state, and submission handling. Such libraries can reduce boilerplate, but developers must understand their state model and accessibility output. Abstraction is beneficial when it expresses a stable pattern; it is harmful when a team cannot explain how values, errors, and submission status are controlled.

Artificial intelligence can assist with generating validation schemas, test cases, or accessibility checklists, but generated code requires review. AI-generated forms may accidentally expose sensitive fields, create weak authorization assumptions, or produce vague error messages. Human developers remain responsible for privacy, security, correctness, and inclusive design.

Best-Practice Checklist

  • Design URLs around user-recognizable resources and tasks.
  • Use nested routes when a persistent parent context is meaningful.
  • Treat client-side route guards as experience features, not security controls.
  • Enforce authentication and authorization on the server.
  • Use labels, semantic input types, and accessible error associations.
  • Validate early enough to help, but not so early that typing becomes stressful.
  • Preserve user-entered values when submission fails.
  • Distinguish loading, empty, unauthorized, not-found, and error states.
  • Make navigation usable with keyboard, touch, small screens, and assistive technology.
  • Test deep links, refreshes, redirects, history behavior, and recovery paths.

Chapter Summary

Routing gives a React SPA a navigable information architecture and enables deep links, nested layouts, parameters, query state, and meaningful browser history. Nested routes should represent genuine parent-child relationships. Protected routes improve the interface for unauthenticated or unauthorized users, but real security requires server-side authentication and authorization.

Forms combine state management, validation, submission, and feedback. Controlled components provide precise coordination between input values and interface behavior, while uncontrolled inputs may be suitable for simpler or specialized cases. Validation should use semantic HTML, client-side feedback, and authoritative server-side rules. Errors must be specific, associated with their fields, and accessible.

Effective user experience depends on appropriate navigation patterns, responsive interaction, focus management, and complete state design. Loading, empty, error, and success states should communicate what happened and what the user can do next. Professional implementation also considers privacy, security, duplicate requests, stale data, testing, and the limitations of automated code generation.

Key Terms

Single-page application: A web application that updates views dynamically without loading a complete document for every internal navigation.

Client-side router: A system that maps browser locations to application views within the client.

Route parameter: A variable segment of a route path, such as :projectId.

Query parameter: Optional key-value data appended to a URL after ?.

Nested route: A child route rendered within a parent route’s layout or outlet.

Index route: The default child route displayed at a parent path.

Deep link: A URL that directly identifies a specific application view.

Authentication: The process of establishing a user’s identity.

Authorization: The process of determining which actions or resources an identity may access.

Protected route: A route restricted by authentication or permission requirements.

Controlled component: A form control whose value is managed by application state.

Uncontrolled component: A form control whose current value is primarily managed by the DOM.

Client-side validation: Validation performed in the browser to provide immediate feedback.

Server-side validation: Authoritative validation performed by a trusted server.

Touched state: Information indicating that a user has interacted with a form field.

Optimistic update: Updating the interface before server confirmation, with a plan for rollback if needed.

Loading state: Interface feedback indicating that data or an operation is in progress.

Empty state: Interface feedback explaining that no relevant content is currently available.

Open redirect: A security flaw in which a user can be redirected to an untrusted external destination.

Review Questions

Short Answer Questions

  1. What is the difference between a route parameter and a query parameter?
  2. Why is a client-side protected route not sufficient for security?
  3. What makes a React form component controlled?
  4. Why should an error message be associated with its input field?
  5. Give two different causes of an empty state.

Long Answer Questions

  1. Explain how nested routing supports application information architecture. Include an example route hierarchy.
  2. Discuss authentication and authorization in a protected React application, including the responsibilities of the client and server.
  3. Compare controlled and uncontrolled form components and identify situations in which each may be appropriate.
  4. Describe a complete form-submission state model, from initial editing through validation, submission, success, and failure.
  5. Explain how responsive design, focus management, and accessible feedback contribute to usable navigation.

Analytical / Application Questions

  1. Design a route structure for an authenticated learning platform containing courses, lessons, assignments, and instructor settings. Identify which routes should be nested and which should be protected by permissions.
  2. A registration form reports all errors only after the user presses Submit, loses entered values after a server failure, and uses red borders without text. Analyze the usability and accessibility problems and propose a revised design.
  3. A mobile project-management application displays a blank screen for four seconds, shows “No results” when the API fails, and creates two records when a user taps Submit twice. Propose a state and interaction design that addresses all three problems.

Multiple Choice Questions with Answers

Question 1

Which statement best describes authorization?

A. Determining whether a user is allowed to perform an action

B. Collecting a user’s email address

C. Rendering a route without a page reload

D. Checking whether a browser supports JavaScript

Correct Answer: A. Determining whether a user is allowed to perform an action

Question 2

What is the primary purpose of an index route?

A. To define a server-only endpoint

B. To display the default child view at a parent path

C. To encrypt route parameters

D. To disable browser history

Correct Answer: B. To display the default child view at a parent path

Question 3

Which practice provides the strongest security for protected data?

A. Hiding the navigation link in the client

B. Disabling the edit button in React

C. Checking permissions on the server for each protected operation

D. Using a visually complex URL

Correct Answer: C. Checking permissions on the server for each protected operation

Question 4

In a controlled React input, where is the authoritative current value normally stored?

A. The browser history only

B. React application state

C. The CSS stylesheet

D. The route definition

Correct Answer: B. React application state

Question 5

Which attribute helps associate an input with its validation message?

A. aria-describedby

B. aria-hidden

C. tabindex="-1" on the message only

D. role="navigation"

Correct Answer: A. aria-describedby

Question 6

Which empty-state message is most appropriate when filters produce no matches?

A. The server is unavailable.

B. You are not authorized to view this data.

C. No projects match these filters. Clear filters to see all projects.

D. The project was deleted permanently.

Correct Answer: C. No projects match these filters. Clear filters to see all projects.

Question 7

When is replacing the current history entry commonly appropriate?

A. For every ordinary internal link

B. After a login redirect when returning to the login page is undesirable

C. When opening a new browser tab

D. When displaying a field error

Correct Answer: B. After a login redirect when returning to the login page is undesirable

Question 8

Why should server-side validation still occur when client-side validation is implemented?

A. Client code can be modified or bypassed by an untrusted user

B. Browsers cannot display text messages

C. React cannot store strings

D. Server validation prevents all loading states

Correct Answer: A. Client code can be modified or bypassed by an untrusted user

Question 9

Which response best represents a usable loading state for a long operation?

A. A permanently blank content area

B. A progress indicator with an understandable status and recovery option when appropriate

C. A success message displayed before the request finishes

D. A disabled page with no explanation

Correct Answer: B. A progress indicator with an understandable status and recovery option when appropriate

Question 10

Which navigation pattern is generally most suitable for switching among closely related views at the same level, such as Profile, Security, and Billing?

A. Tabs

B. A destructive confirmation dialog

C. A file-upload control

D. A validation summary

Correct Answer: A. Tabs

Web Development using JavaScript/React
Chapter 8 of 14