Frontend Development

Chapter 9
State, Forms, Routing and Data Flow

Certificate in Full stack programming
Frontend Development   •   Certificate

State, Forms, Routing and Data Flow

Introduction

Modern frontend applications are interactive systems rather than collections of independent pages. A user selects products, edits profile information, submits a payment form, moves between routes, and expects the interface to remain consistent. These behaviours depend on how an application stores information, changes it, validates it, moves it between components, and associates it with URLs.

This chapter examines four closely related frontend concerns: state management, forms, routing, and data flow. The discussion uses React terminology because React is widely used in full-stack development, but the underlying principles also apply to Angular, Vue, Svelte, and other component-based frameworks.

A useful design principle is:

Store each piece of information in the smallest appropriate scope, update it through clear operations, and derive rather than duplicate values whenever possible.

Poor state design creates contradictory data, difficult debugging, fragile forms, and navigation errors. Good state design makes an application predictable, testable, accessible, and easier to extend.

Learning Objectives

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

  • distinguish local, shared, server, URL, and derived state;
  • explain one-way data flow and unidirectional state updates;
  • choose between component state, lifted state, Context, reducers, and external stores;
  • build controlled forms with validation and accessible error messages;
  • explain client-side routing, route parameters, nested routes, and protected routes;
  • manage asynchronous loading, success, and error states;
  • identify common data-flow problems and select appropriate solutions;
  • apply these concepts to a small realistic application.

1. Understanding State

State is information that can change during the lifetime of an application and whose change may affect what the user sees or what the system does. Examples include a counter value, the selected tab, a shopping cart, a login session, a form field, and the result of an API request.

A component’s rendered output can be viewed conceptually as a function of its inputs and state:

UI = f(props, state, environment)

Here, UI is the rendered interface, props are values supplied by a parent, state is data managed by the component or application, and environment includes relevant external conditions such as route information or browser capabilities. This is a conceptual model rather than a programming equation. It assumes rendering is deterministic for the same inputs.

1.1 Categories of state

State categoryMeaningExamplesTypical location
Local UI stateUsed by one component or a small isolated areaModal visibility, input focus, selected tabComponent state
Shared client stateUsed by several componentsShopping cart, theme, current userLifted state, Context, or store
Server stateRetrieved from or synchronized with a backendProduct list, orders, account dataData-fetching layer or cache
URL stateEncoded in the address and navigation historySearch terms, page number, route IDRouter and query parameters
Derived stateCalculated from other valuesCart total, filtered results, validation statusComputation, not separate storage
Persistent stateSurvives refreshes or sessionsPreferences, saved draftBrowser storage or backend

The distinction is important because different states have different ownership and consistency requirements. A modal’s open status normally does not belong in a global store. Conversely, if the header, checkout page, and cart drawer all need the same cart, keeping separate copies is risky.

1.2 Single source of truth

A single source of truth means that a fact is stored in one authoritative location. Other components receive or derive it from that location. For example, if a cart contains line items, the cart total should normally be calculated from those line items rather than stored independently.

Duplicated state can become inconsistent. If quantity is stored both in a product row and in a cart store, an update may change one copy but not the other. Prefer a normalized or clearly structured representation, such as an array of line items identified by product ID.

1.3 State transitions

State should change through explicit events or actions. An action describes what happened, such as ADD_ITEM, REMOVE_ITEM, or SUBMIT_FORM. A reducer or event handler then determines the next state.

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return addItem(state, action.product);
    case 'REMOVE_ITEM':
      return state.filter(item => item.id !== action.id);
    default:
      return state;
  }
}

The example follows two important rules: it does not mutate the existing state, and each action has a specific meaning. Immutable updates make changes easier to reason about and support reliable rendering and testing.

2. Selecting a State-Management Approach

State management should be proportional to application complexity. Adding a global library to solve a local problem introduces unnecessary structure; placing genuinely shared state in many unrelated components produces duplication and prop drilling.

2.1 Local component state

Local state is appropriate when only one component, or one closely related component group, needs the value. Typical examples are whether a dropdown is open, the current value of a temporary input, or the active accordion panel.

