Claude Code Plugins

Community-maintained marketplace

Feedback

Core React client library for authentication (SecurityService), navigation with URL query syncing, API clients, form validation, toast notifications, and breadcrumbs. Foundation for all @astroapps client packages.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name astroapps-client
description Core React client library for authentication (SecurityService), navigation with URL query syncing, API clients, form validation, toast notifications, and breadcrumbs. Foundation for all @astroapps client packages.

@astroapps/client - Core Client Library

Overview

@astroapps/client is the foundation library for building React applications with the Astrolabe framework. It provides navigation, security, UI services, form validation, and utilities for managing application state with @react-typed-forms/core integration.

When to use: Use this library as the foundation for any React application that needs authentication, URL routing with query parameters, API client management, form validation, and UI services like toasts and breadcrumbs.

Package: @astroapps/client Dependencies: @react-typed-forms/core, React 18+ Extensions: @astroapps/client-nextjs (Next.js), @astroapps/client-msal (Azure AD), @astroapps/client-localusers (Local auth)

Key Concepts

1. AppContext - Dependency Injection

Central context for application-wide services (security, navigation, toast, breadcrumbs). Accessed via hooks throughout the application.

2. SecurityService

Handles authentication state, token management, and authorized HTTP requests. Abstract interface allows different implementations (MSAL, local users, custom).

3. NavigationService

Manages URL routing, query parameters, and provides reactive state synchronized with the browser URL.

4. Query Parameter Synchronization

Two-way binding between form controls and URL query parameters with automatic debouncing and batching.

5. Form Validation & Error Handling

Utilities for validating forms, handling API errors, and mapping FluentValidation errors from ASP.NET Core backends.

Common Patterns

Setting Up AppContext

import {
  AppContextProvider,
  makeProvider,
  SecurityService,
  NavigationService,
} from "@astroapps/client";

// At your app root
function App() {
  const security = yourSecurityService; // Your implementation
  const navigation = yourNavigationService; // Your implementation

  return (
    <AppContextProvider
      value={{
        security,
        navigation,
        // Add other services as needed
      }}
      providers={[
        makeProvider(SomeProvider, { someProp: "value" }),
        // Additional providers
      ]}
    >
      <YourApp />
    </AppContextProvider>
  );
}

Using Security Service

import { useSecurityService } from "@astroapps/client";

function UserProfile() {
  const security = useSecurityService();
  const user = security.currentUser.value;

  if (!user.loggedIn) {
    return <LoginPrompt />;
  }

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Roles: {user.roles?.join(", ")}</p>
      <button onClick={() => security.logout()}>Logout</button>
    </div>
  );
}

Authenticated API Requests

import { useSecurityService } from "@astroapps/client";

function DataFetcher() {
  const security = useSecurityService();

  const fetchData = async () => {
    // Automatically includes Bearer token
    const response = await security.fetch("/api/protected-data");
    const data = await response.json();
    return data;
  };

  return <button onClick={fetchData}>Fetch Data</button>;
}

Using API Clients

import { useApiClient } from "@astroapps/client";
import { UsersClient } from "./generated/api"; // Generated from OpenAPI

