Unit 5: Client-Server Communication and Routing

INT252 — Web App Development With Reactjs 11 min read

I. Orientation

A React application commonly acts as a client: it requests resources from a server, presents asynchronous results, and maps browser URLs to views. Reliable applications therefore separate remote data synchronization from local interface state and treat routing as part of application architecture.

  • Request–response model: A client sends an HTTP request; a server returns a response containing a status code, headers, and usually a representation such as JSON.
  • Asynchronous operation: Network requests do not finish immediately, so interfaces must represent loading, success, empty, and error outcomes.
  • State separation: Client state is locally controlled, while server state is remotely owned and may become stale.
  • Declarative routing: A route configuration maps URL patterns such as /products/:id to React components.
  • Synchronization principle: Data-fetching tools coordinate caches with remote sources; routing tools coordinate rendered views with browser location.

II. HTTP and Request Clients — Communicating with Web Services

HTTP defines the communication contract, while Fetch and Axios provide JavaScript interfaces for executing that contract.

A. HTTP fundamentals and REST principles

HTTP is a stateless request–response protocol in which methods identify intended operations and status codes describe outcomes.

  • Request structure: A request includes a method, URL, headers, and an optional body; POST /api/users may carry JSON in its body.
  • Common methods:
    • GET: Retrieve a resource without changing it.
    • POST: Create a resource or initiate processing.
    • PUT: Replace a complete resource.
    • PATCH: Partially update a resource.
    • DELETE: Remove a resource.
  • Status codes: 200 OK indicates success, 201 Created creation, 204 No Content success without a body, 400 a bad request, 401 missing authentication, 403 forbidden access, 404 absence, and 500 a server failure.
  • REST resources: Resource-oriented endpoints use nouns, such as /api/books/7, rather than operation names such as /getBook.
  • REST constraints: Requests are stateless, resources have representations, and standard HTTP semantics support a uniform interface.
  • Idempotency: Repeating GET, PUT, or DELETE should produce the same intended server state; repeating POST may create duplicates.
  • Headers: Content-Type: application/json describes the request body, while Accept: application/json requests a JSON response.

B. Fetch API and Axios usage patterns

Fetch is browser-native, whereas Axios is a library offering convenience features and consistent request configuration.

  1. Fetch API:
    • Response handling: Fetch rejects primarily on network failure; status codes such as 404 require an explicit response.ok check.
    • JSON conversion: response.json() asynchronously parses the response body.
JSX
async function getUser(id, signal) {
  const response = await fetch(`/api/users/${id}`, { signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
  1. Axios:
    • Automatic behavior: Axios parses JSON and rejects promises for non-2xx responses by default.
    • Instances and interceptors: axios.create() can define a base URL, while interceptors can attach authorization tokens or process errors.
JSX
import axios from "axios";

const api = axios.create({ baseURL: "/api" });
const { data } = await api.get("/users/7");
  • Cancellation: Both modern Fetch and Axios can use AbortController; cancellation prevents obsolete requests from unnecessarily completing.
  • Mutation payloads: Fetch requires JSON.stringify(data) and a content-type header, while axios.post("/users", data) serializes ordinary objects automatically.

III. Asynchronous Rendering and Effects — Managing Request Lifecycles

Network-driven components must model every visible outcome and prevent obsolete side effects from changing current UI.

A. Handling loading, error, and empty states

Asynchronous interfaces should distinguish an unfinished request, a failed request, and a successful response containing no records.

  • Loading state: Display a spinner, skeleton, or progress message while data is unavailable; preserve existing data during background refresh when possible.
  • Error state: Show an actionable message and retry control rather than exposing only a console error.
  • Empty state: A successful response such as [] is not an error; display “No products found” and, where appropriate, a creation or filter-reset action.
  • Conditional order: Check loading and error before rendering data, then test collection length.
JSX
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
if (products.length === 0) return <p>No products found.</p>;
return <ProductList products={products} />;
  • Accessibility: Use aria-live="polite" for status changes and avoid loading indicators that provide no textual meaning.

B. Side effects for data fetching

Data fetching is a side effect because it communicates with a system outside React’s pure rendering process.

  • Effect timing: useEffect runs after a committed render; its dependency array determines when synchronization is repeated.
  • Dependency rule: If a request uses userId, include userId so changing /users/4 to /users/5 triggers another request.
  • Cleanup: Abort requests when the component unmounts or the dependency changes, preventing obsolete results from winning a race.
JSX
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then(r => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      return r.json();
    })
    .then(setUser)
    .catch(e => {
      if (e.name !== "AbortError") setError(e);
    });

  return () => controller.abort();
}, [userId]);
  • Event-driven writes: Submit POST or DELETE requests in event handlers such as handleSubmit, not in an effect that watches button-related state.

