Sitemap

Setting Up a Production-Grade React Application: A Comprehensive Guide

6 min readMay 2, 2025

--

Photo by Lautaro Andreani on Unsplash

In this guide, we’ll walk through setting up a production-grade React application using the latest technologies and best practices. We’ll cover everything from project initialization to advanced features like code splitting and state management.

Prerequisites

Before we begin, make sure you have:

- Node.js (v18 or higher)

- npm or yarn

- A code editor (VS Code recommended)

Project Setup

Let’s start by creating a new project using Vite:

npm create vite@latest my-enterprise-app - - template react-ts
cd my-enterprise-app
npm install

## Installing Dependencies

We’ll need several packages for our production setup:

npm install @tanstack/react-query @tanstack/react-query-devtools
npm install react-router-dom
npm install zod
npm install axios
npm install @hookform/resolvers
npm install react-hook-form
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
npm install @emotion/react @emotion/styled
npm install @mui/material @mui/icons-material

Project Structure

Let’s organize our project with a clean, scalable structure:

src/
├── api/ # API related code
├── components/ # Reusable components
├── features/ # Feature-based modules
├── hooks/ # Custom hooks
├── layouts/ # Layout components
├── pages/ # Page components
├── store/ # State management
├── types/ # TypeScript types
├── utils/ # Utility functions
└── App.tsx

Configuration Files

TypeScript Configuration (tsconfig.json)

{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"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'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
chunkSizeWarningLimit: 1600,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
mui: ['@mui/material', '@mui/icons-material'],
},
},
},
},
})

Setting Up React Router

Create a router configuration:

// src/router/index.tsx
import { createBrowserRouter } from 'react-router-dom'
import { lazy } from 'react'
import Layout from '@/layouts/Layout'
const Home = lazy(() => import('@/pages/Home'))
const About = lazy(() => import('@/pages/About'))
export const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />,
},
{
path: 'about',
element: <About />,
},
],
},
])

Protected Routes Implementation

For enterprise applications, protecting routes is crucial. Here’s how to i have implement protected routes:

// src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/hooks/useAuth'
interface ProtectedRouteProps {
children: React.ReactNode
requiredRoles?: string[]
}
export const ProtectedRoute = ({ children, requiredRoles }: ProtectedRouteProps) => {
const { isAuthenticated, user, isLoading } = useAuth()

const location = useLocation()

if (isLoading) {
return <div>Loading…</div> // Or your loading component
}

if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />
}

if (requiredRoles && !requiredRoles.some(role => user?.roles?.includes(role))) {
return <Navigate to="/unauthorized" replace />
}

return <>{children}</>
}

Create an authentication hook:

// src/context/AuthContext.tsx
import { createContext, useContext, useReducer, ReactNode, useEffect } from 'react'
interface User {
id: string
email: string
roles: string[]
}

interface AuthState {
user: User | null
isAuthenticated: boolean
isLoading: boolean
}

type AuthAction =
| { type: 'LOGIN'; payload: User }
| { type: 'LOGOUT' }
| { type: 'LOADING' }

const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: true,
}

const authReducer = (state: AuthState, action: AuthAction): AuthState => {

switch (action.type) {
case 'LOGIN':
return {
user: action.payload,
isAuthenticated: true,
isLoading: false,
}
case 'LOGOUT':
return {
user: null,
isAuthenticated: false,
isLoading: false,
}
case 'LOADING':
return {
…state,
isLoading: true,
}
default:
return state
}
}

interface AuthContextType {
state: AuthState
login: (user: User) => void
logout: () => void
}
const AuthContext = createContext<AuthContextType | null>(null)

export const AuthProvider = ({ children }: { children: ReactNode }) => {

const [state, dispatch] = useReducer(authReducer, initialState)

useEffect(() => {
// Check for stored user data on mount
const storedUser = localStorage.getItem('user')
if (storedUser) {
dispatch({ type: 'LOGIN', payload: JSON.parse(storedUser) })
} else {
dispatch({ type: 'LOGOUT' })
}
}, []);

const login = (user: User) => {
localStorage.setItem('user', JSON.stringify(user))
dispatch({ type: 'LOGIN', payload: user })
}

const logout = () => {
localStorage.removeItem('user')
dispatch({ type: 'LOGOUT' })
}
return (
<AuthContext.Provider value={{ state, login, logout }}>
{children}
</AuthContext.Provider>
)
}