In React, useState is commonly used:

const [isOpen, setIsOpen] = useState(false);

function toggleMenu() {
  setIsOpen(previous => !previous);
}

The functional update is valuable when the new value depends on the previous value. It avoids relying on a possibly outdated closure when several updates are scheduled.

2.2 Lifting state up

If two sibling components must coordinate, move the shared state to their nearest common parent. The parent owns the value and passes it down as props, together with callbacks for requested changes.

For example, a search page may own query and pass it to a search box and a results list. The search box requests changes; the results list receives the current query. This creates one direction of authority and prevents the siblings from maintaining conflicting values.

2.3 Context

Context allows a value to be made available to descendants without passing it through every intermediate component. It is suitable for relatively stable cross-cutting concerns such as theme, locale, or the authenticated user.

Context is not automatically a complete state-management solution. If a frequently changing large object is placed in one context, many consumers may re-render. Splitting contexts, memoizing values, or using a more specialized store may improve performance and clarity.

2.4 Reducers

A reducer is useful when a state object has several related transitions. It centralizes update logic and gives actions meaningful names. A reducer is especially appropriate for multi-step forms, shopping carts, and workflow states.

A reducer should be predictable: given the same previous state and action, it should return the same next state. Network requests, timers, and direct browser side effects generally belong outside the reducer.

2.5 External stores and server-state tools

A dedicated store can be useful when unrelated parts of a large application need frequent access to shared client state. Examples include Redux Toolkit, Zustand, and similar tools. The choice should consider team familiarity, debugging tools, update patterns, bundle size, and long-term maintenance.

Server state deserves separate treatment. It has loading, caching, refetching, invalidation, and synchronization concerns that ordinary client state does not. Tools such as TanStack Query can manage these concerns. A successful response should not be confused with permanent truth: another user or process may change the backend after data is fetched.

3. Predictable Data Flow

Most component frameworks encourage one-way data flow:

Parent state → props → child rendering
Child event → callback/action → state owner → new rendering

This direction makes ownership visible. A child should generally not modify a parent’s object directly. Instead, it invokes a callback or dispatches an action. The owner decides how the state changes.

3.1 Props and callbacks

Props are inputs. They should be treated as read-only by the receiving component. A callback prop creates an explicit communication path back to the owner.

function QuantityControl({ quantity, onChange }) {
  return (
    <button onClick={() => onChange(quantity + 1)}>
      Add one
    </button>
  );
}

The control does not decide where the quantity is stored. This makes it reusable and easy to test.

3.2 Derived values

A derived value is calculated from authoritative data. For a cart, the total can be calculated as:

T = ∑(qᵢ × pᵢ), for i = 1 to n

T is the cart total, qᵢ is the quantity of item i, pᵢ is its unit price, and n is the number of line items. The formula assumes prices and quantities use compatible units and that tax, discount, and shipping are excluded. If currency rounding is required, the application must define its rounding policy.

Storing T separately is unnecessary unless there is a deliberate performance or persistence reason. In most interfaces, deriving it avoids synchronization bugs.

3.3 Asynchronous data flow

A request should normally represent at least four states: idle, loading, success, and error. A component should not display stale results as though they were current. It should also handle cancellation or request ordering when a user changes a search term quickly.

A conceptual state model is:

idle → loading → success
             ↘ error

For a search interface, each request should be associated with the query that produced it. AbortController or request identifiers can prevent an older response from overwriting a newer one. Loading indicators, retry controls, and useful error messages are part of the user experience, not optional decorations.

4. Controlled Forms

A controlled form stores each field’s current value in application state. The input receives its value from state and reports changes through an event handler.

const [email, setEmail] = useState('');

<input
  type="email"
  value={email}
  onChange={event => setEmail(event.target.value)}
/>

This approach makes the current value available for validation, conditional fields, formatting, and submission. An uncontrolled form instead lets the browser maintain the value and reads it through a reference or FormData at submission time. Uncontrolled forms can be simpler and may reduce re-rendering, but controlled forms are often clearer for dynamic interfaces.

