All skills
softaworks avatar

/react-dev

@1c0662a
by softaworkssoftaworks/agent-toolkit2.5k stars
230

This skill should be used when building React components with TypeScript, typing hooks, handling events, or when React TypeScript, React 19, Server Components are mentioned. Covers type-safe patterns for React 18-19 including generic components, proper event typing, and routing integration (TanStack Router, React Router).

Use this Skill: https://skilld.dev/gh/softaworks/agent-toolkit/react-dev

This session only. Nothing lands on disk.

referencesreact-router.md

≈5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

React Router v7 - Comprehensive Reference Guide

Overview

React Router is a modern routing library that provides flexible client-side and server-side routing for React applications. It offers two primary modes of operation: Framework Mode and Data Mode, with first-class TypeScript support through automatic type generation.

Installation and Setup

Basic Installation

npm install react-router

TypeScript Configuration

React Router generates type-specific files for each route. Configure your tsconfig.json:

{
  "include": [".react-router/types/**/*"],
  "compilerOptions": {
    "rootDirs": [".", "./.react-router/types"]
  }
}

Add .react-router/ to .gitignore:

.react-router/

Extending AppLoadContext

Define your app's context type in a .ts or .d.ts file:

import "react-router";

declare module "react-router" {
  interface AppLoadContext {
    // add context properties here
    apiClient?: APIClient;
    userId?: string;
  }
}

Route Definition Patterns

Basic Route Configuration

Routes are defined as objects passed to createBrowserRouter:

import { createBrowserRouter } from "react-router";

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root
  },
  {
    path: "/about",
    Component: About
  },
]);

Nested Routes

Child routes are rendered through an <Outlet/> in the parent component:

createBrowserRouter([
  {
    path: "/dashboard",
    Component: Dashboard,
    children: [
      { index: true, Component: DashboardHome },
      { path: "settings", Component: Settings },
    ],
  },
]);

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Outlet /> {/* Renders child route */}
    </div>
  );
}

Layout Routes (No Path)

Create layout containers without adding URL segments:

createBrowserRouter([
  {
    // no path - just a layout
    Component: MarketingLayout,
    children: [
      { index: true, Component: Home },
      { path: "contact", Component: Contact },
    ],
  },
]);

Index Routes

Define default routes at a path:

createBrowserRouter([
  {
    path: "/",
    Component: Root,
    children: [
      { index: true, Component: Home }, // renders at "/"
      { path: "about", Component: About },
    ],
  },
]);

Dynamic Segments

Use colons to define dynamic parameters:

{
  path: "teams/:teamId",
  loader: async ({ params }) => {
    return await fetchTeam(params.teamId);
  },
  Component: Team,
}

Multiple dynamic segments:

{
  path: "posts/:postId/comments/:commentId",
  // params.postId and params.commentId available
}

Optional Segments

{
  path: ":lang?/categories" // lang is optional
}

Splat Routes (Catchall)

{
  path: "files/*",
  loader: async ({ params }) => {
    const splat = params["*"]; // remaining URL after "files/"
  },
}

TypeScript Typing

Route Type Imports

In Framework Mode, import route-specific types from the generated +types directory:

import type { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
  // params is typed as { id: string }
  return { planet: `world #${params.id}` };
}

export default function Product({
  loaderData, // typed from loader
}: Route.ComponentProps) {
  return <h1>{loaderData.planet}</h1>;
}

Generated Types

React Router generates these types for each route:

  • Route.LoaderArgs - Loader function arguments
  • Route.ClientLoaderArgs - Client loader arguments
  • Route.ActionArgs - Action function arguments
  • Route.ClientActionArgs - Client action arguments
  • Route.ComponentProps - Props for default export
  • Route.ErrorBoundaryProps - Props for ErrorBoundary
  • Route.HydrateFallbackProps - Props for HydrateFallback

Manual Loader Data Typing

In Data Mode, type loader data manually:

export async function loader() {
  return { invoices: await getInvoices() };
}

export default function Invoices() {
  const data = useLoaderData<typeof loader>();
  // data is typed as { invoices: ... }
}

Action Data Typing

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  return { message: `Hello, ${formData.get("name")}` };
}

export default function Form() {
  const actionData = useActionData<typeof action>();
  // actionData is typed correctly
}

Data Loading with Loaders

Basic Loader Pattern

Loaders provide data to route components before rendering:

createBrowserRouter([
  {
    path: "/posts/:postId",
    loader: async ({ params }) => {
      const post = await fetchPost(params.postId);
      return { post };
    },
    Component: Post,
  },
]);

function Post() {
  const { post } = useLoaderData();
  return <h1>{post.title}</h1>;
}

Loader Arguments