function UsersList() {
  const usersClient = useApiClient(UsersClient);
  const [users, setUsers] = useState([]);

  useEffect(() => {
    usersClient.getUsers().then(setUsers);
  }, []);

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Navigation and Routing

import { useNavigationService } from "@astroapps/client";

function Navigation() {
  const nav = useNavigationService();

  return (
    <nav>
      {/* Type-safe Link component */}
      <nav.Link href="/dashboard">Dashboard</nav.Link>
      <nav.Link href="/settings">Settings</nav.Link>

      {/* Programmatic navigation */}
      <button onClick={() => nav.push("/profile")}>
        Go to Profile
      </button>

      {/* Current location info */}
      <p>Current path: {nav.pathname}</p>
      <p>Query: {JSON.stringify(nav.query)}</p>
    </nav>
  );
}

Query Parameter Syncing

import { useSyncParam, StringParam, useQueryControl } from "@astroapps/client";

function SearchPage() {
  const queryControl = useQueryControl();

  // Sync form control with URL query parameter "q"
  const searchControl = useSyncParam(
    queryControl,
    "q",
    StringParam
  );

  return (
    <div>
      <input
        value={searchControl.value}
        onChange={(e) => {
          searchControl.value = e.target.value;
          // URL automatically updates to ?q=...
        }}
        placeholder="Search..."
      />
      <p>Search query: {searchControl.value}</p>
    </div>
  );
}

Custom Query Parameter Converters

import { useSyncParam, convertStringParam, useQueryControl } from "@astroapps/client";

function FilteredList() {
  const queryControl = useQueryControl();

  // Number parameter
  const pageControl = useSyncParam(
    queryControl,
    "page",
    convertStringParam(
      (num) => num.toString(),
      (str) => parseInt(str) || 1,
      1 // default value
    )
  );

  // Boolean parameter
  const showArchivedControl = useSyncParam(
    queryControl,
    "archived",
    convertStringParam(
      (bool) => bool.toString(),
      (str) => str === "true",
      false
    )
  );

  return (
    <div>
      <p>Page: {pageControl.value}</p>
      <button onClick={() => pageControl.value++}>Next Page</button>

      <label>
        <input
          type="checkbox"
          checked={showArchivedControl.value}
          onChange={(e) => {
            showArchivedControl.value = e.target.checked;
          }}
        />
        Show Archived
      </label>
    </div>
  );
}

Form Validation with API Errors

import { validateAndRunMessages, useToast } from "@astroapps/client";

function CreateUserForm() {
  const control = useControl({ name: "", email: "" });
  const toast = useToast();

  const handleSubmit = async () => {
    const success = await validateAndRunMessages(
      control,
      () => fetch("/api/users", {
        method: "POST",
        body: JSON.stringify(control.value),
      }),
      {
        400: "Invalid user data",
        409: "User already exists",
        429: "Too many requests, please try again later",
      },
      "An unexpected error occurred"
    );

    if (success) {
      toast.addToast("User created successfully!", { type: "success" });
    }
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <input {...control.fields.name.bind()} placeholder="Name" />
      <input {...control.fields.email.bind()} placeholder="Email" />
      <button type="submit">Create User</button>
    </form>
  );
}

Handling FluentValidation Errors

import { badRequestToErrors } from "@astroapps/client";

function UpdateProfileForm() {
  const control = useControl({ firstName: "", lastName: "", age: 0 });

  const handleSubmit = async () => {
    try {
      await fetch("/api/profile", {
        method: "PUT",
        body: JSON.stringify(control.value),
      });
    } catch (error) {
      // Automatically maps FluentValidation errors to form controls
      badRequestToErrors(
        error,
        control,
        // Optional: custom error message mapping
        (err) => {
          if (err.ErrorCode === "TooYoung") return "Must be 18 or older";
          return err.ErrorMessage;
        }
      );
    }
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <input {...control.fields.firstName.bind()} />
      {control.fields.firstName.error && (
        <span className="error">{control.fields.firstName.error}</span>
      )}

      <input {...control.fields.lastName.bind()} />
      {control.fields.lastName.error && (
        <span className="error">{control.fields.lastName.error}</span>
      )}

      <input type="number" {...control.fields.age.bind()} />
      {control.fields.age.error && (
        <span className="error">{control.fields.age.error}</span>
      )}

      <button type="submit">Update</button>
    </form>
  );
}

Toast Notifications

import { useToast } from "@astroapps/client";

function NotificationExample() {
  const toast = useToast();

  const showSuccess = () => {
    toast.addToast("Operation completed!", { type: "success" });
  };

  const showError = () => {
    toast.addToast("Something went wrong", { type: "error" });
  };

  const showWithAction = () => {
    toast.addToast("File deleted", {
      type: "info",
      action: {
        label: "Undo",
        response: () => {
          // Undo logic here
        },
      },
    });
  };

  return (
    <div>
      <button onClick={showSuccess}>Success</button>
      <button onClick={showError}>Error</button>
      <button onClick={showWithAction}>With Action</button>
    </div>
  );
}

Breadcrumbs

import { useBreadcrumbService, getBreadcrumbs } from "@astroapps/client";

const routes = {
  admin: {
    label: "Admin",
    children: {
      users: { label: "Users" },
      settings: { label: "Settings" },
    },
  },
};

function Breadcrumbs() {
  const nav = useNavigationService();
  const breadcrumbService = useBreadcrumbService();

  // Get breadcrumb links from current path
  const breadcrumbs = getBreadcrumbs(
    routes,
    nav.pathSegments,
    "",
    breadcrumbService.overrideLabels || {}
  );

  return (
    <nav>
      {breadcrumbs.map((crumb, i) => (
        <span key={i}>
          {i > 0 && " / "}
          <a href={crumb.href}>{crumb.label}</a>
        </span>
      ))}
    </nav>
  );
}

// Set custom breadcrumb label dynamically
function UserDetailPage({ userId }: { userId: string }) {
  const breadcrumbs = useBreadcrumbService();

  useEffect(() => {
    fetchUser(userId).then(user => {
      breadcrumbs.setBreadcrumbLabel(
        `/admin/users/${userId}`,
        user.name
      );
    });
  }, [userId]);

  return <div>User details...</div>;
}

Best Practices

1. Use Context for Services

// ✅ DO - Access services via hooks
const security = useSecurityService();
const nav = useNavigationService();

// ❌ DON'T - Create services inline
const security = new MySecurityService(); // Won't work with AppContext

2. Sync Important State to URL

// ✅ DO - Sync search, filters, pagination to URL for shareability
const searchControl = useSyncParam(queryControl, "q", StringParam);
const pageControl = useSyncParam(queryControl, "page", NumberParam);

// ❌ DON'T - Keep important state only in component
const [search, setSearch] = useState(""); // Lost on page refresh

3. Handle All Error Scenarios

// ✅ DO - Handle specific error codes
await validateAndRunMessages(
  control,
  action,
  {
    400: "Invalid data",
    401: "Please log in",
    403: "Access denied",
    404: "Not found",
    409: "Already exists",
  },
  "An unexpected error occurred"
);

// ❌ DON'T - Use generic error handling
try {
  await action();
} catch (e) {
  alert("Error"); // Not user-friendly
}

4. Use Automatic Token Injection

// ✅ DO - Use security.fetch for authenticated requests
await security.fetch("/api/data");

// ❌ DON'T - Manually add auth headers
await fetch("/api/data", {
  headers: { Authorization: `Bearer ${token}` }
});

Troubleshooting

Common Issues

Issue: Services undefined in components

  • Cause: Component not wrapped in AppContextProvider
  • Solution: Ensure AppContextProvider wraps your app root with services in value prop

Issue: Query parameters not syncing

  • Cause: Not calling useDefaultSyncRoute or missing QueryControl
  • Solution: Use useDefaultSyncRoute with queryControl and navigation service

Issue: Form errors not displaying after API call

  • Cause: Error format doesn't match FluentValidation structure
  • Solution: Verify backend returns errors in FluentValidation format or use custom error mapping

Issue: Infinite re-renders with useSyncParam

  • Cause: Creating new converter object on each render
  • Solution: Use provided converters (StringParam, etc.) or memoize custom converters with useMemo

Issue: Security.fetch not adding Bearer token

  • Cause: accessToken not set in UserState
  • Solution: Ensure login flow sets accessToken in security.currentUser.value

Issue: Navigation not updating URL

  • Cause: Using wrong navigation service or not integrated with router
  • Solution: Use appropriate navigation service for your framework (e.g., useNextNavigationService for Next.js)

Package Information

  • Package: @astroapps/client
  • Path: astrolabe-client/
  • Published to: npm
  • Version: 2.6.0+