CtrlK
BlogDocsLog inGet started
Tessl Logo

react-project-starter

Scaffold a React 19 + Vite project with TypeScript strict mode, Tailwind CSS v4, React Router v7, and ESLint flat config using a feature-based folder structure.

72

Quality

63%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./frontend/react-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

React Project Starter

Scaffold a React 19 + Vite project with TypeScript strict mode, Tailwind CSS v4, React Router v7, and ESLint flat config using a feature-based folder structure.

Prerequisites

  • Node.js >= 20.x
  • npm >= 10.x (or pnpm >= 9.x)
  • Git

Scaffold Command

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm install react-router@7 react-router-dom@7
npm install tailwindcss @tailwindcss/vite
npm install -D eslint @eslint/js typescript-eslint globals eslint-plugin-react-hooks eslint-plugin-react-refresh

# Configure path aliases (add to tsconfig.app.json)

Project Structure

src/
├── app/
│   ├── App.tsx                # Root component with RouterProvider
│   ├── router.tsx             # Route definitions (createBrowserRouter)
│   └── providers.tsx          # Composed context providers
├── features/
│   ├── auth/
│   │   ├── components/        # Feature-specific components
│   │   ├── hooks/             # Feature-specific hooks
│   │   ├── context/           # Feature-specific context
│   │   ├── services/          # API calls for this feature
│   │   ├── types.ts           # Feature-specific types
│   │   └── index.ts           # Public API barrel export
│   ├── dashboard/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── index.ts
│   └── settings/
│       ├── components/
│       ├── hooks/
│       └── index.ts
├── shared/
│   ├── components/            # Reusable UI components (Button, Modal, etc.)
│   ├── hooks/                 # Generic reusable hooks
│   ├── utils/                 # Pure utility functions
│   ├── types/                 # Global shared types
│   └── constants/             # App-wide constants
├── assets/                    # Static assets (images, fonts)
├── styles/
│   └── app.css                # Tailwind CSS entry point
├── main.tsx                   # Entry point (ReactDOM.createRoot)
└── vite-env.d.ts
.env.example                    # Required env vars template

Key Conventions

  • Feature-based organization: each feature is a self-contained folder under src/features/. Cross-feature imports go through barrel index.ts files only.
  • TypeScript strict mode: enable "strict": true in tsconfig.json. No any without explicit justification.
  • Path aliases: configure @/ to resolve to src/ in both tsconfig.json and vite.config.ts.
  • Named exports only: avoid export default to keep refactoring and auto-imports predictable.
  • Component files: one component per file, file name matches component name in PascalCase.
  • Hooks prefix: all custom hooks start with use and live in hooks/ directories.
  • No index.tsx for components: name component files explicitly (LoginForm.tsx, not index.tsx) to improve file search.

Essential Patterns

Path Alias Config (tsconfig.app.json)

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Vite Path Alias (vite.config.ts)

resolve: {
  alias: {
    '@': path.resolve(__dirname, './src'),
  },
},

TypeScript Config (tsconfig.json additions)

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

Vite Config (vite.config.ts)

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

Tailwind CSS Entry (src/styles/app.css)

@import "tailwindcss";

ESLint Flat Config (eslint.config.js)

import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2024,
      globals: globals.browser,
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
    },
  }
);

Component Structure

// src/features/auth/components/LoginForm.tsx
import { useState } from "react";
import { useAuth } from "@/features/auth/hooks/useAuth";
import { Button } from "@/shared/components/Button";

interface LoginFormProps {
  onSuccess: () => void;
}

export function LoginForm({ onSuccess }: LoginFormProps) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const { login, isLoading, error } = useAuth();

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    await login({ email, password });
    onSuccess();
  }

  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        className="rounded border px-3 py-2"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        className="rounded border px-3 py-2"
        required
      />
      {error && <p className="text-sm text-red-600">{error}</p>}
      <Button type="submit" disabled={isLoading}>
        {isLoading ? "Signing in..." : "Sign In"}
      </Button>
    </form>
  );
}

Custom Hook

// src/features/auth/hooks/useAuth.ts
import { useState, useCallback } from "react";
import { useAuthContext } from "@/features/auth/context/AuthContext";
import { authService } from "@/features/auth/services/authService";

interface LoginCredentials {
  email: string;
  password: string;
}