4.1 Form state model

A practical form model commonly includes:

  • values: current field values;
  • touched: whether a field has been visited;
  • errors: validation messages;
  • status: idle, submitting, success, or failure;
  • serverError: an error returned by the backend.

Validation should occur both on the client and on the server. Client validation improves immediate feedback; server validation remains authoritative because a client can be modified or bypassed.

4.2 Validation principles

Validation rules should be explicit and consistent. Common rules include required fields, length limits, allowed formats, numeric ranges, and cross-field relationships. An error should explain how to correct the input, for example, “Enter a valid email address,” rather than merely “Invalid.”

Client-side validation must not be treated as a security boundary. Authorization, ownership checks, price calculations, and business rules must be enforced on the server.

4.3 Accessibility

Every form control should have a visible or programmatically associated label. Error text should be associated with its input through aria-describedby, and invalid controls can use aria-invalid="true". Focus should move to the first invalid field after a failed submission when appropriate.

Avoid disabling the submit button solely because validation is incomplete; users may need to submit to discover what remains missing. However, prevent duplicate submissions while a request is in progress and provide a clear status message.

4.4 Submission and security

A form submission should normalize input, validate it, display progress, handle success and failure, and prevent accidental duplicate requests. Sensitive data should not be placed in URLs, logs, or local storage without a clear security justification. Passwords should never be stored in browser storage by frontend code.

5. Routing and Navigation

Routing maps a URL to an interface state. Client-side routers can change the displayed view without requesting a complete document for every navigation, while still supporting browser history and shareable URLs.

Important routing concepts include:

  • Route path: a pattern such as /products;
  • Route parameter: a variable segment such as /products/:id;
  • Query parameter: optional URL data such as ?page=2&sort=price;
  • Nested route: a route rendered within a parent layout;
  • Link navigation: user-driven navigation that preserves browser semantics;
  • Programmatic navigation: navigation triggered after an action, such as successful login;
  • Not-found route: a fallback for an unknown path.

Use links for ordinary navigation so that users can open them in a new tab, copy their address, and use keyboard controls. Programmatic navigation is appropriate after events such as form submission, but it should not replace meaningful links.

5.1 Route parameters and query parameters

A product identifier belongs naturally in a route parameter because it identifies the resource: /products/42. A search term, filter, or page number commonly belongs in query parameters because it describes a view of a collection: /products?category=books&page=2.

URL state should be parsed and validated. Do not assume that an ID is numeric or that a page parameter is positive. Invalid parameters should produce a useful error, a not-found response, or a safe default.

5.2 Protected routes

A protected route checks authentication and, where necessary, authorization. Authentication asks who the user is; authorization asks what the user is allowed to do. Hiding a link is not sufficient protection. The backend must enforce access rules for every protected operation.

A frontend guard improves navigation experience by redirecting unauthenticated users to a sign-in page, often preserving the intended destination. It does not replace server-side enforcement.

5.3 Routing and data loading

A route may load data when it becomes active. The interface should handle missing records, loading states, network failures, and stale parameters. Route changes should not leave old data visible without indicating that a new request is in progress.

6. Illustrative Case: Course Registration Portal

Background

This illustrative case concerns a small course registration portal. Students browse courses, filter by department, view a course page, add courses to a selection list, and submit registration.

Problem

An initial implementation stores the selected courses separately in the course list, course details page, and registration summary. The search form resets when the user navigates back, and invalid registration data produces no useful feedback.

Analysis

The selected courses are shared client state and require one authoritative owner. The search term and page number are URL state because they should be bookmarkable. Course records are server state. The registration form is local state until submission. The total credit count is derived from selected courses.

Approach and solution

A suitable design is:

  1. A router maps /courses, /courses/:courseId, and /register to views.
  2. Query parameters store search and filter values.
  3. A selection context or reducer owns selected course IDs.
  4. Course details are fetched from the backend and cached by course ID.
  5. The registration form owns its field values and validation state.
  6. The summary derives total credits from selected course records.
  7. Submission sends selected IDs and form data to the server.
  8. The server checks prerequisites, capacity, authentication, and duplicate enrollment.