type LoaderFunctionArgs = {
  params: Params; // URL parameters
  request: Request; // fetch API Request
  context?: AppLoadContext; // shared context
};

Error Handling in Loaders

Throw errors with data() function for client-side error boundaries:

import { data } from "react-router";

export async function loader({ params }: Route.LoaderArgs) {
  const record = await fakeDb.getRecord(params.id);

  if (!record) {
    throw data("Record Not Found", { status: 404 });
  }

  return record;
}

Revalidation Control

Control when loaders rerun:

export const shouldRevalidate: ShouldRevalidateFunctionArgs = (args) => {
  // Return true to revalidate, false to skip
  // Default: true when params change or after successful action
  return true;
};

Lazy Loading

Load components and loaders on-demand:

{
  path: "/app",
  lazy: async () => {
    const { Component, loader } = await import("./app");
    return { Component, loader };
  },
}

Actions and Form Handling

Basic Action Pattern

Actions handle form submissions and mutations:

createBrowserRouter([
  {
    path: "/projects",
    action: async ({ request }) => {
      const formData = await request.formData();
      const title = formData.get("title");
      const project = await createProject({ title });
      return project; // data available via useActionData
    },
    Component: Projects,
  },
]);

Form Submission Methods

Using <Form> Component

Causes navigation and adds history entry:

import { Form } from "react-router";

export default function CreateEvent() {
  return (
    <Form action="/events" method="post">
      <input type="text" name="title" />
      <button type="submit">Create</button>
    </Form>
  );
}
Using useSubmit Hook

Imperative form submission:

import { useSubmit } from "react-router";

export default function Timer() {
  const submit = useSubmit();

  const handleTimeout = () => {
    submit(
      { quizTimedOut: true },
      { action: "/end-quiz", method: "post" }
    );
  };

  return <div>{/* ... */}</div>;
}
Using useFetcher Hook

Submit without navigation (no history entry):

import { useFetcher } from "react-router";

export default function Task() {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method="post" action="/update-task/123">
      <input type="text" name="title" />
      <button type="submit">Save</button>
    </fetcher.Form>
  );
}

Form Validation

Return errors with non-2xx status codes:

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email"));

  const errors: Record<string, string> = {};

  if (!email.includes("@")) {
    errors.email = "Invalid email";
  }

  if (Object.keys(errors).length > 0) {
    return data({ errors }, { status: 400 }); // Don't revalidate
  }

  await createUser({ email });
  return redirect("/dashboard");
}

export default function Signup() {
  const fetcher = useFetcher();
  const errors = fetcher.data?.errors;

  return (
    <fetcher.Form method="post">
      <input name="email" />
      {errors?.email && <span>{errors.email}</span>}
      <button>Sign Up</button>
    </fetcher.Form>
  );
}

Accessing Action Data

import { useActionData } from "react-router";

export default function Project() {
  const actionData = useActionData<typeof action>();

  return (
    <div>
      <Form method="post">
        {/* form fields */}
      </Form>
      {actionData && <p>Success: {actionData.message}</p>}
    </div>
  );
}

Navigation Patterns

Link Component

For client-side navigation without active styling:

import { Link } from "react-router";

<Link to="/dashboard">Dashboard</Link>

<Link to={{
  pathname: "/search",
  search: "?q=react",
  hash: "#results"
}}>
  Search
</Link>

NavLink Component

For navigation with active/pending states:

import { NavLink } from "react-router";

<NavLink to="/messages">
  {({ isActive, isPending }) => (
    <span className={isActive ? "active" : ""}>
      Messages
    </span>
  )}
</NavLink>

Programmatic Navigation

import { useNavigate } from "react-router";

export default function LogoutButton() {
  const navigate = useNavigate();

  return (
    <button onClick={() => navigate("/")}>
      Go Home
    </button>
  );
}

Navigate Options

navigate("/path", {
  replace: true,              // Don't add to history
  state: { from: "/" },       // Pass state
  relative: "route",          // Or "path"
  preventScrollReset: true,   // Don't scroll to top
  viewTransition: true,       // Enable view transition
});

navigate(-1);  // Go back
navigate(1);   // Go forward

Redirect in Loaders/Actions

import { redirect } from "react-router";

export async function loader({ request }: Route.LoaderArgs) {
  const user = await getUser(request);
  if (!user) {
    return redirect("/login");
  }
  return { user };
}

export async function action() {
  const project = await createProject();
  return redirect(`/projects/${project.id}`);
}

URL Parameters and Search Params

Route Parameters

Access dynamic route parameters:

import { useParams } from "react-router";

export default function Post() {
  const params = useParams<{ postId: string }>();
  // params.postId is typed as string

  return <h1>Post: {params.postId}</h1>;
}

Search Parameters

Handle query strings:

import { useSearchParams } from "react-router";

