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
63%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./frontend/react-project-starter/SKILL.mdScaffold 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 templatesrc/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 --noEmitnpm 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.181fcbc
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.