📅 NGÀY 40: Component Patterns - Part 2 (Legacy Patterns)
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu Render Props pattern và tại sao nó từng phổ biến
- [ ] Hiểu Higher-Order Components (HOCs) pattern và use cases
- [ ] So sánh được legacy patterns với modern Hooks approach
- [ ] Biết cách migrate code từ Render Props/HOCs sang Hooks
- [ ] Nhận biết khi nào đọc code legacy và refactor an toàn
🤔 Kiểm tra đầu vào (5 phút)
- Custom Hooks (Ngày 24): Làm sao share logic giữa components bằng Hooks?
- Compound Components (Ngày 39): Làm sao share state implicit giữa parent/children?
- Context API (Ngày 36-38): Tại sao Context tốt hơn props drilling?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế - Lịch Sử React Patterns
Timeline React Patterns:
2013-2015: Class Components + Mixins
├─ Problem: Mixins cause name collisions
├─ Solution needed: Better composition
2016-2018: Higher-Order Components (HOCs)
├─ Pattern: Wrap components to add behavior
├─ Problem: Wrapper hell, props collision
├─ Still better than Mixins
2017-2018: Render Props
├─ Pattern: Function as child
├─ Problem: Callback hell, hard to read
├─ Better flexibility than HOCs
2019-Present: Hooks 🎉
├─ Pattern: Reusable stateful logic
├─ Solution: Cleaner, more composable
└─ Current standardTình huống thực tế:
// Bạn join công ty, đọc codebase cũ:
// ❌ Code style 2017 - Render Props
<MouseTracker>
{({ x, y }) => (
<DataFetcher url='/api/users'>
{({ data, loading }) => (
<Theme>
{({ theme }) => (
<div>
Mouse: {x}, {y}
Data: {loading ? 'Loading...' : data}
Theme: {theme}
</div>
)}
</Theme>
)}
</DataFetcher>
)}
</MouseTracker>;
// ❌ Code style 2018 - HOCs
const Enhanced = withMouse(withData(withTheme(Component)));
// ✅ Code style 2024 - Hooks
function Component() {
const { x, y } = useMouse();
const { data, loading } = useFetch('/api/users');
const { theme } = useTheme();
return (
<div>
Mouse: {x}, {y}
Data: {loading ? 'Loading...' : data}
Theme: {theme}
</div>
);
}Mission của bài học hôm nay:
- Hiểu legacy patterns để đọc code cũ
- Biết refactor an toàn sang Hooks
- Không bao giờ dùng trong code mới (trừ edge cases rất hiếm)
1.2 Render Props Pattern
Định nghĩa: Component nhận một function (thường là children hoặc render prop) và gọi function đó với data.
Cấu trúc:
// Pattern cơ bản
<Component>
{(data) => <UI>{data}</UI>}
</Component>
// Hoặc
<Component render={(data) => <UI>{data}</UI>} />Tại sao pattern này tồn tại?
- Trước Hooks, không có cách nào share stateful logic
- Cần share state/behavior mà không dùng inheritance
- Flexible hơn HOCs (control render từ bên ngoài)
1.3 Higher-Order Components (HOCs)
Định nghĩa: Function nhận component, return component mới với enhanced behavior.
Cấu trúc:
// HOC là function
function withSomething(Component) {
return function EnhancedComponent(props) {
// Add behavior here
return (
<Component
{...props}
extraProp={value}
/>
);
};
}
// Usage
const Enhanced = withSomething(MyComponent);Tại sao pattern này tồn tại?
- Reuse component logic
- Add behaviors (logging, auth, data fetching)
- Composition (kết hợp nhiều HOCs)
1.4 Mental Model
EVOLUTION OF SHARING LOGIC IN REACT:
2013: Mixins
class MyComponent extends React.Component {
mixins: [MouseMixin, DataMixin] ❌ Deprecated
}
2016: HOCs
const Enhanced = withMouse(withData(Component))
⚠️ Works, but wrapper hell
2017: Render Props
<Mouse>
{mouse => <Data>{data => <Component />}</Data>}
</Mouse>
⚠️ Works, but callback hell
2019: Hooks
function Component() {
const mouse = useMouse();
const data = useData();
}
✅ Clean, composable, modern
Analogy:
- Mixins = Chung nồi (conflicts)
- HOCs = Matryoshka dolls (wrapper hell)
- Render Props = Nested callbacks (pyramid of doom)
- Hooks = Lego blocks (compose freely) ✅1.5 Hiểu Lầm Phổ Biến
❌ "Render Props và HOCs đã deprecated" → Không! Chúng vẫn hoạt động, nhưng Hooks là better approach.
❌ "Không bao giờ được dùng Render Props/HOCs" → Không hẳn. Một số edge cases vẫn OK (rất hiếm), nhưng default là Hooks.
❌ "HOCs giống Hooks" → Khác! HOCs wrap components, Hooks compose logic.
❌ "Phải refactor tất cả code cũ ngay" → Không! Refactor từ từ, ưu tiên code hay thay đổi.
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Render Props Pattern ⭐
Mouse Tracker với Render Props (Legacy way)
💡 Code Example
/**
* Mouse Tracker - Render Props Pattern (LEGACY)
*
* ⚠️ Đây là legacy pattern - KHÔNG khuyến khích dùng
* Mục đích: Hiểu để đọc code cũ
*
* @example
* <Mouse>
* {({ x, y }) => <div>Position: {x}, {y}</div>}
* </Mouse>
*/
import { useState, useEffect } from 'react';
// ❌ LEGACY: Render Props Component
function Mouse({ children, render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// Support cả 2 patterns: children function hoặc render prop
return children ? children(position) : render(position);
}
// Usage - Callback hell khi combine nhiều
function AppLegacy() {
return (
<div>
<h2>Legacy: Render Props</h2>
{/* Pattern 1: Children as function */}
<Mouse>
{({ x, y }) => (
<div style={{ padding: '20px', border: '1px solid blue' }}>
<h3>Mouse Position (Children)</h3>
<p>
X: {x}, Y: {y}
</p>
</div>
)}
</Mouse>
{/* Pattern 2: Render prop */}
<Mouse
render={({ x, y }) => (
<div style={{ padding: '20px', border: '1px solid green' }}>
<h3>Mouse Position (Render Prop)</h3>
<p>
X: {x}, Y: {y}
</p>
</div>
)}
/>
{/* ❌ Problem: Callback hell khi nested */}
<Mouse>
{({ x, y }) => (
<WindowSize>
{({ width, height }) => (
<div style={{ padding: '20px', border: '1px solid red' }}>
<p>
Mouse: {x}, {y}
</p>
<p>
Window: {width}x{height}
</p>
{/* Tưởng tượng 5-10 layers như này! */}
</div>
)}
</WindowSize>
)}
</Mouse>
</div>
);
}
// Helper component for demo
function WindowSize({ children }) {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return children(size);
}
// ✅ MODERN: Hooks approach (so sánh)
function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
function AppModern() {
const { x, y } = useMouse();
const { width, height } = useWindowSize();
return (
<div>
<h2>Modern: Hooks</h2>
<div style={{ padding: '20px', border: '1px solid blue' }}>
<p>
Mouse: {x}, {y}
</p>
<p>
Window: {width}x{height}
</p>
{/* ✅ Flat, readable, no nesting! */}
</div>
</div>
);
}
// Compare side by side
function App() {
return (
<div style={{ padding: '20px' }}>
<AppLegacy />
<hr style={{ margin: '40px 0' }} />
<AppModern />
</div>
);
}
/*
So sánh:
Render Props:
❌ Callback hell khi combine nhiều
❌ Khó đọc (nested deeply)
❌ Khó debug
❌ Performance issues (tạo function mới mỗi render)
Hooks:
✅ Flat structure
✅ Dễ đọc
✅ Dễ compose
✅ Dễ test
✅ Better performance
*/Demo 2: Higher-Order Components (HOCs) ⭐⭐
withAuth HOC (Legacy pattern)
💡 Code Example
/**
* Authentication HOC - Higher-Order Component Pattern (LEGACY)
*
* ⚠️ Legacy pattern - KHÔNG khuyến khích dùng
* Mục đích: Hiểu để đọc code cũ
*
* @example
* const ProtectedPage = withAuth(Dashboard);
*/
import { useState } from 'react';
// ❌ LEGACY: HOC Pattern
function withAuth(Component) {
// Return new component
return function AuthenticatedComponent(props) {
const [isAuthenticated] = useState(false); // Giả lập auth state
if (!isAuthenticated) {
return (
<div style={{ padding: '20px', border: '2px solid red' }}>
<h3>Access Denied</h3>
<p>Please login to view this page</p>
</div>
);
}
// Pass through all props
return <Component {...props} />;
};
}
// Another HOC for loading
function withLoading(Component) {
return function LoadingComponent(props) {
const [isLoading] = useState(false); // Giả lập loading
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
};
}
// Another HOC for logging
function withLogger(Component) {
return function LoggedComponent(props) {
console.log('Component rendered with props:', props);
return <Component {...props} />;
};
}
// Base component
function Dashboard({ title, user }) {
return (
<div style={{ padding: '20px', border: '1px solid green' }}>
<h2>{title}</h2>
<p>Welcome, {user}!</p>
</div>
);
}
// ❌ Problem: Wrapper hell
const EnhancedDashboard = withAuth(withLoading(withLogger(Dashboard)));
// ❌ Problem: Props collision
function withUser(Component) {
return function UserComponent(props) {
const user = { name: 'John from HOC' };
// ⚠️ Nếu props cũng có 'user', sẽ bị override!
return (
<Component
{...props}
user={user}
/>
);
};
}
// ❌ Problem: Static composition (không dynamic)
// Phải decide HOCs khi define component, không thể conditional
// Usage
function AppLegacy() {
return (
<div>
<h2>Legacy: HOCs</h2>
<EnhancedDashboard
title='Dashboard'
user='Alice'
/>
{/* Component tree:
withAuth(
withLoading(
withLogger(
Dashboard
)
)
)
→ 3 wrapper components!
→ Hard to debug in DevTools
*/}
</div>
);
}
// ✅ MODERN: Hooks approach
function useAuth() {
const [isAuthenticated] = useState(false);
return { isAuthenticated };
}
function useLoading() {
const [isLoading] = useState(false);
return { isLoading };
}
function useLogger(componentName, props) {
console.log(`${componentName} rendered with:`, props);
}
function DashboardModern({ title, user }) {
const { isAuthenticated } = useAuth();
const { isLoading } = useLoading();
useLogger('Dashboard', { title, user });
if (!isAuthenticated) {
return (
<div style={{ padding: '20px', border: '2px solid red' }}>
<h3>Access Denied</h3>
<p>Please login to view this page</p>
</div>
);
}
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div style={{ padding: '20px', border: '1px solid green' }}>
<h2>{title}</h2>
<p>Welcome, {user}!</p>
</div>
);
}
function AppModern() {
return (
<div>
<h2>Modern: Hooks</h2>
<DashboardModern
title='Dashboard'
user='Alice'
/>
{/* Component tree:
Just DashboardModern
✅ Flat structure
✅ Easy to debug
✅ No wrapper hell
*/}
</div>
);
}
// Compare
function App() {
return (
<div style={{ padding: '20px' }}>
<AppLegacy />
<hr style={{ margin: '40px 0' }} />
<AppModern />
</div>
);
}
/*
So sánh:
HOCs:
❌ Wrapper hell (nhiều layers)
❌ Props collision risk
❌ Hard to debug (DevTools shows wrappers)
❌ Static composition
❌ Naming conflicts
Hooks:
✅ No wrappers
✅ No props collision
✅ Easy to debug
✅ Dynamic composition
✅ Clear dependencies
*/Demo 3: Migration Strategy ⭐⭐⭐
Refactor từ Legacy sang Hooks
💡 Code Example
/**
* Migration Guide: Legacy Patterns → Modern Hooks
*
* Step-by-step refactoring examples
*/
import { useState, useEffect } from 'react';
// =====================================
// EXAMPLE 1: Render Props → Hook
// =====================================
// ❌ BEFORE: Render Props
function DataFetcherLegacy({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, [url]);
return children({ data, loading, error });
}
// Usage
function UserListLegacy() {
return (
<DataFetcherLegacy url='/api/users'>
{({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Data: {JSON.stringify(data)}</div>;
}}
</DataFetcherLegacy>
);
}
// ✅ AFTER: Custom Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// Usage - Much cleaner!
function UserListModern() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Data: {JSON.stringify(data)}</div>;
}
// =====================================
// EXAMPLE 2: HOC → Hook
// =====================================
// ❌ BEFORE: HOC
function withTheme(Component) {
return function ThemedComponent(props) {
const [theme] = useState('dark');
return (
<Component
{...props}
theme={theme}
/>
);
};
}
function ButtonLegacy({ theme, children }) {
return (
<button
style={{
background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#000',
}}
>
{children}
</button>
);
}
const ThemedButton = withTheme(ButtonLegacy);
// ✅ AFTER: Hook
function useTheme() {
const [theme] = useState('dark');
return { theme };
}
function ButtonModern({ children }) {
const { theme } = useTheme();
return (
<button
style={{
background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#000',
}}
>
{children}
</button>
);
}
// =====================================
// EXAMPLE 3: Multiple HOCs → Multiple Hooks
// =====================================
// ❌ BEFORE: Multiple HOCs
function withAuth(Component) {
return function (props) {
const isAuth = true;
if (!isAuth) return <div>Not authenticated</div>;
return (
<Component
{...props}
isAuth={isAuth}
/>
);
};
}
function withUser(Component) {
return function (props) {
const user = { name: 'John' };
return (
<Component
{...props}
user={user}
/>
);
};
}
function withPermissions(Component) {
return function (props) {
const canEdit = true;
return (
<Component
{...props}
canEdit={canEdit}
/>
);
};
}
// Wrapper hell!
const ProfileLegacy = withAuth(
withUser(
withPermissions(function ({ user, canEdit }) {
return (
<div>
<h2>{user.name}</h2>
{canEdit && <button>Edit</button>}
</div>
);
}),
),
);
// ✅ AFTER: Compose Hooks
function useAuth() {
const isAuth = true;
return { isAuth };
}
function useUser() {
const user = { name: 'John' };
return { user };
}
function usePermissions() {
const canEdit = true;
return { canEdit };
}
function ProfileModern() {
const { isAuth } = useAuth();
const { user } = useUser();
const { canEdit } = usePermissions();
if (!isAuth) return <div>Not authenticated</div>;
return (
<div>
<h2>{user.name}</h2>
{canEdit && <button>Edit</button>}
</div>
);
}
// =====================================
// MIGRATION CHECKLIST
// =====================================
/*
STEP 1: Identify pattern
- Render Props? → Extract to custom hook
- HOC? → Extract to custom hook
- Multiple HOCs? → Multiple hooks
STEP 2: Extract logic
- Move state management to hook
- Move effects to hook
- Return values needed by component
STEP 3: Update component
- Remove render prop callback
- Remove HOC wrapper
- Use hook directly
STEP 4: Test
- Same behavior?
- Same performance?
- Same props API (if public component)?
STEP 5: Clean up
- Remove old code
- Update tests
- Update documentation
*/
// Demo comparison
function App() {
return (
<div style={{ padding: '20px' }}>
<h1>Migration Examples</h1>
<section>
<h2>Example 1: Data Fetching</h2>
<h3>Legacy (Render Props):</h3>
<UserListLegacy />
<h3>Modern (Hooks):</h3>
<UserListModern />
</section>
<hr style={{ margin: '40px 0' }} />
<section>
<h2>Example 2: Theme</h2>
<h3>Legacy (HOC):</h3>
<ThemedButton>Click me (Legacy)</ThemedButton>
<h3>Modern (Hook):</h3>
<ButtonModern>Click me (Modern)</ButtonModern>
</section>
<hr style={{ margin: '40px 0' }} />
<section>
<h2>Example 3: Multiple Concerns</h2>
<h3>Legacy (Multiple HOCs):</h3>
<ProfileLegacy />
<h3>Modern (Multiple Hooks):</h3>
<ProfileModern />
</section>
</div>
);
}
/*
Key Takeaways:
1. Render Props → Custom Hook
- Extract logic to hook
- Return values instead of calling children
- No more callback nesting
2. HOC → Custom Hook
- Extract logic to hook
- No more wrapper component
- No props collision
3. Multiple HOCs → Multiple Hooks
- Each HOC becomes a hook
- Compose hooks in component
- Flat, readable structure
4. When NOT to migrate immediately:
- Code is stable and not changing
- Team unfamiliar with Hooks
- Third-party library uses old pattern
- Migration would break public API
5. When TO migrate:
- Adding new features
- Fixing bugs in that area
- Code is hard to maintain
- Team wants to modernize
*/🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Bài 1: Nhận Biết Pattern (15 phút)
🎯 Mục tiêu: Phân biệt patterns và identify refactor opportunities
/**
* 🎯 Mục tiêu: Đọc code và nhận biết pattern được dùng
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: Class components
*
* Requirements:
* Cho mỗi đoạn code sau:
* 1. Identify pattern (Render Props / HOC / Hooks)
* 2. List pros/cons
* 3. Nên refactor không? Tại sao?
*/
// Code Snippet 1
function App() {
return (
<MouseTracker>
{(position) => (
<div>
Mouse at: {position.x}, {position.y}
</div>
)}
</MouseTracker>
);
}
// Code Snippet 2
const EnhancedComponent = withLogging(withAuth(MyComponent));
// Code Snippet 3
function App() {
const position = useMouse();
const { user } = useAuth();
return (
<div>
User: {user.name}, Mouse: {position.x}
</div>
);
}
// Code Snippet 4
function Toggle({ children }) {
const [isOn, setIsOn] = useState(false);
return (
<>
{children({
isOn,
toggle: () => setIsOn(!isOn),
})}
</>
);
}
// 🎯 NHIỆM VỤ: Phân tích từng snippet💡 Solution
/**
* Pattern Recognition & Analysis
*/
// =====================================
// SNIPPET 1: Render Props
// =====================================
/*
Pattern: Render Props (Children as function)
Pros:
- Flexible - control render từ outside
- Share state dễ dàng
Cons:
- Callback nesting
- Khó đọc khi nhiều levels
- Performance (tạo function mới mỗi render)
Refactor?
✅ YES - Should refactor to Hook
Reason:
- Single concern (mouse tracking)
- Easy to extract to useMouse()
- Better readability
- Better performance
Refactored:function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return position;
}
function App() {
const position = useMouse();
return (
<div>
Mouse at: {position.x}, {position.y}
</div>
);
}*/
// =====================================
// SNIPPET 2: HOC (Higher-Order Component)
// =====================================
/*
Pattern: Multiple HOCs (Wrapper composition)
Pros:
- Reuse logic across components
- Separation of concerns
Cons:
- Wrapper hell (2 levels here)
- Props collision risk
- Hard to debug (DevTools shows wrappers)
- Static composition
Refactor?
✅ YES - Definitely should refactor
Reason:
- Wrapper hell (multiple HOCs)
- Each HOC can be a hook
- Much cleaner with hooks
Refactored:function useLogging(componentName) {
useEffect(() => {
console.log(`${componentName} mounted`);
return () => console.log(`${componentName} unmounted`);
}, [componentName]);
}
function useAuth() {
const [user, setUser] = useState(null);
// auth logic
return { user, isAuthenticated: !!user };
}
function MyComponent() {
useLogging('MyComponent');
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) return <div>Please login</div>;
return <div>Welcome, {user.name}</div>;
}*/
// =====================================
// SNIPPET 3: Hooks (Modern)
// =====================================
/*
Pattern: Custom Hooks
Pros:
✅ Clean, flat structure
✅ Easy to read
✅ Easy to test
✅ Composable
✅ No wrappers
Cons:
(None - this is the modern way!)
Refactor?
❌ NO - Already modern pattern
Reason:
- Using hooks correctly
- Clean composition
- Best practice
Keep as is! ✅
*/
// =====================================
// SNIPPET 4: Render Props (Toggle)
// =====================================
/*
Pattern: Render Props
Pros:
- Flexible control
- Works (not broken)
Cons:
- Callback syntax
- Overkill for simple toggle
Refactor?
⚠️ MAYBE - Depends on context
Reason to refactor:
- If used in many places → useToggle hook
- Cleaner syntax
Reason NOT to refactor:
- If only used once → overhead of creating hook
- Team preference for render props
- Part of public API (breaking change)
Option 1: Keep as is (if rarely used)
Option 2: Refactor to hook (if common)function useToggle(initialValue = false) {
const [isOn, setIsOn] = useState(initialValue);
const toggle = useCallback(() => setIsOn((prev) => !prev), []);
return [isOn, toggle];
}
// Usage
function App() {
const [isOn, toggle] = useToggle();
return (
<div>
<p>{isOn ? 'ON' : 'OFF'}</p>
<button onClick={toggle}>Toggle</button>
</div>
);
}
Option 3: Compound Components (if complex UI)<Toggle>
<Toggle.Button />
<Toggle.Content>Content here</Toggle.Content>
</Toggle>*/
/*
SUMMARY DECISION MATRIX:
Snippet 1: Render Props → ✅ Refactor to useMouse()
Snippet 2: Multiple HOCs → ✅ Refactor to hooks
Snippet 3: Hooks → ❌ Keep (already modern)
Snippet 4: Render Props → ⚠️ Maybe (depends on usage)
General Rules:
1. Single concern + stateful → Custom Hook
2. Multiple HOCs → Multiple Hooks
3. Render Props with nesting → Hook
4. Render Props (simple, once) → Maybe keep
5. Already Hooks → Keep
Factors to consider:
- How often used? (1x vs 10x)
- Team familiarity?
- Breaking changes?
- Time available?
- Code churn risk?
*/⭐⭐ Bài 2: Refactor Render Props (25 phút)
🎯 Mục tiêu: Thực hành refactor từ Render Props sang Hook
/**
* 🎯 Mục tiêu: Refactor component từ Render Props sang Hook
* ⏱️ Thời gian: 25 phút
*
* Scenario: Codebase có FormValidation component dùng Render Props
* Task: Refactor thành useFormValidation hook
*
* Requirements:
* 1. Extract logic vào custom hook
* 2. Maintain same functionality
* 3. Improve readability
* 4. Keep backwards compatibility (bonus)
*/
// ❌ LEGACY: Render Props
function FormValidation({ initialValues, validate, children }) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));
// Validate on change
const fieldErrors = validate({ ...values, [name]: value });
setErrors(fieldErrors);
};
const handleBlur = (name) => {
setTouched((prev) => ({ ...prev, [name]: true }));
};
const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
// Mark all as touched
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {});
setTouched(allTouched);
// Validate
const formErrors = validate(values);
setErrors(formErrors);
// Submit if no errors
if (Object.keys(formErrors).length === 0) {
onSubmit(values);
}
};
return children({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
});
}
// Usage (ví dụ)
function LoginForm() {
const validate = (values) => {
const errors = {};
if (!values.email) errors.email = 'Required';
if (!values.password) errors.password = 'Required';
return errors;
};
return (
<FormValidation
initialValues={{ email: '', password: '' }}
validate={validate}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
}) => (
<form onSubmit={handleSubmit(() => console.log('Submitted!'))}>
<input
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
<input
type='password'
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
/>
{touched.password && errors.password && (
<span>{errors.password}</span>
)}
<button type='submit'>Login</button>
</form>
)}
</FormValidation>
);
}
// 🎯 NHIỆM VỤ:
// 1. Tạo useFormValidation hook
// 2. Refactor LoginForm để dùng hook
// 3. (Bonus) Keep FormValidation component cho backwards compatibility💡 Solution
/**
* Form Validation - Refactored from Render Props to Hook
*/
import { useState, useCallback } from 'react';
// ✅ MODERN: Custom Hook
function useFormValidation({ initialValues, validate }) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = useCallback(
(name, value) => {
setValues((prev) => {
const newValues = { ...prev, [name]: value };
// Validate on change
const fieldErrors = validate(newValues);
setErrors(fieldErrors);
return newValues;
});
},
[validate],
);
const handleBlur = useCallback((name) => {
setTouched((prev) => ({ ...prev, [name]: true }));
}, []);
const handleSubmit = useCallback(
(onSubmit) => {
return (e) => {
e.preventDefault();
// Mark all as touched
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {});
setTouched(allTouched);
// Validate
const formErrors = validate(values);
setErrors(formErrors);
// Submit if no errors
if (Object.keys(formErrors).length === 0) {
onSubmit(values);
}
};
},
[values, validate],
);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset,
};
}
// ✅ Refactored LoginForm - Much cleaner!
function LoginFormModern() {
const validate = useCallback((values) => {
const errors = {};
if (!values.email) errors.email = 'Email is required';
if (!values.password) errors.password = 'Password is required';
if (values.password && values.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
return errors;
}, []);
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset,
} = useFormValidation({
initialValues: { email: '', password: '' },
validate,
});
const onSubmit = (values) => {
console.log('Form submitted:', values);
alert('Login successful!');
reset();
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
style={{ maxWidth: '400px' }}
>
<div style={{ marginBottom: '16px' }}>
<label
htmlFor='email'
style={{ display: 'block', marginBottom: '4px' }}
>
Email
</label>
<input
id='email'
type='email'
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
style={{
width: '100%',
padding: '8px',
border:
touched.email && errors.email
? '2px solid red'
: '1px solid #ccc',
borderRadius: '4px',
}}
/>
{touched.email && errors.email && (
<span style={{ color: 'red', fontSize: '14px' }}>{errors.email}</span>
)}
</div>
<div style={{ marginBottom: '16px' }}>
<label
htmlFor='password'
style={{ display: 'block', marginBottom: '4px' }}
>
Password
</label>
<input
id='password'
type='password'
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
style={{
width: '100%',
padding: '8px',
border:
touched.password && errors.password
? '2px solid red'
: '1px solid #ccc',
borderRadius: '4px',
}}
/>
{touched.password && errors.password && (
<span style={{ color: 'red', fontSize: '14px' }}>
{errors.password}
</span>
)}
</div>
<button
type='submit'
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Login
</button>
</form>
);
}
// 🎁 BONUS: Backwards compatibility wrapper
// Keep old API working cho existing code
function FormValidation({ initialValues, validate, children }) {
const formState = useFormValidation({ initialValues, validate });
return children(formState);
}
// Old code vẫn hoạt động!
function LoginFormLegacy() {
const validate = (values) => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
};
return (
<FormValidation
initialValues={{ email: '', password: '' }}
validate={validate}
>
{({ values, errors, handleChange, handleSubmit }) => (
<form onSubmit={handleSubmit(() => console.log('Legacy submit'))}>
<input
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
<button>Submit</button>
</form>
)}
</FormValidation>
);
}
// Demo both versions
function App() {
return (
<div style={{ padding: '20px' }}>
<h1>Form Validation Refactoring</h1>
<section>
<h2>Modern (Hook):</h2>
<LoginFormModern />
</section>
<hr style={{ margin: '40px 0' }} />
<section>
<h2>Legacy (Render Props - Backwards Compatible):</h2>
<LoginFormLegacy />
</section>
</div>
);
}
/*
Migration Benefits:
Before (Render Props):
❌ Callback nesting
❌ Verbose syntax
❌ Hard to reuse logic
After (Hook):
✅ Clean, flat code
✅ Easy to compose with other hooks
✅ Reusable across components
✅ Better readability
✅ Easier to test
Backwards Compatibility:
✅ Old code still works
✅ Gradual migration possible
✅ No breaking changes
✅ Can migrate component by component
*/⭐⭐⭐ Bài 3: Refactor Multiple HOCs (40 phút)
🎯 Mục tiêu: Giải quyết wrapper hell
/**
* 🎯 Mục tiêu: Refactor component có nhiều HOCs
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là developer, tôi muốn code dễ đọc và maintain hơn"
*
* ✅ Acceptance Criteria:
* - [ ] Refactor tất cả HOCs thành hooks
* - [ ] Maintain same functionality
* - [ ] Improve readability
* - [ ] Better performance (if possible)
* - [ ] Easier to test
*
* 🎨 Technical Constraints:
* - Must work exactly like before
* - No breaking changes to component API
* - Clean up wrapper components
*
* 🚨 Edge Cases:
* - Props collision between HOCs
* - Execution order matters
* - Conditional logic in HOCs
*/
// ❌ LEGACY: Multiple HOCs
// HOC 1: Add analytics tracking
function withAnalytics(Component) {
return function AnalyticsComponent(props) {
useEffect(() => {
console.log('Page view:', props.pageName);
}, [props.pageName]);
return <Component {...props} />;
};
}
// HOC 2: Add authentication check
function withAuth(Component) {
return function AuthComponent(props) {
const [user] = useState({ name: 'John', role: 'admin' });
const [isAuthenticated] = useState(true);
if (!isAuthenticated) {
return <div>Please login</div>;
}
return (
<Component
{...props}
user={user}
isAuthenticated={isAuthenticated}
/>
);
};
}
// HOC 3: Add loading state
function withLoading(Component) {
return function LoadingComponent(props) {
const [isLoading] = useState(false);
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
};
}
// HOC 4: Add error boundary
function withErrorBoundary(Component) {
return function ErrorBoundaryComponent(props) {
const [hasError] = useState(false);
if (hasError) {
return <div>Something went wrong</div>;
}
return <Component {...props} />;
};
}
// Base component
function Dashboard({ pageName, user, isAuthenticated }) {
return (
<div>
<h1>{pageName}</h1>
<p>Welcome, {user?.name}</p>
<p>Role: {user?.role}</p>
<p>Authenticated: {isAuthenticated ? 'Yes' : 'No'}</p>
</div>
);
}
// ❌ Wrapper hell!
const EnhancedDashboard = withAnalytics(
withAuth(withLoading(withErrorBoundary(Dashboard))),
);
function AppLegacy() {
return <EnhancedDashboard pageName='Dashboard' />;
}
// 🎯 NHIỆM VỤ:
// 1. Tạo hooks tương ứng cho mỗi HOC
// 2. Refactor Dashboard để dùng hooks
// 3. Compare code trước/sau
// 4. Document improvements💡 Solution
/**
* Multiple HOCs → Multiple Hooks Refactoring
*/
import { useState, useEffect, useCallback } from 'react';
// =====================================
// ✅ STEP 1: Create Hooks
// =====================================
// Hook 1: Analytics
function useAnalytics(pageName) {
useEffect(() => {
console.log('Page view:', pageName);
// Simulate analytics call
// analytics.track('page_view', { page: pageName });
return () => {
console.log('Page leave:', pageName);
};
}, [pageName]);
}
// Hook 2: Authentication
function useAuth() {
const [user] = useState({ name: 'John', role: 'admin' });
const [isAuthenticated] = useState(true);
// In real app: fetch from API, check token, etc.
return {
user,
isAuthenticated,
logout: useCallback(() => {
console.log('Logging out...');
}, []),
};
}
// Hook 3: Loading
function useLoading(initialState = false) {
const [isLoading, setIsLoading] = useState(initialState);
const startLoading = useCallback(() => setIsLoading(true), []);
const stopLoading = useCallback(() => setIsLoading(false), []);
return {
isLoading,
startLoading,
stopLoading,
};
}
// Hook 4: Error handling
function useErrorHandler() {
const [error, setError] = useState(null);
const handleError = useCallback((err) => {
console.error('Error caught:', err);
setError(err);
}, []);
const clearError = useCallback(() => setError(null), []);
return {
error,
hasError: !!error,
handleError,
clearError,
};
}
// =====================================
// ✅ STEP 2: Refactored Component
// =====================================
function DashboardModern({ pageName }) {
// Compose all hooks - clean and flat!
useAnalytics(pageName);
const { user, isAuthenticated, logout } = useAuth();
const { isLoading, startLoading, stopLoading } = useLoading();
const { error, hasError, handleError, clearError } = useErrorHandler();
// Simulate data fetching
useEffect(() => {
startLoading();
setTimeout(() => {
stopLoading();
}, 1000);
}, [startLoading, stopLoading]);
// Early returns for different states
if (!isAuthenticated) {
return (
<div style={{ padding: '20px', border: '2px solid red' }}>
<h2>Access Denied</h2>
<p>Please login to view this page</p>
</div>
);
}
if (hasError) {
return (
<div style={{ padding: '20px', border: '2px solid red' }}>
<h2>Something went wrong</h2>
<p>{error?.message || 'Unknown error'}</p>
<button onClick={clearError}>Try Again</button>
</div>
);
}
if (isLoading) {
return (
<div style={{ padding: '20px' }}>
<h2>Loading...</h2>
<p>Please wait...</p>
</div>
);
}
// Main content
return (
<div style={{ padding: '20px', border: '1px solid #ccc' }}>
<h1>{pageName}</h1>
<p>
Welcome, <strong>{user?.name}</strong>
</p>
<p>Role: {user?.role}</p>
<p>Authenticated: {isAuthenticated ? '✅ Yes' : '❌ No'}</p>
<div style={{ marginTop: '20px', display: 'flex', gap: '8px' }}>
<button onClick={logout}>Logout</button>
<button onClick={() => handleError(new Error('Test error'))}>
Simulate Error
</button>
<button
onClick={() => {
startLoading();
setTimeout(stopLoading, 2000);
}}
>
Simulate Loading
</button>
</div>
</div>
);
}
// =====================================
// ✅ STEP 3: Comparison
// =====================================
function App() {
return (
<div style={{ padding: '20px' }}>
<h1>HOCs vs Hooks Refactoring</h1>
<section style={{ marginBottom: '40px' }}>
<h2>Legacy (Multiple HOCs):</h2>
<pre
style={{
background: '#f5f5f5',
padding: '16px',
overflow: 'auto',
fontSize: '14px',
}}
>
{`// Wrapper hell!
const EnhancedDashboard = withAnalytics(
withAuth(
withLoading(
withErrorBoundary(
Dashboard
)
)
)
);
// Component tree in DevTools:
AnalyticsComponent
└─ AuthComponent
└─ LoadingComponent
└─ ErrorBoundaryComponent
└─ Dashboard
Problems:
❌ 4 wrapper components
❌ Hard to debug
❌ Props collision risk
❌ Execution order unclear
❌ Can't easily conditional compose`}
</pre>
</section>
<section style={{ marginBottom: '40px' }}>
<h2>Modern (Hooks):</h2>
<pre
style={{
background: '#e8f5e9',
padding: '16px',
overflow: 'auto',
fontSize: '14px',
}}
>
{`function DashboardModern({ pageName }) {
useAnalytics(pageName);
const { user, isAuthenticated } = useAuth();
const { isLoading } = useLoading();
const { error, hasError } = useErrorHandler();
// Clean, flat, composable!
}
// Component tree in DevTools:
DashboardModern (just one!)
Benefits:
✅ No wrappers
✅ Easy to debug
✅ No props collision
✅ Clear execution order
✅ Easy conditional logic`}
</pre>
</section>
<section>
<h2>Live Demo:</h2>
<DashboardModern pageName='My Dashboard' />
</section>
<section style={{ marginTop: '40px' }}>
<h3>📊 Metrics Comparison:</h3>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
marginTop: '16px',
}}
>
<thead>
<tr style={{ backgroundColor: '#f5f5f5' }}>
<th
style={{
padding: '12px',
border: '1px solid #ddd',
textAlign: 'left',
}}
>
Metric
</th>
<th
style={{
padding: '12px',
border: '1px solid #ddd',
textAlign: 'left',
}}
>
HOCs
</th>
<th
style={{
padding: '12px',
border: '1px solid #ddd',
textAlign: 'left',
}}
>
Hooks
</th>
</tr>
</thead>
<tbody>
<tr>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
Wrapper Components
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>4</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>0</td>
</tr>
<tr>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
Lines of Code
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>~80</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>~50</td>
</tr>
<tr>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
Readability
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
⭐⭐
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
⭐⭐⭐⭐⭐
</td>
</tr>
<tr>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
Testability
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
⭐⭐
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
⭐⭐⭐⭐⭐
</td>
</tr>
<tr>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
DevTools Clarity
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
⭐⭐
</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
⭐⭐⭐⭐⭐
</td>
</tr>
</tbody>
</table>
</section>
</div>
);
}
/*
Migration Steps Taken:
1. ✅ Identified each HOC's concern
2. ✅ Created equivalent hooks
3. ✅ Composed hooks in component
4. ✅ Maintained same functionality
5. ✅ Improved readability
6. ✅ Better performance (fewer components)
7. ✅ Easier to test (hooks can be tested in isolation)
Key Improvements:
Performance:
- Before: 5 components in tree (4 HOCs + 1 actual)
- After: 1 component in tree
- Result: Faster rendering, less memory
Readability:
- Before: Nested function calls, hard to follow
- After: Linear hook calls, clear flow
- Result: Easier onboarding, faster debugging
Maintainability:
- Before: Change HOC = might break other uses
- After: Change hook = clear impact
- Result: Safer refactoring
Testability:
- Before: Must test with full HOC stack
- After: Can test hooks individually
- Result: Better test coverage, faster tests
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh Chi Tiết
| Aspect | Render Props | HOCs | Hooks | Compound Components |
|---|---|---|---|---|
| Year Introduced | 2017 | 2016 | 2019 | 2018 |
| Status | Legacy | Legacy | ✅ Current | ✅ Current |
| Code Complexity | ⭐⭐ (callbacks) | ⭐⭐ (wrappers) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Readability | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Composition | ⭐⭐ (nested) | ⭐⭐⭐ (static) | ⭐⭐⭐⭐⭐ (flexible) | ⭐⭐⭐⭐ |
| Performance | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| DevTools | ⭐⭐⭐ | ⭐⭐ (wrappers) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Learning Curve | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Use Case | Logic sharing | Enhancing components | Logic sharing | UI composition |
| When to Use | Never (use Hooks) | Never (use Hooks) | ✅ Always | ✅ Complex UI |
Decision Tree - When to Use What?
Need to share logic between components?
│
├─ Stateful logic? (useState, useEffect, etc.)
│ └─> ✅ Custom Hook
│ Example: useFetch, useAuth, useMouse
│
├─ UI composition? (flexible layout)
│ └─> ✅ Compound Components
│ Example: <Tabs>, <Menu>, <Modal>
│
├─ Simple prop transformation?
│ └─> ✅ Regular component with props
│ Example: <Button variant="primary" />
│
└─ Reading legacy code?
├─ See render props?
│ └─> Understand pattern, consider refactoring to Hook
│
└─ See HOCs?
└─> Understand pattern, consider refactoring to Hook
NEVER start new code with:
❌ Render Props (use Hooks instead)
❌ HOCs (use Hooks instead)
❌ Mixins (deprecated!)Evolution Timeline
REACT PATTERNS EVOLUTION:
2013 ─── Mixins
class MyComponent extends React.Component {
mixins: [MouseMixin]
}
❌ Deprecated in 2015
2016 ─── Higher-Order Components (HOCs)
const Enhanced = withMouse(Component)
⚠️ Still works, but legacy
Problems: Wrapper hell, props collision
2017 ─── Render Props
<Mouse>{mouse => <UI />}</Mouse>
⚠️ Still works, but legacy
Problems: Callback hell, hard to read
2018 ─── Compound Components
<Tabs><Tabs.Tab /></Tabs>
✅ Still good for UI composition
Best for: Complex UI components
2019 ─── Hooks 🎉
const mouse = useMouse()
✅ Current standard
Best for: Logic sharing, stateful logic
2020+ ── Server Components (Next.js)
async function Component() { ... }
✅ Cutting edge
Best for: Data fetching, SSR
(Will learn in Next.js module)
Current Best Practices (2024):
1. Default: Hooks for logic
2. Compound Components for complex UI
3. Regular components for simple UI
4. Understand legacy patterns to read old code🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Props Collision in HOCs
// ❌ Code có bug
function withUser(Component) {
return function (props) {
const user = { name: 'HOC User', id: 1 };
return (
<Component
{...props}
user={user}
/>
);
};
}
function withAuth(Component) {
return function (props) {
const user = { name: 'Auth User', id: 2, role: 'admin' };
return (
<Component
{...props}
user={user}
/>
);
};
}
const Enhanced = withUser(withAuth(MyComponent));
function MyComponent({ user }) {
return <div>User: {user.name}</div>;
}❓ Câu hỏi: User name hiển thị là gì? Tại sao đây là vấn đề?
💡 Giải thích
Kết quả: "HOC User" (from withUser)
Nguyên nhân: Props collision - HOC bên ngoài override props từ HOC bên trong
// Execution order:
1. withAuth wraps MyComponent
→ Passes user = { name: 'Auth User', id: 2, role: 'admin' }
2. withUser wraps the result
→ Passes user = { name: 'HOC User', id: 1 }
→ OVERWRITES previous user!
Result: Only see 'HOC User', lose 'role' data!❌ This is a MAJOR problem with HOCs:
- Props can collide
- Outer HOC wins
- Silent data loss
- Hard to debug
✅ Solutions:
Option 1: Namespace props
function withUser(Component) {
return function (props) {
const userData = { name: 'HOC User', id: 1 };
return (
<Component
{...props}
userData={userData}
/>
);
};
}
function withAuth(Component) {
return function (props) {
const authData = { name: 'Auth User', id: 2, role: 'admin' };
return (
<Component
{...props}
authData={authData}
/>
);
};
}
// No collision!
function MyComponent({ userData, authData }) {
return (
<div>
<p>User: {userData.name}</p>
<p>
Auth: {authData.name}, Role: {authData.role}
</p>
</div>
);
}Option 2: Merge props
function withUser(Component) {
return function (props) {
const user = { name: 'HOC User', id: 1 };
// Merge instead of replace
return (
<Component
{...props}
user={{ ...props.user, ...user }}
/>
);
};
}Option 3: Use Hooks instead! (BEST)
function useUser() {
return { name: 'Hook User', id: 1 };
}
function useAuth() {
return { name: 'Auth User', id: 2, role: 'admin' };
}
function MyComponent() {
const user = useUser();
const auth = useAuth();
// Clear, no collision!
return (
<div>
<p>User: {user.name}</p>
<p>
Auth: {auth.name}, Role: {auth.role}
</p>
</div>
);
}Lesson: Props collision là lý do chính để avoid HOCs. Hooks không có vấn đề này!
Bug 2: Stale Closure in Render Props
// ❌ Code có bug
function Counter({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return children({ count, increment });
}
function App() {
return (
<Counter>
{({ count, increment }) => (
<div>
<p>Count: {count}</p>
<button
onClick={() => {
increment();
increment();
increment();
}}
>
+3
</button>
</div>
)}
</Counter>
);
}❓ Câu hỏi: Click button, count tăng bao nhiêu? Tại sao?
💡 Giải thích
Kết quả: Count chỉ tăng +1 (not +3!)
Nguyên nhân: Stale closure - increment function captures old count value
// ❌ Problem
const increment = () => setCount(count + 1);
// ^^^^^ captured value!
// When button clicked:
increment(); // setCount(0 + 1) = 1
increment(); // setCount(0 + 1) = 1 (still using count = 0!)
increment(); // setCount(0 + 1) = 1
// Result: 1✅ FIX 1: Functional update
function Counter({ children }) {
const [count, setCount] = useState(0);
// ✅ Use previous value
const increment = () => setCount((prev) => prev + 1);
return children({ count, increment });
}
// Now:
increment(); // setCount(prev => 0 + 1) = 1
increment(); // setCount(prev => 1 + 1) = 2
increment(); // setCount(prev => 2 + 1) = 3
// Result: 3 ✅✅ FIX 2: Use Hook instead
function useCounter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return { count, increment };
}
function App() {
const { count, increment } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button
onClick={() => {
increment();
increment();
increment();
}}
>
+3
</button>
</div>
);
}Why Hooks are better here:
- Clearer code
- useCallback memoization
- No callback syntax
- Easier to spot closure bugs
Lesson: Stale closures affect both Render Props and Hooks, but Hooks make it easier to see and fix!
Bug 3: HOC Order Matters
// ❌ Code có bug
function withLogger(Component) {
return function LoggerComponent(props) {
useEffect(() => {
console.log('Component mounted with props:', props);
}, [props]);
return <Component {...props} />;
};
}
function withAuth(Component) {
return function AuthComponent(props) {
const [isAuth] = useState(false);
if (!isAuth) {
return <div>Not authenticated</div>;
}
return <Component {...props} />;
};
}
// Version A
const ComponentA = withLogger(withAuth(MyComponent));
// Version B
const ComponentB = withAuth(withLogger(MyComponent));❓ Câu hỏi: Hai versions khác nhau như thế nào? Version nào đúng?
💡 Giải thích
Behavior khác nhau:
Version A: withLogger(withAuth(MyComponent))
Flow:
1. withAuth kiểm tra authentication
2. Nếu !isAuth → return "Not authenticated"
3. Logger KHÔNG chạy vì component không mount
4. MyComponent KHÔNG render
Console: (không có log nếu not authenticated)Version B: withAuth(withLogger(MyComponent))
Flow:
1. Logger wraps MyComponent
2. withAuth kiểm tra authentication
3. Nếu !isAuth → return "Not authenticated"
4. Logger VẪN chạy (vì LoggerComponent mounted)
5. MyComponent KHÔNG render
Console: "Component mounted with props: ..."
(log xuất hiện dù not authenticated!)Vấn đề:
- HOC order matters!
- Không rõ ràng từ code
- Dễ sai khi refactor
- Hard to reason about
Which is correct?
- Depends on requirements!
- Usually A (don't log if not authenticated)
- But not obvious from code!
✅ With Hooks - Much clearer:
function MyComponent() {
// Execution order is CLEAR!
const { isAuth } = useAuth();
useLogger('MyComponent'); // This runs regardless
if (!isAuth) {
return <div>Not authenticated</div>;
}
// Or:
// Only log if authenticated
if (isAuth) {
useLogger('MyComponent');
}
return <div>Content</div>;
}Why Hooks are better:
- Execution order visible in code
- Easy to change order
- Can conditionally run hooks (with care)
- No surprises
Lesson: HOC composition order affects behavior but isn't obvious from code. Hooks make execution flow explicit!
✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu Render Props pattern và tại sao nó legacy
- [ ] Tôi hiểu HOCs pattern và vấn đề của nó
- [ ] Tôi biết refactor Render Props → Hooks
- [ ] Tôi biết refactor HOCs → Hooks
- [ ] Tôi hiểu trade-offs giữa các patterns
- [ ] Tôi biết khi nào nên refactor và khi nào nên giữ nguyên
- [ ] Tôi nhận biết được các patterns trong legacy code
- [ ] Tôi hiểu tại sao Hooks là modern standard
Code Review Checklist
Khi review legacy code:
Identify Patterns:
- [ ] Is this Render Props? (children as function)
- [ ] Is this HOC? (function returns component)
- [ ] Are there multiple HOCs? (wrapper hell)
- [ ] Is this causing bugs? (props collision, stale closures)
Refactor Decision:
- [ ] Is code actively being changed?
- [ ] Is it causing maintenance issues?
- [ ] Is team comfortable with Hooks?
- [ ] Is refactor worth the risk?
If Refactoring:
- [ ] Extract logic to custom hooks
- [ ] Maintain same API if possible
- [ ] Add tests before refactoring
- [ ] Refactor incrementally
- [ ] Test thoroughly after
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Identify & Document Legacy Patterns
Cho đoạn code sau, identify patterns và propose refactoring:
// Legacy codebase
const EnhancedForm = withValidation(withSubmit(withReset(FormComponent)));
function App() {
return (
<DataProvider url='/api/data'>
{({ data, loading }) => (
<Theme>
{({ theme }) => (
<EnhancedForm
data={data}
loading={loading}
theme={theme}
/>
)}
</Theme>
)}
</DataProvider>
);
}Tasks:
- List all patterns used
- Identify potential bugs
- Write refactored version with Hooks
- Document migration steps
Nâng cao (60 phút)
Build a Migration Guide
Tạo document về migration strategy cho team:
- Assessment: Làm sao identify code cần refactor?
- Priority: Code nào refactor trước?
- Process: Step-by-step migration process
- Testing: Làm sao đảm bảo không break?
- Rollback: Plan B nếu có vấn đề
- Timeline: Realistic estimates
Include:
- Code examples (before/after)
- Checklist templates
- Common pitfalls
- Success metrics
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Docs - Render Propshttps://legacy.reactjs.org/docs/render-props.html (Note: Legacy docs, nhưng explains pattern well)
React Docs - Higher-Order Componentshttps://legacy.reactjs.org/docs/higher-order-components.html
React Docs - Hooks Introductionhttps://react.dev/learn#using-hooks (Shows why Hooks replace old patterns)
Đọc thêm
Michael Jackson - "Never Write Another HOC" Classic talk explaining Render Props
Dan Abramov - "Making Sense of React Hooks"https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889
Kent C. Dodds - "How to Use React Context Effectively" Shows modern patterns vs legacy
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần từ những ngày trước)
- Ngày 24: Custom Hooks fundamentals
- Ngày 32-34: Performance patterns (memo, useCallback, useMemo)
- Ngày 36-38: Context API (thay thế props drilling)
- Ngày 39: Compound Components (modern pattern)
Hướng tới (những ngày sau sẽ dùng)
- Ngày 41-44: Form libraries (modern approaches, not Render Props)
- Ngày 53-57: Testing patterns (easier với Hooks)
- Ngày 58-59: TypeScript (better inference với Hooks)
- Projects: Tất cả code mới sẽ dùng Hooks
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. When to Refactor Legacy Code
HIGH PRIORITY (refactor ngay):
✅ Code causing bugs
✅ Code being actively developed
✅ Team wants to learn Hooks
✅ Clear path to improvement
MEDIUM PRIORITY (refactor khi có thời gian):
⚠️ Code stable nhưng hard to maintain
⚠️ Multiple HOCs/Render Props nested
⚠️ Planning future features in area
LOW PRIORITY (có thể để nguyên):
❌ Code stable, not changing
❌ Would break public API
❌ Team unfamiliar with Hooks
❌ No resources for testing2. Migration Strategy
INCREMENTAL APPROACH:
Week 1: Assessment
- Identify legacy patterns
- Measure code complexity
- Prioritize by impact
Week 2-3: Foundation
- Train team on Hooks
- Create migration guide
- Setup testing infrastructure
Week 4+: Migrate
- One component at a time
- Test thoroughly
- Monitor production
- Rollback plan ready
Don't:
❌ Refactor everything at once
❌ Mix old and new patterns in same component
❌ Skip tests
❌ Rush without measuring3. Team Considerations
Communication:
- Explain WHY refactoring
- Show before/after examples
- Address concerns
- Get buy-in
Training:
- Hooks workshop
- Pair programming
- Code review focus
- Documentation
Metrics:
- Code complexity (before/after)
- Bug rate
- Development speed
- Team satisfactionCâu Hỏi Phỏng Vấn
Junior Level:
Q: "Render Props là gì?" A: Pattern where component nhận function và gọi nó với data. Legacy pattern, giờ dùng Hooks.
Mid Level:
Q: "Tại sao Hooks tốt hơn HOCs?" A:
- No wrapper hell
- No props collision
- Clearer code
- Better composition
- Easier to debug
Q: "Khi nào vẫn OK dùng Render Props?" A:
- Third-party library requirement
- Public API can't change
- Team consensus
- But default should be Hooks
Senior Level:
Q: "Làm sao migrate large codebase từ HOCs sang Hooks?" A:
- Assess and prioritize
- Create migration guide
- Train team
- Migrate incrementally
- Test thoroughly
- Monitor production
- Maintain backwards compatibility initially
Q: "Trade-offs khi refactor legacy patterns?" A:
- ✅ Better maintainability
- ✅ Modern patterns
- ✅ Easier onboarding
- ❌ Risk of bugs
- ❌ Time investment
- ❌ Learning curve
- Decision: Depends on code churn, team capacity, business priority
War Stories
Story 1: The HOC Hell
Context: Enterprise app with 50+ HOCs
Problem: withAuth(withTheme(withData(withLogger(...))))
Impact: 10+ wrapper components, 2s initial render
Solution: Migrated to Hooks over 3 months
Result: 1 component, 200ms render, 90% fewer bugs
Lesson: Don't let HOCs grow uncheckedStory 2: Props Collision Bug
Context: Payment form with withUser + withPayment HOCs
Problem: Both added "currency" prop
Impact: Used wrong currency, wrong charges!
Discovery: Customer complaint, hard to debug
Solution: Refactored to useUser() + usePayment()
Result: No more collisions, explicit dependencies
Lesson: Props collision can cause $$$ bugsStory 3: Migration Too Fast
Context: Team refactored 100 components in 1 week
Problem: Introduced 20+ bugs, production down
Impact: Rollback, lost 1 week work
Solution: Slow down, 5 components/week with testing
Result: Clean migration over 3 months
Lesson: Slow and steady wins the race🎯 PREVIEW NGÀY 41
Ngày mai: React Hook Form - Basics
Chúng ta sẽ học:
- Tại sao form trong React khó?
- React Hook Form là gì?
- Performance benefits
- Basic form handling
- Validation strategies
Chuẩn bị:
- Review controlled components (Ngày 13)
- Suy nghĩ: Form nào bạn từng làm?
- Những vấn đề gì gặp với forms?
Mục tiêu: Master form handling với modern library
✅ Chúc mừng bạn đã hoàn thành Ngày 40!
Bạn đã hiểu legacy patterns và biết refactor sang modern approach. Đây là kỹ năng quan trọng khi làm việc với codebase thực tế! 🚀