export default function Search() {
  const [searchParams, setSearchParams] = useSearchParams();

  const query = searchParams.get("q");
  const page = searchParams.get("page") || "1";

  return (
    <div>
      <input value={query} />
      <button onClick={() => setSearchParams({ q: "react" })}>
        Search
      </button>
    </div>
  );
}

Setting Search Params

// String
setSearchParams("?tab=1");

// Object
setSearchParams({ tab: "1" });

// Multiple values
setSearchParams({ brand: ["nike", "reebok"] });

// Callback
setSearchParams((prev) => {
  prev.set("tab", "2");
  return prev;
});

Error Handling

Framework Mode Error Boundaries

import { Route } from "./+types/root";
import { isRouteErrorResponse } from "react-router";

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
      </div>
    );
  }

  return <h1>Unknown Error</h1>;
}

Data Mode Error Boundaries

import { useRouteError, isRouteErrorResponse } from "react-router";

function RootErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return <h1>Error</h1>;
}

createBrowserRouter([
  {
    path: "/",
    Component: Root,
    ErrorBoundary: RootErrorBoundary,
  },
]);

Throwing Errors

Unintentional errors are caught:

export async function loader() {
  return undefined(); // Error thrown and caught
}

Intentional errors with status codes:

import { data } from "react-router";

export async function loader({ params }: Route.LoaderArgs) {
  const post = await getPost(params.id);
  if (!post) {
    throw data("Not Found", { status: 404 });
  }
  return post;
}

Advanced Hooks

useLoaderData

Get data from the route's loader:

export default function Component() {
  const data = useLoaderData<typeof loader>();
  // fully typed
}

useActionData

Get data from the last action submission:

export default function Form() {
  const actionData = useActionData<typeof action>();
  return actionData ? <p>{actionData.message}</p> : null;
}

useFetcher

Independent data submission without navigation:

const fetcher = useFetcher();

fetcher.state; // "idle" | "loading" | "submitting"
fetcher.data; // data from action/loader
fetcher.Form; // component for forms
fetcher.load(path); // load data
fetcher.submit(data, options); // submit data
fetcher.reset(); // reset state

Keyed fetchers (access from other components):

const fetcher = useFetcher({ key: "my-fetcher" });

useMatches

Get all active route matches:

const matches = useMatches();
// Array of: { route, pathname, params, data, handle }

matches.forEach(match => {
  console.log(match.data);    // Loader data
  console.log(match.handle);  // Route handle metadata
});

useLocation

Get current location:

const location = useLocation();
// {
//   pathname: "/posts/123",
//   search: "?tab=1",
//   hash: "#section",
//   state: { from: "/" }
// }

useNavigation

Track navigation state:

const navigation = useNavigation();

navigation.state; // "idle" | "loading" | "submitting"
navigation.location; // new location during navigation
navigation.formData; // form data being submitted

Advanced Route Features

Route Handle Property

Attach metadata to routes for use in ancestor components:

// Route definition
export const handle = {
  breadcrumb: () => <Link to="/posts">Posts</Link>,
  icon: "📝",
};

// Access in ancestor
function Root() {
  const matches = useMatches();

  return (
    <header>
      {matches
        .filter(m => m.handle?.breadcrumb)
        .map(m => m.handle.breadcrumb())}
    </header>
  );
}

Middleware

Run code before/after navigations:

export async function middleware({ request }, next) {
  console.log(`Starting: ${request.url}`);
  await next();
  console.log("Complete");
}

createBrowserRouter([
  {
    path: "/",
    middleware: [middleware],
    loader: rootLoader,
    Component: Root,
  },
]);

Outlet Component

Render child routes:

import { Outlet } from "react-router";

function Layout() {
  return (
    <div>
      <nav>{/* navigation */}</nav>
      <Outlet /> {/* Child routes render here */}
    </div>
  );
}

OutletContext

Pass data to child routes via Outlet:

// Parent
function Parent() {
  return <Outlet context={{ user: { name: "John" } }} />;
}

// Child
import { useOutletContext } from "react-router";

function Child() {
  const { user } = useOutletContext<{ user: User }>();
  return <p>{user.name}</p>;
}

Comparison with TanStack Router

Type Safety Similarities

Both React Router v7 and TanStack Router offer first-class TypeScript support:

Feature React Router v7 TanStack Router
Route type generation ✓ Automatic .react-router/types/ ✓ Automatic
Param typing ✓ Auto-inferred from path ✓ Auto-inferred
Loader data typing ✓ Via Route.LoaderArgs ✓ Via loader() return type
Action typing ✓ Via Route.ActionArgs ✓ Via action() return type
Search params typing ✓ Manual with useSearchParams ✓ Route-level definition
Redirect typing ✓ Standard function ✓ Standard function