C. Limitations of useEffect for data synchronization

Manual useEffect fetching handles simple cases but does not itself provide a complete server-data synchronization system.

  • Boilerplate: Each component must separately manage data, loading, errors, cancellation, and retry behavior.
  • Race conditions: Rapid dependency changes can allow an older request to overwrite newer data unless cancellation or request identity is implemented.
  • No shared cache: Two components requesting /api/users/7 may issue duplicate requests and maintain inconsistent copies.
  • Missing policies: Refetch-on-focus, stale-data rules, request deduplication, pagination, and mutation invalidation require custom logic.
  • Development behavior: React Strict Mode may run an extra development-only effect setup and cleanup cycle, exposing unsafe effects.
  • Waterfalls: Parent fetching followed by child fetching delays rendering because requests begin sequentially rather than in parallel.

IV. State Ownership and React Query — Synchronizing Remote Data

Correct state ownership determines whether ordinary React state or a dedicated server-state cache should manage a value.

A. Distinction between client state and server state

Client state belongs to the interface, while server state is a cached snapshot of information owned by a remote service.

  1. Client state:

    • Examples: Modal visibility, selected tab, unsaved form input, and theme preference.
    • Control: Updated synchronously through useState, useReducer, context, or a client-state store.
  2. Server state:

    • Examples: User records, product inventories, account balances, and paginated comments.
    • Characteristics: Asynchronous, shared across clients, potentially stale, and subject to external changes.
  • Derived values: A filtered product list should usually be computed from products and the current filter, not stored as another independent copy.
  • Boundary: Copying fetched data into local state can disconnect it from cache updates; local state is justified when creating an editable draft.

B. Introduction to server state management using React Query

TanStack Query, commonly called React Query, manages fetching, caching, retries, invalidation, and synchronization through declarative queries.

  • Provider setup: A QueryClient stores cache entries and is supplied through QueryClientProvider.
  • Query identity: queryKey: ["user", userId] uniquely identifies data and includes every parameter used by the request.
  • Query function: queryFn returns a promise that resolves with data or rejects with an error.
JSX
const query = useQuery({
  queryKey: ["user", userId],
  queryFn: () => getUser(userId),
  enabled: Boolean(userId)
});

const { data, isPending, isError, error } = query;
  • Mutations: useMutation handles create, update, or delete operations; successful mutations commonly invalidate related queries.
  • Invalidation: queryClient.invalidateQueries({ queryKey: ["users"] }) marks matching cached data stale and may trigger a refetch.

C. Caching strategies and background synchronization concepts

A query cache balances immediate reuse against the need to refresh data that may have changed remotely.

  • Freshness: staleTime specifies how long data remains fresh; a value of 60_000 treats it as fresh for 60 seconds.
  • Garbage collection: gcTime controls how long unused cached data remains before removal.
  • Cache-first rendering: Previously fetched data can render immediately when revisiting a view, avoiding a blank loading screen.
  • Background refetching: Stale data may remain visible while a new request updates it; isFetching can indicate this quieter synchronization.
  • Automatic triggers: Policies can refetch when a component mounts, the window regains focus, or network connectivity returns.
  • Deduplication: Concurrent components using the same query key can share one in-flight request.
  • Invalidation strategy: Invalidate affected keys after mutations; avoid clearing the entire cache when only ["todos"] changed.

V. Client-Side Routing — Mapping URLs to React Views

Client-side routing changes rendered views without requesting a complete new HTML document for every navigation.

A. Client-side routing fundamentals