export function useAuth() {
  const { setUser } = useAuthContext();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const login = useCallback(async (credentials: LoginCredentials) => {
    setIsLoading(true);
    setError(null);
    try {
      const user = await authService.login(credentials);
      setUser(user);
    } catch (err) {
      const message = err instanceof Error ? err.message : "Login failed";
      setError(message);
      throw err;
    } finally {
      setIsLoading(false);
    }
  }, [setUser]);

  const logout = useCallback(async () => {
    await authService.logout();
    setUser(null);
  }, [setUser]);

  return { login, logout, isLoading, error };
}

Context Provider

// src/features/auth/context/AuthContext.tsx
import { createContext, useContext, useState, type ReactNode } from "react";

interface User {
  id: string;
  email: string;
  name: string;
}

interface AuthContextValue {
  user: User | null;
  setUser: (user: User | null) => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const value: AuthContextValue = {
    user,
    setUser,
    isAuthenticated: user !== null,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuthContext(): AuthContextValue {
  const context = useContext(AuthContext);
  if (context === null) {
    throw new Error("useAuthContext must be used within an AuthProvider");
  }
  return context;
}

Composed Providers

// src/app/providers.tsx
import type { ReactNode } from "react";
import { AuthProvider } from "@/features/auth/context/AuthContext";

export function AppProviders({ children }: { children: ReactNode }) {
  return (
    <AuthProvider>
      {children}
    </AuthProvider>
  );
}

Router with Lazy Loading

// src/app/router.tsx
import { createBrowserRouter, Navigate } from "react-router-dom";
import { lazy, Suspense } from "react";

const DashboardPage = lazy(() => import("@/features/dashboard/components/DashboardPage"));
const SettingsPage = lazy(() => import("@/features/settings/components/SettingsPage"));
const LoginPage = lazy(() => import("@/features/auth/components/LoginPage"));

function LazyPage({ Component }: { Component: React.LazyExoticComponent<() => React.JSX.Element> }) {
  return (
    <Suspense fallback={<div className="flex h-screen items-center justify-center">Loading...</div>}>
      <Component />
    </Suspense>
  );
}

export const router = createBrowserRouter([
  {
    path: "/",
    element: <Navigate to="/dashboard" replace />,
  },
  {
    path: "/login",
    element: <LazyPage Component={LoginPage} />,
  },
  {
    path: "/dashboard",
    element: <LazyPage Component={DashboardPage} />,
  },
  {
    path: "/settings",
    element: <LazyPage Component={SettingsPage} />,
  },
]);

App Entry

// src/app/App.tsx
import { RouterProvider } from "react-router-dom";
import { AppProviders } from "@/app/providers";
import { router } from "@/app/router";

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

Main Entry Point

// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "@/app/App";
import "@/styles/app.css";

const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");

createRoot(rootElement).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Environment Variables

// Environment variables — prefix with VITE_ to expose to client
const API_URL = import.meta.env.VITE_API_URL;

Error Boundary

import { Component, type ReactNode } from "react";

interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean }

class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false };
  static getDerivedStateFromError(): State { return { hasError: true }; }
  render() {
    if (this.state.hasError) return this.props.fallback ?? <p>Something went wrong.</p>;
    return this.props.children;
  }
}
export default ErrorBoundary;

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Install dependencies: npm install
  3. Start dev server: npm run dev
  4. Verify the app loads at http://localhost:5173
  5. Run npx tsc --noEmit to confirm TypeScript is clean

Common Commands

# Development
npm run dev                    # Start dev server (default: http://localhost:5173)

# Build
npm run build                  # TypeScript check + Vite production build
npm run preview                # Preview production build locally

# Lint
npx eslint .                   # Run ESLint on all files
npx eslint . --fix             # Auto-fix lint issues

# Type check (without emitting)
npx tsc --noEmit

Integration Notes

  • Testing: pair with Vitest + React Testing Library. Add npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom.
  • State management: for complex global state beyond context, add Zustand (npm install zustand) or TanStack Query for server state (npm install @tanstack/react-query).
  • API layer: pair with an API client skill (fetch wrapper or Axios). TanStack Query handles caching/refetching.
  • Auth: pair with an auth skill (JWT, OAuth, or session-based). The context provider pattern shown above is the integration point.
  • UI components: Tailwind CSS v4 handles styling. For prebuilt components, add shadcn/ui (npx shadcn@latest init).
  • Forms: for complex forms, add React Hook Form (npm install react-hook-form) + Zod (npm install zod) for validation.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.