frontend/react-project-starter/SKILL.md
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.
npx skillsauth add achreftlili/deep-dev-skills react-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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)
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
src/features/. Cross-feature imports go through barrel index.ts files only."strict": true in tsconfig.json. No any without explicit justification.@/ to resolve to src/ in both tsconfig.json and vite.config.ts.export default to keep refactoring and auto-imports predictable.use and live in hooks/ directories.LoginForm.tsx, not index.tsx) to improve file search.tsconfig.app.json){
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
vite.config.ts)resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
tsconfig.json additions){
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
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"),
},
},
});
src/styles/app.css)@import "tailwindcss";
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 }],
},
}
);
// 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>
);
}
// 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 };
}
// 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;
}
// 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>
);
}
// 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} />,
},
]);
// 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>
);
}
// 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 — prefix with VITE_ to expose to client
const API_URL = import.meta.env.VITE_API_URL;
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;
.env.example to .env and fill in valuesnpm installnpm run devnpx tsc --noEmit to confirm TypeScript is clean# 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
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom.npm install zustand) or TanStack Query for server state (npm install @tanstack/react-query).npx shadcn@latest init).npm install react-hook-form) + Zod (npm install zod) for validation.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.