React TypeScript Setup and Configuration
Overview
React TypeScript setup with modern development patterns, type safety, and performance optimizations, following AzmX development standards for scalable frontend applications.
Core Dependencies
From typical React TypeScript package.json:
{
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"typescript": "^5.5.4"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.0",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.9",
"tailwindcss": "^3.4.10",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.41"
}
}
Project Structure
react-typescript-app/
├── public/
│ ├── index.html
│ ├── favicon.ico
│ └── manifest.json
├── src/
│ ├── components/
│ │ ├── common/
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.test.tsx
│ │ │ │ └── index.ts
│ │ │ └── Input/
│ │ └── layout/
│ │ ├── Header/
│ │ ├── Footer/
│ │ └── Sidebar/
│ ├── pages/
│ │ ├── Home/
│ │ ├── About/
│ │ └── Contact/
│ ├── hooks/
│ │ ├── useApi.ts
│ │ ├── useLocalStorage.ts
│ │ └── useAuth.ts
│ ├── services/
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ └── storage.ts
│ ├── types/
│ │ ├── api.ts
│ │ ├── user.ts
│ │ └── common.ts
│ ├── utils/
│ │ ├── constants.ts
│ │ ├── helpers.ts
│ │ └── validation.ts
│ ├── styles/
│ │ ├── globals.css
│ │ ├── components.css
│ │ └── utilities.css
│ ├── App.tsx
│ ├── main.tsx
│ └── vite-env.d.ts
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
├── postcss.config.js
├── eslint.config.js
└── package.json
TypeScript Configuration
TSConfig (tsconfig.json)
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/components/*": ["src/components/*"],
"@/hooks/*": ["src/hooks/*"],
"@/services/*": ["src/services/*"],
"@/types/*": ["src/types/*"],
"@/utils/*": ["src/utils/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Vite Configuration (vite.config.ts)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
host: '0.0.0.0',
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
})
Type Definitions
Common Types (src/types/common.ts)
// Base response structure
export interface ApiResponse<T = any> {
success: boolean
data?: T
message?: string
errors?: Record<string, string[]>
}
// Pagination
export interface PaginationParams {
page: number
limit: number
sortBy?: string
sortOrder?: 'asc' | 'desc'
}
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
limit: number
totalPages: number
}
// Form state
export interface FormState {
isSubmitting: boolean
errors: Record<string, string>
touched: Record<string, boolean>
}
// Component props
export interface BaseComponentProps {
className?: string
children?: React.ReactNode
testId?: string
}
// API states
export type LoadingState = 'idle' | 'loading' | 'succeeded' | 'failed'
export interface AsyncState<T = any> {
data: T | null
status: LoadingState
error: string | null
}
User Types (src/types/user.ts)
export interface User {
id: number
email: string
firstName: string
lastName: string
avatar?: string
role: UserRole
isActive: boolean
createdAt: string
updatedAt: string
}
export type UserRole = 'admin' | 'user' | 'moderator'
export interface CreateUserRequest {
email: string
firstName: string
lastName: string
password: string
role?: UserRole
}
export interface UpdateUserRequest {
firstName?: string
lastName?: string
avatar?: string
role?: UserRole
isActive?: boolean
}
export interface AuthResponse {
user: User
accessToken: string
refreshToken: string
}
export interface LoginRequest {
email: string
password: string
rememberMe?: boolean
}
Component Architecture
Button Component (src/components/common/Button/Button.tsx)
import React from 'react'
import { BaseComponentProps } from '@/types/common'
export interface ButtonProps extends BaseComponentProps {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
type?: 'button' | 'submit' | 'reset'
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
type = 'button',
className = '',
children,
testId,
onClick,
}) => {
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
outline: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:ring-blue-500',
ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
const classes = [
baseClasses,
variantClasses[variant],
sizeClasses[size],
disabled || loading ? 'opacity-50 cursor-not-allowed' : '',
className,
].join(' ')
return (
<button
type={type}
className={classes}
disabled={disabled || loading}
onClick={onClick}
data-testid={testId}
>
{loading && (
<svg
className="w-4 h-4 mr-2 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{children}
</button>
)
}
export default Button
Custom Hooks (src/hooks/useApi.ts)
import { useState, useEffect, useCallback } from 'react'
import { AsyncState, ApiResponse } from '@/types/common'
interface UseApiOptions {
immediate?: boolean
onSuccess?: (data: any) => void
onError?: (error: string) => void
}
export function useApi<T>(
apiCall: () => Promise<ApiResponse<T>>,
options: UseApiOptions = {}
): AsyncState<T> & {
refetch: () => Promise<void>
} {
const { immediate = false, onSuccess, onError } = options
const [state, setState] = useState<AsyncState<T>>({
data: null,
status: 'idle',
error: null,
})
const execute = useCallback(async () => {
setState(prev => ({ ...prev, status: 'loading', error: null }))
try {
const response = await apiCall()
if (response.success && response.data) {
setState({
data: response.data,
status: 'succeeded',
error: null,
})
onSuccess?.(response.data)
} else {
const errorMessage = response.message || 'An error occurred'
setState({
data: null,
status: 'failed',
error: errorMessage,
})
onError?.(errorMessage)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Network error'
setState({
data: null,
status: 'failed',
error: errorMessage,
})
onError?.(errorMessage)
}
}, [apiCall, onSuccess, onError])
useEffect(() => {
if (immediate) {
execute()
}
}, [immediate, execute])
return {
...state,
refetch: execute,
}
}
// Usage example hook
export function useUsers() {
return useApi(() => fetch('/api/users').then(res => res.json()), {
immediate: true,
})
}
API Service Layer
API Service (src/services/api.ts)
import { ApiResponse } from '@/types/common'
class ApiService {
private baseURL: string
private defaultHeaders: Record<string, string>
constructor() {
this.baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
this.defaultHeaders = {
'Content-Type': 'application/json',
}
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const token = localStorage.getItem('accessToken')
const config: RequestInit = {
...options,
headers: {
...this.defaultHeaders,
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
}
try {
const response = await fetch(`${this.baseURL}${endpoint}`, config)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('API request failed:', error)
throw error
}
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'GET' })
}
async post<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
})
}
async put<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
})
}
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'DELETE' })
}
}
export const apiService = new ApiService()
export default apiService
Styling Configuration
Tailwind Config (tailwind.config.js)
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
900: '#111827',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}
Global Styles (src/styles/globals.css)
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-sans;
}
}
@layer components {
.btn {
@apply inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.input {
@apply flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
}
}
Testing Configuration
Jest Configuration (jest.config.js)
export default {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/main.tsx',
'!src/vite-env.d.ts',
],
}
Test Setup (src/test-setup.ts)
import '@testing-library/jest-dom'
import { configure } from '@testing-library/react'
// Configure testing library
configure({ testIdAttribute: 'data-testid' })
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
}
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})
Environment Configuration
Environment Variables (.env.development)
# API Configuration
VITE_API_URL=http://localhost:8000/api
VITE_APP_ENV=development
# Authentication
VITE_AUTH_TOKEN_KEY=accessToken
VITE_REFRESH_TOKEN_KEY=refreshToken
# Feature Flags
VITE_ENABLE_ANALYTICS=false
VITE_ENABLE_SENTRY=false
# External Services
VITE_SENTRY_DSN=
VITE_GA_TRACKING_ID=
Production Environment (.env.production)
# API Configuration
VITE_API_URL=https://api.azmx.sa/api
VITE_APP_ENV=production
# Authentication
VITE_AUTH_TOKEN_KEY=accessToken
VITE_REFRESH_TOKEN_KEY=refreshToken
# Feature Flags
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_SENTRY=true
# External Services
VITE_SENTRY_DSN=${SENTRY_DSN}
VITE_GA_TRACKING_ID=${GA_TRACKING_ID}
Build and Deployment
Docker Configuration (Dockerfile)
FROM node:18-alpine as build
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built application
COPY --from=build /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx Configuration (nginx.conf)
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Handle React Router
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
}
Development Scripts
Package.json Scripts
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --ext ts,tsx --fix",
"type-check": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\""
}
}
Performance Optimizations
Code Splitting Example
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
// Lazy load components
const Home = lazy(() => import('@/pages/Home'))
const About = lazy(() => import('@/pages/About'))
const Contact = lazy(() => import('@/pages/Contact'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
)
}
Memoization Best Practices
import React, { memo, useMemo, useCallback } from 'react'
interface UserListProps {
users: User[]
onUserSelect: (userId: number) => void
}
export const UserList = memo<UserListProps>(({ users, onUserSelect }) => {
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) => a.lastName.localeCompare(b.lastName))
}, [users])
const handleUserClick = useCallback((userId: number) => {
onUserSelect(userId)
}, [onUserSelect])
return (
<div className="space-y-2">
{sortedUsers.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => handleUserClick(user.id)}
/>
))}
</div>
)
})
Development Commands
Common Development Tasks
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Type checking
npm run type-check
# Linting
npm run lint
npm run lint:fix
# Format code
npm run format
# Install dependencies
npm install
# Update dependencies
npm update