These guidelines help create frontend code that is easy to change by following four key criteria: Readability, Predictability, Cohesion, and Coupling.
Easy-to-change code means:
- New requirements can be implemented by modifying existing code smoothly
- Code intent and behavior are clear and understandable
- The scope of impact from changes is predictable
- Readability - Code should be easy to understand
- Predictability - Consistent patterns and behaviors
- Cohesion - Code that changes together stays together
- Coupling - Minimize dependencies between modules
Code that is easy to understand at first glance. Readable code minimizes cognitive context and flows naturally from top to bottom.
// ❌ Bad: Mixed conditional logic
function SubmitButton() {
const isViewer = useRole() === "viewer";
useEffect(() => {
if (!isViewer) {
showButtonAnimation();
}
}, [isViewer]);
return isViewer ?
<TextButton disabled>Submit</TextButton> :
<Button type="submit">Submit</Button>;
}
// ✅ Good: Separated by role
function SubmitButton() {
const isViewer = useRole() === "viewer";
return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
}
function ViewerSubmitButton() {
return <TextButton disabled>Submit</TextButton>;
}
function AdminSubmitButton() {
useEffect(() => {
showButtonAnimation();
}, []);
return <Button type="submit">Submit</Button>;
}// ❌ Bad: Low-level details exposed
async function LoginStartPage() {
const handleLogin = async () => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
if (response.ok) {
localStorage.setItem('token', response.token);
window.location.href = '/dashboard';
}
};
}
// ✅ Good: Abstracted implementation
async function LoginStartPage() {
const handleLogin = async () => {
const success = await attemptLogin(username, password);
if (success) {
navigateToDashboard();
}
};
}// ❌ Bad: Mixed logic types
function usePageState() {
// Data fetching logic
const fetchData = async () => { ... };
// UI state management
const toggleModal = () => { ... };
// Form validation
const validateForm = () => { ... };
return { fetchData, toggleModal, validateForm };
}
// ✅ Good: Separated by concern
function usePageData() {
const fetchData = async () => { ... };
return { fetchData };
}
function usePageUI() {
const toggleModal = () => { ... };
return { toggleModal };
}
function usePageForm() {
const validateForm = () => { ... };
return { validateForm };
}// ❌ Bad: Unclear condition
const result = products.filter(product =>
product.categories.some(category =>
category.id === targetCategory.id &&
product.prices.some(price => price >= minPrice && price <= maxPrice)
)
);
// ✅ Good: Named conditions
const matchedProducts = products.filter(product => {
return product.categories.some(category => {
const isSameCategory = category.id === targetCategory.id;
const isPriceInRange = product.prices.some(
price => price >= minPrice && price <= maxPrice
);
return isSameCategory && isPriceInRange;
});
});// ❌ Bad: Unclear number meaning
async function onLikeClick() {
await postLike(url);
await delay(300);
await refetchPostLike();
}
// ✅ Good: Named constant
const ANIMATION_DELAY_MS = 300;
async function onLikeClick() {
await postLike(url);
await delay(ANIMATION_DELAY_MS);
await refetchPostLike();
}// ❌ Bad: Jumping between different times
function UserPolicy() {
const policy = fetchPolicy(); // Future: async
if (!user) return null; // Present: check
useEffect(() => { // Future: effect
trackView();
}, []);
return <div>{policy?.content}</div>; // Present: render
}
// ✅ Good: Consistent timeline
function UserPolicy() {
// All present checks first
if (!user) return null;
// All data fetching
const policy = fetchPolicy();
// All effects
useEffect(() => {
trackView();
}, []);
// Final render
return <div>{policy?.content}</div>;
}// ❌ Bad: Complex nested ternary
const message = isLoading ? "Loading..." :
hasError ? "Error occurred" :
data ? `Found ${data.length} items` :
"No data";
// ✅ Good: Clear conditions
function getMessage() {
if (isLoading) return "Loading...";
if (hasError) return "Error occurred";
if (data) return `Found ${data.length} items`;
return "No data";
}
const message = getMessage();Code should behave as expected based on function names, parameters, and return types.
// ❌ Bad: Confusing names
import { Button } from './components/Button';
import { Button as BaseButton } from 'library';
// ✅ Good: Clear distinctions
import { AppButton } from './components/AppButton';
import { Button } from 'library';// ❌ Bad: Inconsistent returns
function useUser() {
const query = useQuery({ queryKey: ["user"], queryFn: fetchUser });
return query; // Returns query object
}
function useServerTime() {
const query = useQuery({ queryKey: ["serverTime"], queryFn: fetchServerTime });
return query.data; // Returns only data!
}
// ✅ Good: Consistent pattern
function useUser() {
const query = useQuery({ queryKey: ["user"], queryFn: fetchUser });
return query;
}
function useServerTime() {
const query = useQuery({ queryKey: ["serverTime"], queryFn: fetchServerTime });
return query; // Same return pattern
}2.3 Reveal Hidden Logic
// ❌ Bad: Hidden side effect
async function fetchBalance() {
const balance = await http.get("/balance");
logging.log("balance_fetched"); // Hidden!
return balance;
}
// ✅ Good: Explicit behavior
async function fetchBalance() {
const balance = await http.get("/balance");
return balance;
}
// At usage site
const balance = await fetchBalance();
logging.log("balance_fetched"); // Visible at call siteCode that changes together should live together.
// ❌ Bad: Organized by file type
src/
├── components/
├── hooks/
├── utils/
└── constants/
// ✅ Good: Organized by domain/feature
src/
├── shared/ # Used across features
│ ├── components/
│ └── hooks/
└── features/
├── auth/ # All auth-related code
│ ├── components/
│ ├── hooks/
│ └── utils/
└── products/ # All product-related code
├── components/
├── hooks/
└── utils/
// ❌ Bad: Same value in multiple places
// In animation.js
fadeIn(300);
// In transition.js
slideOut(300);
// In delay.js
wait(300);
// ✅ Good: Single source of truth
// In constants.js
export const ANIMATION_DURATION_MS = 300;
// In all files
import { ANIMATION_DURATION_MS } from './constants';
fadeIn(ANIMATION_DURATION_MS);
slideOut(ANIMATION_DURATION_MS);
wait(ANIMATION_DURATION_MS);Use when fields have independent validation and can be reused separately.
// Each field manages its own state and validation
function EmailField({ value, onChange, error }) {
const validate = (email) => {
if (!email) return "Email required";
if (!email.includes('@')) return "Invalid email";
return "";
};
return (
<input
value={value}
onChange={(e) => onChange(e.target.value, validate(e.target.value))}
/>
);
}Use when fields depend on each other or share validation logic.
// Centralized form management
function useFormValidation(values) {
const errors = {};
if (values.password !== values.confirmPassword) {
errors.confirmPassword = "Passwords must match";
}
if (values.endDate < values.startDate) {
errors.endDate = "End date must be after start date";
}
return errors;
}Minimize dependencies between modules to reduce change impact.
// ❌ Bad: Multiple responsibilities
function usePageState() {
// User management
const [user, setUser] = useState();
const fetchUser = () => { ... };
// Posts management
const [posts, setPosts] = useState();
const fetchPosts = () => { ... };
// UI state
const [isModalOpen, setIsModalOpen] = useState();
return { user, posts, isModalOpen, ... };
}
// ✅ Good: Separated concerns
function useUser() {
const [user, setUser] = useState();
const fetchUser = () => { ... };
return { user, fetchUser };
}
function usePosts() {
const [posts, setPosts] = useState();
const fetchPosts = () => { ... };
return { posts, fetchPosts };
}
function useModal() {
const [isOpen, setIsOpen] = useState(false);
return { isOpen, setIsOpen };
}// ❌ Bad: Forced abstraction creating coupling
function useBottomSheet(type) {
// Complex shared logic trying to handle all cases
if (type === 'product') { ... }
else if (type === 'user') { ... }
else if (type === 'order') { ... }
}
// ✅ Good: Independent implementations
function useProductSheet() {
// Product-specific logic
}
function useUserSheet() {
// User-specific logic
}
// Some duplication is better than wrong abstraction// ❌ Bad: Props drilling
function ItemEditModal({ items, recommendedItems, onConfirm }) {
return (
<Modal>
<ItemEditBody
items={items}
recommendedItems={recommendedItems}
onConfirm={onConfirm}
/>
</Modal>
);
}
// ✅ Good: Composition pattern
function ItemEditModal({ onConfirm }) {
return (
<Modal>
<ItemEditBody>
<ItemEditList onConfirm={onConfirm} />
</ItemEditBody>
</Modal>
);
}// Only when composition isn't enough
const ItemContext = createContext();
function ItemProvider({ children, items }) {
return (
<ItemContext.Provider value={items}>
{children}
</ItemContext.Provider>
);
}
function DeepChildComponent() {
const items = useContext(ItemContext);
// Can access items without props drilling
}When reviewing or writing code, verify:
- Functions have single, clear purposes
- Complex conditions have descriptive names
- Magic numbers are replaced with named constants
- Code flows logically from top to bottom
- Implementation details are properly abstracted
- Similar functions have consistent return types
- Hidden side effects are made explicit
- Names clearly indicate function behavior
- No surprising behaviors in functions
- Related files are in the same directory
- Shared constants are defined once
- Form validation matches form structure
- Changes require modifying files in one place
- Components have single responsibilities
- Props drilling doesn't exceed 2-3 levels
- Duplication is allowed when it reduces coupling
- Dependencies between modules are minimized
When generating frontend code:
- Start with the simplest solution that meets requirements
- Extract abstractions only when patterns repeat 3+ times
- Prefer composition over complex prop passing
- Keep functions small - under 50 lines ideally
- Name things based on what they do, not how they do it
- Phase 1: Write working code that solves the problem
- Phase 2: Apply readability improvements
- Phase 3: Extract common patterns if found
- Phase 4: Optimize performance if needed
// Use type inference where possible
const [count, setCount] = useState(0); // Type inferred
// Be explicit for function parameters
function calculate(a: number, b: number): number {
return a + b;
}
// Use unions for finite states
type Status = 'idle' | 'loading' | 'success' | 'error';Remember these principles can conflict:
- Readability vs. Cohesion: Sometimes duplication is clearer than abstraction
- Predictability vs. Flexibility: Consistent patterns may limit flexibility
- Cohesion vs. Coupling: Grouping code together can increase dependencies
Decision Framework:
- What changes together? → Increase cohesion
- What changes separately? → Reduce coupling
- Who will maintain this? → Prioritize readability
- How often will this change? → Consider all factors
Consider refactoring when:
- A file exceeds 200 lines
- A function exceeds 50 lines
- Props are passed through 3+ components unchanged
- The same code appears in 3+ places
- A component has 3+ separate responsibilities
- Nested conditionals exceed 3 levels deep