A client-side router observes browser location and selects the matching React element.

  • URL as state: /products/12?tab=reviews records a view that can be bookmarked, refreshed, or shared.
  • History API: Routers use browser history to push or replace entries while avoiding full-page reloads.
  • Server requirement: Production hosting must usually return the application entry document for unknown non-file paths such as /products/12.
  • Route matching: Static, dynamic, and nested path segments determine which component tree renders.

B. Routing configuration setup

React Router configures route matching through a router provider or declarative route components.

  • Installation: The web package is commonly imported from react-router-dom.
  • Configuration: createBrowserRouter defines route objects, while RouterProvider connects them to the application.
JSX
const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/products", element: <Products /> },
  { path: "*", element: <NotFound /> }
]);

root.render(<RouterProvider router={router} />);
  • Fallback route: * catches unmatched locations and renders a not-found view.

C. Navigation between application views

Router navigation preserves single-page application behavior and browser history.

  • Declarative navigation: <Link to="/products">Products</Link> creates accessible navigation without reloading the document.
  • Active links: NavLink can style a link according to whether its destination matches the current location.
  • Programmatic navigation: useNavigate() is appropriate after actions such as successful login or form submission.
  • History replacement: navigate("/login", { replace: true }) replaces the current entry, preventing an invalid page from remaining in Back history.
  • External destinations: Use a normal <a href> for another website or downloadable resource.

D. Dynamic routes and route parameters

Dynamic segments allow one route pattern to represent many resource-specific pages.

  • Pattern: /products/:productId matches /products/42; productId receives the string "42".
  • Reading parameters: useParams() returns matched values.
JSX
const { productId } = useParams();
const id = Number(productId);
  • Validation: Because parameters are strings and users can edit URLs, validate id; Number.isInteger(id) can reject malformed identifiers.
  • Data identity: Include the parameter in a query key, such as ["product", productId], to separate cache entries.

E. Query parameter handling

Query parameters represent optional URL state such as filters, search terms, sorting, and pagination.

  • Syntax: In /products?category=books&page=2, the path is /products, while category and page are query parameters.
  • React Router API: useSearchParams() reads and updates values through a URLSearchParams-like interface.
  • Type conversion: searchParams.get("page") returns a string or null; convert and validate before numerical use.
  • Updates: Setting { category: "books", page: "2" } creates a shareable and history-aware filtered view.
  • Appropriate use: Query parameters suit optional presentation state; resource identity generally belongs in a path parameter.

F. Nested routes and layout routes

Nested routing mirrors hierarchical interfaces by rendering child routes inside a parent layout.

  • Outlet: A parent component renders <Outlet /> where the selected child element should appear.
  • Shared layout: Navigation, sidebars, and footers can remain stable while child content changes.
  • Index route: An index child renders at the parent’s exact path.
JSX
{
  path: "/dashboard",
  element: <DashboardLayout />,
  children: [
    { index: true, element: <Overview /> },
    { path: "settings", element: <Settings /> }
  ]
}
  • Relative paths: The child "settings" resolves to /dashboard/settings, avoiding duplicated parent segments.

G. Protected routes

Protected routes restrict rendering based on authentication or authorization state, while the server remains responsible for actual data security.

  • Authentication check: A guard waits for session loading, renders permitted content, or redirects unauthenticated users.
  • Redirect pattern: <Navigate to="/login" replace state={{ from: location }} /> can preserve the intended destination.
  • Authorization: Role checks distinguish authenticated users from users permitted to access an administrative view.
  • Security boundary: Hiding a route is not sufficient protection; APIs must independently verify tokens, sessions, and permissions.
  • Loading state: Do not redirect until authentication initialization finishes, or valid users may briefly be sent to login.

H. Route-based code splitting

Route-based code splitting loads JavaScript for a view only when that route is needed.

  • Dynamic import: React.lazy converts an import() promise into a lazily loaded component.
  • Fallback UI: Suspense displays temporary content while the route bundle downloads.
JSX
const Reports = lazy(() => import("./Reports"));

<Suspense fallback={<Spinner />}>
  <Reports />
</Suspense>
  • Benefit: Large administration or reporting pages are removed from the initial bundle, improving initial download and parse time.
  • Trade-off: The first visit to a lazy route may pause; route prefetching can reduce that delay.
  • Chunk failure: An error boundary should handle failed dynamic imports caused by network errors or outdated deployments.