A reducer might receive ADD_COURSE, REMOVE_COURSE, and CLEAR_SELECTION actions. The registration page does not modify the course list directly; it reads the shared selection and dispatches actions. If the backend rejects a full course, the interface displays the server’s result and updates the relevant data rather than assuming success.

Outcome

The design removes duplicated selection state, preserves search URLs, provides meaningful validation, and makes the backend authoritative for enrollment rules. Components can be tested independently because ownership and communication paths are explicit.

Learning points

The case illustrates that state location should follow responsibility. URL state belongs in the URL, server state belongs in a data layer, temporary form state belongs near the form, and shared selection belongs in a common owner. Separating these categories reduces accidental complexity.

7. Common Problems and Best Practices

Prop drilling

Passing props through many components is not always wrong; it can make dependencies visible. It becomes problematic when intermediate components pass data they neither use nor understand. Consider composition, Context, or a store when the dependency is genuinely cross-cutting.

Excessive global state

Global state can become an unstructured substitute for design. Keep state local unless several independent parts need it. A global store should have clear ownership, actions, and lifecycle rules.

Mutating state

Direct mutation can prevent reliable change detection and make historical state difficult to inspect. Create new arrays and objects for changed structures, while avoiding unnecessary deep copying.

Race conditions

When requests overlap, responses may arrive out of order. Cancel obsolete requests or ignore responses that no longer match the current request. This is especially important for autocomplete and search.

Persistence mistakes

Browser storage is convenient but exposed to scripts running in the origin. Do not store sensitive credentials casually. Consider expiration, logout behaviour, versioning, and what happens when stored data becomes incompatible with a new application release.

Testing

Test reducers as pure functions, validation rules with boundary cases, forms through user-visible behaviour, and routes with navigation and permission scenarios. Tests should verify outcomes rather than implementation details whenever possible.

8. Contemporary Practice and Professional Implications

Modern frontend development increasingly treats server state, URL state, and client UI state as distinct concerns. Data-fetching libraries provide caching and invalidation; framework routers may support route-level data loading; form libraries reduce repetitive registration and validation code; and TypeScript can make state shapes and action payloads explicit.

These tools improve productivity but do not remove the need for design. A library cannot decide which component owns a fact, whether a value should be persisted, or whether a server response is authoritative. Developers must still consider accessibility, privacy, security, performance, and maintainability.

AI-assisted development can generate form handlers, routes, or reducers, but generated code must be reviewed. Particular risks include insecure storage of tokens, missing authorization checks, incorrect validation, leaked personal data, and assumptions that a client-side guard provides security. Professional responsibility remains with the developer and organization deploying the system.

Chapter Summary

State is changing information that influences an interface or its behaviour. It may be local, shared, server-derived, URL-based, persistent, or calculated. Applications become more predictable when each fact has one source of truth, updates occur through explicit events, and derived values are calculated rather than duplicated.

Local state is suitable for isolated interface behaviour; lifted state coordinates siblings; Context supports cross-cutting values; reducers organize complex transitions; and external tools can manage large client stores or server-state concerns. One-way data flow uses props downward and callbacks or actions upward.

Controlled forms keep field values in application state and support immediate validation, conditional behaviour, and accessible feedback. Client validation improves usability but cannot replace server validation or authorization. Routing connects URLs with views and should distinguish route parameters, query parameters, nested routes, and protected navigation. Authentication and authorization must ultimately be enforced by the backend.

The most reliable applications separate UI state, URL state, server state, and persistent state; handle loading and error conditions; prevent race conditions; protect sensitive data; and test behaviour from the user’s perspective.

Key Terms

State: Changeable information that influences interface behaviour or output.

Local state: State used by one component or a small isolated component group.

Shared state: State required by multiple independent components.

Server state: Data retrieved from or synchronized with a backend system.

Derived state: A value calculated from authoritative data.

Single source of truth: One authoritative location for a particular fact.