Key Differences

Type Definition Level:

  • React Router: File-based type generation (.d.ts files)
  • TanStack Router: Route-level type registration

Search Params:

  • React Router: Runtime URLSearchParams API
  • TanStack Router: Compile-time schema validation

Nested Route Typing:

  • React Router: Inherited from parent context
  • TanStack Router: Explicit inheritance patterns

Error Typing:

  • React Router: isRouteErrorResponse() utility
  • TanStack Router: Built-in discriminated unions

Learning Curve:

  • React Router: Familiar to those migrating from v6
  • TanStack Router: More opinionated, explicit type patterns

Best Practices

1. Type Your Routes Consistently

// In Framework Mode, always use the generated types
import type { Route } from "./+types/my-route";

export async function loader({ params }: Route.LoaderArgs) {
  // params is typed
}

export default function Component({ loaderData }: Route.ComponentProps) {
  // loaderData is typed
}

2. Use Forms for Navigation-Causing Mutations

// Good: Uses <Form> for main app actions
<Form method="post" action="/projects">
  <input name="title" />
  <button>Create</button>
</Form>

3. Use Fetchers for Non-Navigation Mutations

// Good: Uses fetcher for secondary updates
const fetcher = useFetcher();

<fetcher.Form method="post" action="/task/123">
  <input name="status" />
  <button>Update</button>
</fetcher.Form>

4. Handle Validation Errors Properly

// Return with 4xx status to prevent revalidation
if (validationFailed) {
  return data({ errors }, { status: 400 });
}

5. Use redirect() in Loaders/Actions

// Prefer redirect in loaders/actions
if (!user) {
  return redirect("/login");
}

// Over useNavigate in components
const navigate = useNavigate(); // Reserve for rare cases

6. Leverage useMatches() for Layout Data

// Access parent route data in nested components
const matches = useMatches();
const parentData = matches[matches.length - 2]?.data;

Router Creation

Data Mode (createBrowserRouter)

import { createBrowserRouter, RouterProvider } from "react-router";

const router = createBrowserRouter([
  { path: "/", Component: Home },
  { path: "/about", Component: About },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

Framework Mode Setup

Use the framework convention with routes.ts:

import { route, type RouteConfig } from "@react-router/dev/routes";

export default [
  route("/", "./routes/index.tsx"),
  route("about", "./routes/about.tsx"),
  route("posts/:id", "./routes/post.tsx"),
] satisfies RouteConfig;

Router Options

const router = createBrowserRouter(routes, {
  basename: "/app",                    // URL base path
  future: { v7_... },                 // Future flags
  hydrationData: serverData,          // SSR hydration
  dataStrategy: customStrategy,       // Custom data loading
  getContext: createContext,          // Custom context
  patchRoutesOnNavigation: patchFn,  // Dynamic routes
  unstable_instrumentations: [logger] // Instrumentation
});

Conclusion

React Router v7 provides a mature, type-safe routing solution with automatic TypeScript support. Its familiar API for v6 users, combined with modern features like middleware, server-side rendering support, and comprehensive type generation, makes it a strong choice for building scalable React applications.

The key advantage over alternatives is the seamless integration with existing React patterns (hooks, components) and the framework's approach to type safety through automatic code generation rather than manual annotations.

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The react-dev skill is a comprehensive documentation and reference guide for building React applications with TypeScript, covering React 18 and 19 features, hooks, and routing. No security issues were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    9 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 1c0662a. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago
version
1.0.0
  • React
  • TypeScript
  • react-19
  • hooks
  • server-components
  • tanstack-router
  • react-router
  • event-handlers
  • generic-components

README badge

README badge for softaworks/agent-toolkit/react-dev

Teaches type-safe React component patterns with TypeScript across React 18-19, including generic components, event handler typing, hooks, Server Components, and router integration (TanStack Router, React Router v7). Covers React 19 breaking changes like ref-as-prop, useActionState, and use() for promise unwrapping.

Generated from the current SKILL.md.

Does this skill cover React 19?
Yes. The skill covers React 19 breaking changes including ref as prop (forwardRef deprecated), useActionState (replaces useFormState), and use() for unwrapping promises and context.
What routing libraries does this support?
TanStack Router and React Router v7. Both support type-safe routing with compile-time validation of params and search query strings.
Does this cover Server Components and Server Actions?
Yes. The skill includes patterns for async Server Components, 'use server' Server Actions, and passing promises to Client Components via use().
Can I use this with generic components?
Yes. The skill covers generic component patterns including constrained generics, keyof T for type-safe column access, and render props for custom rendering.
Does this work with non-TypeScript React?
No. The skill requires TypeScript and focuses exclusively on type-safe patterns for React 18-19.

Generated from the current SKILL.md. These answers refresh after source changes.