This document explains why we made specific architectural choices.
We use feature-based structure:
src/features/
├── auth/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ └── slices/
└── products/
├── components/
├── hooks/
├── services/
└── slices/
Why feature-based?
| Feature-Based | Type-Based |
|---|---|
| Related code lives together | All components in one folder |
| Easy to find related files | Hard to navigate as app grows |
| Enables code splitting | Everything loads together |
| Scales well | Becomes messy |
When to use type-based:
- Very small apps (< 5 pages)
- Team prefers that structure
| Aspect | Context API | Redux Toolkit |
|---|---|---|
| Performance | Good for low-frequency updates | Optimized, memoized |
| DevTools | Limited | Time-travel debugging |
| Boilerplate | Minimal | Minimal (thanks to RTK) |
| Scalability | Good for small-medium apps | Excellent |
| Learning curve | Low | Medium |
Why Context API?
// AuthContext.jsx - Simple and effective
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(() => localStorage.getItem('token'));
const login = async (credentials) => {
const response = await authService.login(credentials);
setToken(response.data.accessToken);
setUser(response.data.user);
localStorage.setItem('token', response.data.accessToken);
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{ user, token, isAuthenticated: !!user, login, logout }}>
{children}
</AuthContext.Provider>
);
}When to use Context:
- Auth state (user, login, logout)
- Theme (light/dark mode)
- Locale/i18n
- Simple UI state (sidebar open)
- Feature-level state (products list)
Why not Redux?
- Redux adds unnecessary complexity for simple state management
- Context API is built into React (no extra dependencies)
- Easier to understand for beginners
- Suitable for the scale of bootcamp projects
Note: Redux Toolkit is still available in the boilerplate but is not used by default. Use it only if you have complex cross-component state that Context cannot handle efficiently.
| Aspect | Tailwind CSS | Styled Components |
|---|---|---|
| Learning curve | Medium | Low |
| Performance | Excellent (purged) | Good |
| No class conflicts | Yes | Yes |
| JS/CSS separation | No | Yes |
| IDE support | Good | Excellent |
Why Tailwind CSS?
-
Faster prototyping
// Tailwind <div className="p-4 bg-white rounded-lg shadow"> // Styled Components const Container = styled.div` padding: 1rem; background: white; border-radius: 0.5rem; box-shadow: ... `;
-
Smaller bundle - Unused styles removed
-
Easier maintenance - No file hopping
-
Consistency - Same spacing, colors
// services/api.js
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
// Request interceptor - add token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor - handle errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);Why this approach?
- Centralized - All API calls go through one instance
- Token management - Automatically added to every request
- Error handling - Global error handling
- Easy testing - Can mock the api instance
// context/AuthContext.jsx
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(() => localStorage.getItem('token'));
const login = async (credentials) => {
const response = await authService.login(credentials);
setToken(response.data.accessToken);
setUser(response.data.user);
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{ user, token, isAuthenticated: !!token, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// hooks/useAuthContext.js
export const useAuthContext = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuthContext must be used within an AuthProvider');
}
return context;
};
// hooks/useAuth.js (wrapper for convenience)
import { useAuthContext } from '../context/AuthContext';
export const useAuth = () => {
return useAuthContext();
};Why this pattern?
- Reusable logic
- Separation of concerns
- Easy to test
- Cleaner components
- Native React (no extra dependencies)
// context/ProductContext.jsx
export function ProductProvider({ children }) {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const fetchProducts = async () => {
setLoading(true);
try {
const response = await productService.getAll();
setProducts(response.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const addProduct = async (data) => {
const response = await productService.create(data);
setProducts(prev => [...prev, response.data]);
return response.data;
};
return (
<ProductContext.Provider value={{ products, loading, fetchProducts, addProduct }}>
{children}
</ProductContext.Provider>
);
}Why this pattern?
- State lives close to where it's used
- No global store to manage
- Simple data flow
- Easy to understand
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | LoginForm.jsx |
| Hooks | camelCase + use prefix | useAuth.js |
| Services | camelCase + Service suffix | authService.js |
| Slices | camelCase + Slice suffix | authSlice.js |
| Utils | camelCase | validators.js |
| Constants | SCREAMING_SNAKE | constants.js |
// components/ui/Button.jsx
import PropTypes from 'prop-types';
export function Button({
children,
variant = 'primary',
onClick,
disabled
}) {
const baseStyles = 'px-4 py-2 rounded font-medium transition';
const variants = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
};
return (
<button
className={`${baseStyles} ${variants[variant]}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(['primary', 'secondary']),
onClick: PropTypes.func,
disabled: PropTypes.bool,
};Why this structure?
- Props documentation with PropTypes
- Reusable style variants
- Clear prop naming
- Type-safe (PropTypes)
| Decision | Rationale |
|---|---|
| Feature-based structure | Scalability, code splitting |
| Context API | Native React, simplicity, no extra dependencies |
| Tailwind CSS | Speed, consistency, bundle size |
| Axios interceptors | Centralized auth/error handling |
| Custom hooks | Reusability, clean code |
| AI Integration | Context-based for easy state management |