Props: Read-only inputs passed from a parent component to a child.

Callback: A function supplied to a component so it can request an action from its owner.

Reducer: A predictable function that calculates next state from current state and an action.

Action: A description of an event or requested state transition.

Context: A mechanism for supplying values to descendants without passing them through every intermediate component.

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

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

Validation: Checking whether input satisfies defined rules.

Routing: Mapping URLs and navigation events to application views.

Route parameter: A variable segment of a route path identifying a resource.

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

Authentication: Establishing the identity of a user or system.

Authorization: Determining what an authenticated user is permitted to do.

Race condition: Incorrect behaviour caused by the timing or ordering of concurrent operations.

Review Questions

Short Answer Questions

  1. What is the difference between local state and shared state?
  2. Why is a single source of truth valuable?
  3. What is a controlled form input?
  4. Distinguish a route parameter from a query parameter.
  5. Why can client-side validation not replace server-side validation?

Long Answer Questions

  1. Explain one-way data flow and describe how props and callbacks implement it.
  2. Compare component state, lifted state, Context, reducers, and external stores.
  3. Describe the principal states and accessibility requirements of a robust form.
  4. Explain how protected routes should work and why frontend protection alone is insufficient.
  5. Discuss how separating client state, server state, URL state, and derived state improves maintainability.

Analytical / Application Questions

  1. A shopping cart stores line items in three different components. Identify the design problems and propose a state architecture.
  2. A search request for “react” returns after a newer request for “react router.” Explain the possible bug and recommend a solution.
  3. Design the state, routes, validation, and data flow for a small appointment-booking frontend. Justify where each important value should be stored.

Multiple Choice Questions with Answers

Question 1

Which state is most appropriate for a modal’s open or closed status when only one component uses it?

A. Local component state

B. A server database

C. A URL route parameter

D. A global authentication store

Correct Answer: A. Local component state

Question 2

What is the main purpose of a single source of truth?

A. To make every component global

B. To prevent contradictory copies of the same fact

C. To eliminate all server requests

D. To ensure every value is stored in the URL

Correct Answer: B. To prevent contradictory copies of the same fact

Question 3

In one-way data flow, how does a child normally request a change to parent-owned state?

A. By directly mutating the parent object

B. By changing the browser URL only

C. By calling a callback or dispatching an action

D. By writing to a separate duplicate state value

Correct Answer: C. By calling a callback or dispatching an action

Question 4

Which value is usually derived rather than stored separately in a shopping cart?

A. The product identifier

B. The unit price received from the backend

C. The quantity of a line item

D. The cart total calculated from line items

Correct Answer: D. The cart total calculated from line items

Question 5

What characterizes a controlled form input?

A. Its value is managed by application state

B. It cannot be validated

C. It stores its value only in the server database

D. It must use a route parameter

Correct Answer: A. Its value is managed by application state

Question 6

Which statement about client-side validation is correct?

A. It removes the need for server validation

B. It is mainly useful for immediate user feedback

C. It guarantees authorization

D. It prevents a modified client from sending invalid data

Correct Answer: B. It is mainly useful for immediate user feedback

Question 7

Which URL best represents a product identified by ID 42?

A. /products?product=42

B. /products/42

C. /products#42

D. /products/42?route=product

Correct Answer: B. /products/42

Question 8

What is authorization?

A. Determining what an authenticated user may do

B. Determining the network speed of a user

C. Converting a route into a component

D. Validating an email format

Correct Answer: A. Determining what an authenticated user may do

Question 9

What problem can occur when an older asynchronous response arrives after a newer response?

A. Prop drilling

B. A race condition causing stale data to overwrite current data

C. Automatic authorization

D. CSS inheritance

Correct Answer: B. A race condition causing stale data to overwrite current data

Question 10

Which approach is most suitable for a complex set of related state transitions such as a shopping cart?

A. A reducer with explicit actions

B. Independent duplicated state in every item row

C. Only a browser bookmark

D. A disabled submit button

Correct Answer: A. A reducer with explicit actions

Frontend Development
Chapter 9 of 15