Learn how to extend the boilerplate with new features.
src/features/
├── users/ # New feature
│ ├── components/
│ ├── context/
│ ├── hooks/
│ ├── services/
│ └── index.js
// src/features/users/services/userService.js
import api from '../../../services/api';
export const userService = {
getAll: () => api.get('/users'),
getById: (id) => api.get(`/users/${id}`),
create: (data) => api.post('/users', data),
update: (id, data) => api.put(`/users/${id}`, data),
delete: (id) => api.delete(`/users/${id}`),
search: (query) => api.get(`/users?q=${query}`),
};// src/features/users/context/UserContext.jsx
import { createContext, useContext, useState, useCallback } from 'react';
import { userService } from '../services/userService';
const UserContext = createContext(null);
export function UserProvider({ children }) {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
const response = await userService.getAll();
setUsers(response.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
const addUser = useCallback(async (userData) => {
setLoading(true);
try {
const response = await userService.create(userData);
setUsers(prev => [...prev, response.data]);
return response.data;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, []);
const removeUser = useCallback(async (id) => {
setLoading(true);
try {
await userService.delete(id);
setUsers(prev => prev.filter(u => u.id !== id));
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, []);
const value = { users, loading, error, fetchUsers, addUser, removeUser };
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
export const useUsersContext = () => useContext(UserContext);// src/features/users/hooks/useUsers.js
import { useEffect } from 'react';
import { useUsersContext } from '../context/UserContext';
export const useUsers = (autoFetch = true) => {
const { users, loading, error, fetchUsers, addUser, removeUser } = useUsersContext();
useEffect(() => {
if (autoFetch) {
fetchUsers();
}
}, [autoFetch, fetchUsers]);
return {
users,
loading,
error,
fetchUsers,
addUser,
removeUser,
};
};// src/features/users/components/UserList.jsx
import { useUsers } from '../hooks/useUsers';
import { UserCard } from './UserCard';
export function UserList() {
const { users, loading, error } = useUsers();
if (loading) return <Skeleton />;
if (error) return <ErrorMessage>{error}</ErrorMessage>;
return (
<div className="grid grid-cols-3 gap-4">
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}// src/main.jsx
import { UserProvider } from './features/users/context/UserContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<ProductProvider>
<UserProvider> {/* Add new providers here */}
<App />
</UserProvider>
</ProductProvider>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);Simply add a new method:
// src/features/products/services/productService.js
export const productService = {
getAll: () => api.get('/products'),
getById: (id) => api.get(`/products/${id}`),
create: (data) => api.post('/products', data),
update: (id, data) => api.put(`/products/${id}`, data),
delete: (id) => api.delete(`/products/${id}`),
// New endpoint
exportCSV: () => api.get('/products/export', { responseType: 'blob' }),
// New endpoint
bulkCreate: (data) => api.post('/products/bulk', data),
};Create a separate axios instance:
// src/services/paymentApi.js
import axios from 'axios';
const paymentApi = axios.create({
baseURL: import.meta.env.VITE_PAYMENT_API_URL,
timeout: 10000,
});
paymentApi.interceptors.response.use(
(response) => response,
(error) => {
// Handle payment-specific errors
return Promise.reject(error);
}
);
export default paymentApi;// src/pages/Users.jsx
import { lazy } from 'react';
function Users() {
return <div>Users Page</div>;
}
export default Users;// src/App.jsx
import { Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Users = lazy(() => import('./pages/Users'));
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="users" element={
<Suspense fallback={<Skeleton />}>
<Users />
</Suspense>
} />
</Route>
</Routes>
);
}// src/components/layout/Header.jsx
import { Link } from 'react-router-dom';
export function Header() {
return (
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/users">Users</Link>
</nav>
</header>
);
}// src/components/ui/ComponentName.jsx
import PropTypes from 'prop-types';
export function ComponentName({
children,
variant = 'default',
size = 'md',
className = '',
onClick,
disabled = false
}) {
const baseStyles = 'transition-colors';
const variants = {
default: 'bg-gray-100',
primary: 'bg-blue-500 text-white',
danger: 'bg-red-500 text-white',
};
const sizes = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={`
${baseStyles}
${variants[variant]}
${sizes[size]}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className}
`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
ComponentName.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(['default', 'primary', 'danger']),
size: PropTypes.oneOf(['sm', 'md', 'lg']),
className: PropTypes.string,
onClick: PropTypes.func,
disabled: PropTypes.bool,
};When adding a new feature:
- Create feature directory structure (including context/)
- Create service file
- Create Context with state and CRUD methods
- Create custom hook that uses context
- Create components
- Add page route
- Add provider to main.jsx
- Add navigation link
- Add tests