export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}

Update the router configuration to include protected routes:

// src/router/index.tsx
import { createBrowserRouter } from 'react-router-dom'
import { lazy } from 'react'
import Layout from '@/layouts/Layout'
import { ProtectedRoute } from '@/components/ProtectedRoute'
const Home = lazy(() => import('@/pages/Home'))
const About = lazy(() => import('@/pages/About'))
const Dashboard = lazy(() => import('@/pages/Dashboard'))
const AdminPanel = lazy(() => import('@/pages/AdminPanel'))
const Login = lazy(() => import('@/pages/Login'))
const Unauthorized = lazy(() => import('@/pages/Unauthorized'))
export const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />,
},
{
path: 'about',
element: <About />,
},
{
path: 'login',
element: <Login />,
},
{
path: 'unauthorized',
element: <Unauthorized />,
},
{
path: 'dashboard',
element: (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
),
},
{
path: 'admin',
element: (
<ProtectedRoute requiredRoles={['admin']}>
<AdminPanel />
</ProtectedRoute>
),
},
],
},
])

Create a login page with authentication:

This is basic login page, you can use your own logic as well.

// src/pages/Login.tsx
import { useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/context/AuthContext'
import { loginSchema } from '@/utils/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import api from '@/api/axios'

export default function Login() {
const navigate = useNavigate()
const location = useLocation()
const { login } = useAuth()
const [error, setError] = useState('')

const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(loginSchema),
})

const onSubmit = async (data) => {
try {
const response = await api.post('/auth/login', data)
login(response.data.user)
const from = location.state?.from?.pathname || '/dashboard'
navigate(from, { replace: true })
} catch (err) {
setError('Invalid credentials')
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email</label>
<input {…register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<label>Password</label>
<input type="password" {…register('password')} />
{errors.password && <span>{errors.password.message}</span>}
</div>

{error && <div>{error}</div>}
<button type="submit">Login</button>
</form>
)
}

This implementation provides:

  1. Route protection based on authentication status
  2. Role-based access control

3. Persistent authentication state

4. Protected route redirection

5. Login form with validation

6. Secure token handling

Remember to:

- Store sensitive data (like tokens) securely

- Implement proper session management

- Add CSRF protection

- Use HTTPS in production

- Implement proper error handling

- Add rate limiting for login attempts

State Management with React Query

Set up React Query for server state management:

// src/providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 1,
},
},
})
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

API Setup with Axios

Create a base API configuration:

// src/api/axios.ts
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token') // or you can use cookies as well
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// Response interceptor
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
}
return Promise.reject(error)
}
)
export default api

Form Validation with Zod

Create reusable validation schemas:

// src/utils/validation.ts
import { z } from 'zod'
export const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
export type LoginFormData = z.infer<typeof loginSchema>

Main Application Setup

Finally, let’s set up the main application:

// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { QueryProvider } from '@/providers/QueryProvider'
import { AuthProvider } from '@/context/AuthContext'
import { router } from '@/router'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryProvider>
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</QueryProvider>
</React.StrictMode>
)

Best Practices and Tips

1. Code Splitting: Use React.lazy() and Suspense for route-based code splitting

2. Error Boundaries: Implement error boundaries at the route level

3. Performance Monitoring: Set up performance monitoring with tools like Sentry

4. Testing: Implement unit and integration tests using Jest and React Testing Library

5. CI/CD: Set up continuous integration and deployment pipelines

6. Security: Implement proper security measures including CSP headers and XSS protection

Conclusion

This setup provides a solid foundation for building enterprise-grade React applications. The combination of TypeScript, React Query, React Router, and other modern tools ensures a scalable, maintainable, and performant application.

Remember to:

- Keep your dependencies updated

- Follow the principle of least privilege

- Implement proper error handling

- Use TypeScript’s strict mode

- Follow React’s best practices for performance optimization

Happy coding!

--

--

Ritesh Singh
Ritesh Singh

Written by Ritesh Singh

Full Stack Developer & Tech Consultant.

No responses yet