Skip to content

Modern React 38: Auth, roles, and protected UI

This is part 38 of the Modern React development series.

Auth-aware UI helps users understand what they can do, but it is not the same as authorization. React can hide, disable, or label controls based on a session snapshot. The server still decides whether the action is allowed.

Concept

Authentication identifies the user. Authorization decides what that user can do. Roles and permissions are inputs to UI rendering, but trusted checks belong at server, route, or API boundaries.

Terms

  • Auth: Authentication, the process of identifying who the user is.
  • Authorization: The decision about whether an identified user can perform an action.
  • Role: A named group of permissions such as admin, editor, or viewer.
  • Protected UI: Interface that changes based on the user’s auth or permission state.
  • API: Application programming interface, the boundary where code sends or receives structured data or actions.

Mental model

Think of protected UI as signage, not the lock. Good signage prevents confusion. The lock still lives on the server-side door.

How it is used

Use auth-aware React UI for navigation, disabled controls, empty states, admin sections, account menus, upgrade prompts, and warnings before privileged actions. Check permissions again in server functions, route actions, API handlers, and data loaders.

How to use it

  1. Load a minimal session or permission snapshot for rendering.
  2. Pass permissions to components through props, context, or route data.
  3. Render unavailable actions as hidden, disabled, or explanatory based on user experience needs.
  4. Check authorization at every trusted mutation and data read boundary.
  5. Keep permission names domain-specific and test important combinations.

Example: Permission-aware button

import type { MouseEventHandler } from "react";
type DeleteProjectButtonProps = {
canDelete: boolean;
onDelete: MouseEventHandler<HTMLButtonElement>;
};
export function DeleteProjectButton({
canDelete,
onDelete,
}: DeleteProjectButtonProps) {
if (!canDelete) {
return <p>You need project admin access to delete this project.</p>;
}
return (
<button type="button" onClick={onDelete}>
Delete project
</button>
);
}
React output

The UI explains the missing permission. The server still needs to enforce the same rule.

Example: Server check at mutation boundary

import { canDeleteProject, requireCurrentUser } from "./auth";
import { db } from "./db";
async function deleteProject(projectId: string) {
const user = await requireCurrentUser();
const allowed = await canDeleteProject(user.id, projectId);
if (!allowed) {
throw new Error("Not allowed");
}
await db.project.delete({ id: projectId });
}
Runtime result
async function deleteProject(projectId: string) {
  const user = await requireCurrentUser();
  const allowed = await canDeleteProject(user.id, projectId);
  if (!allowed) {
    throw new Error("Not allowed");
  }
  await db.project.delete({ id: projectId });
}

The trusted write checks authorization where the data changes.

Details to watch

  • UI feedback: Hide actions when they are irrelevant. Disable or explain actions when the user needs to understand why they cannot proceed.
  • Server authority: Never treat hidden UI as authorization.
  • Session freshness: Client session snapshots can be stale. Server checks use the current source of truth.
  • Roles vs permissions: Roles are convenient labels. Permissions describe exact capabilities.

Series navigation

References