separation-of-concerns
Use when component does too many things. Use when mixing data fetching, logic, and presentation. Use when code is hard to test.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when component does too many things. Use when mixing data fetching, logic, and presentation. Use when code is hard to test.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when writing tests. Use when test structure is unclear. Use when arrange/act/assert phases are mixed.
Use when designing or modifying APIs. Use when adding breaking changes. Use when clients depend on API stability.
Use when implementing authentication. Use when storing passwords. Use when asked to store credentials insecurely.
Use when same data is fetched repeatedly. Use when database queries are slow. Use when implementing caching without invalidation strategy.
Use when tempted to use class inheritance. Use when creating class hierarchies. Use when subclass needs only some parent behavior.
Use when acquiring multiple locks. Use when operations wait for each other. Use when system hangs without crashing.
| name | separation-of-concerns |
| description | Use when component does too many things. Use when mixing data fetching, logic, and presentation. Use when code is hard to test. |
Each piece of code should do one thing. Data, logic, and presentation should be separate.
Mixed concerns create untestable, unreusable, unmaintainable code. Separation enables testing, reuse, and clarity.
NEVER mix data fetching, business logic, and presentation in one place.
No exceptions:
If one file does fetch + transform + display, STOP:
// ❌ VIOLATION: Component does everything
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Data fetching
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
// Business logic / transformation
const fullName = `${data.firstName} ${data.lastName}`;
const memberSince = new Date(data.createdAt).toLocaleDateString();
const isVIP = data.orderCount > 100;
setUser({ ...data, fullName, memberSince, isVIP });
setLoading(false);
});
}, [userId]);
// Presentation
if (loading) return <div>Loading...</div>;
return (
<div className="user-profile">
<h1>{user.fullName}</h1>
{user.isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {user.memberSince}</p>
</div>
);
}
Problems:
// ✅ CORRECT: Separated concerns
// 1. Data fetching (hook)
// hooks/useUser.ts
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
// 2. Business logic (pure functions)
// utils/userFormatters.ts
interface FormattedUser {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function formatUser(user: User): FormattedUser {
return {
fullName: `${user.firstName} ${user.lastName}`,
memberSince: new Date(user.createdAt).toLocaleDateString(),
isVIP: user.orderCount > 100,
};
}
// 3. Presentation (dumb component)
// components/UserCard.tsx
interface UserCardProps {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function UserCard({ fullName, memberSince, isVIP }: UserCardProps) {
return (
<div className="user-profile">
<h1>{fullName}</h1>
{isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {memberSince}</p>
</div>
);
}
// 4. Composition (container component)
// pages/UserProfile.tsx
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
const formatted = formatUser(user);
return <UserCard {...formatted} />;
}
| Mixed | Separated |
|---|---|
| Can't test formatting | formatUser() tested in isolation |
| Can't reuse fetch | useUser() reusable anywhere |
| Can't reuse UI | UserCard reusable with any data |
| 1 complex component | 4 simple pieces |
Pressure: "For simple cases, separation is overkill"
Response: Small becomes large. Start clean, stay clean.
Action: Separate even for small components. It costs little.
Pressure: "Everything in one place is easier to understand"
Response: Mixed concerns seem simple but are hard to test, debug, and modify.
Action: Separation is simpler in the long run.
Pressure: "This component is unique, won't be reused"
Response: Testability matters even for unique components.
Action: Separate for testability, not just reuse.
useEffect with fetch + transform + setStateAll of these mean: Separate the concerns.
| Concern | Where It Belongs |
|---|---|
| API calls | Hooks / Services |
| Data transformation | Pure functions |
| Business rules | Pure functions |
| UI rendering | Presentation components |
| Connecting pieces | Container components |
| Excuse | Reality |
|---|---|
| "Small component" | Small grows. Separate now. |
| "Simpler together" | Separated is simpler to test/modify. |
| "Only used once" | Testability matters. |
| "It works" | Working ≠ maintainable. |
| "Over-engineering" | This is just engineering. |
Data fetching in hooks. Logic in pure functions. UI in components.
Separation enables testing, reuse, and maintainability. A component should either fetch data, transform it, or display it - never all three.