Best practices for design patterns in React
React, as a mainstream framework in modern front-end development, naturally aligns with component-based thinking and design patterns. Properly applying design patterns can enhance code maintainability, reusability, and extensibility, especially in complex application scenarios. Below is a practical analysis of common design patterns in React.
Compound Components Pattern
The compound pattern achieves functional decoupling through component nesting, with a classic example being the relationship between <select>
and <option>
. In React, this can be implemented using React.Context
and React.cloneElement
:
const Tabs = ({ children }) => {
const [activeIndex, setActiveIndex] = useState(0);
return (
<TabContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">{children}</div>
</TabContext.Provider>
);
};
const TabList = ({ children }) => {
const { activeIndex, setActiveIndex } = useContext(TabContext);
return React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isActive: index === activeIndex,
onClick: () => setActiveIndex(index)
})
);
};
// Usage example
<Tabs>
<TabList>
<Tab>Home</Tab>
<Tab>Products</Tab>
</TabList>
<TabPanels>
<TabPanel>Home content</TabPanel>
<TabPanel>Product list</TabPanel>
</TabPanels>
</Tabs>
This pattern allows the parent component to manage state while child components access the state via context, keeping UI and logic separate.
Render Props Pattern
Passing dynamic content through a function as a child component (children prop
) to achieve logic reuse:
const MouseTracker = ({ children }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{children(position)}
</div>
);
};
// Usage example
<MouseTracker>
({ x, y }) => (
<h1>
Mouse position: {x}, {y}
</h1>
)
</MouseTracker>
A more flexible variant uses any prop name:
<DataProvider render={(data) => <Child data={data} />} />
Higher-Order Components (HOC)
Wrapping components with functions to implement cross-cutting concerns:
const withLoading = (WrappedComponent) => {
return (props) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setLoading(false), 1000);
return () => clearTimeout(timer);
}, []);
return loading ? <Spinner /> : <WrappedComponent {...props} />;
};
};
// Usage example
const UserProfile = ({ user }) => <div>{user.name}</div>;
const UserProfileWithLoading = withLoading(UserProfile);
Note: HOCs may cause ref loss, which can be resolved using React.forwardRef
.
State Lifting and Single Source of Truth
A common pattern in form components for lifting state:
const FormContainer = () => {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<form>
<Input
name="username"
value={formData.username}
onChange={handleChange}
/>
<Input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
</form>
);
};
Provider Pattern
Implementing global state management via the Context API:
const ThemeContext = React.createContext('light');
const App = () => {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
};
const ThemedButton = () => {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#EEE' }}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
Toggle Theme
</button>
);
};
Container/Presentational Component Pattern
A classic pattern for separating logic and UI:
// Container component
const UserListContainer = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return <UserList users={users} loading={loading} />;
};
// Presentational component
const UserList = ({ users, loading }) => (
loading ? <Spinner /> : (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
);
Custom Hook Pattern
Extracting component logic into reusable hooks:
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
// Usage example
const Counter = () => {
const [count, setCount] = useLocalStorage('counter', 0);
return (
<button onClick={() => setCount(c => c + 1)}>
Click count: {count}
</button>
);
};
Strategy Pattern in Form Validation
Dynamically switching validation rules:
const validationStrategies = {
email: (value) => /^\S+@\S+$/.test(value),
password: (value) => value.length >= 8
};
const useValidation = (strategyName) => {
const validate = validationStrategies[strategyName];
const [error, setError] = useState('');
const check = (value) => {
const isValid = validate(value);
setError(isValid ? '' : `Invalid ${strategyName}`);
return isValid;
};
return [error, check];
};
// Usage example
const EmailInput = () => {
const [email, setEmail] = useState('');
const [error, validateEmail] = useValidation('email');
return (
<>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => validateEmail(email)}
/>
{error && <div className="error">{error}</div>}
</>
);
};
Observer Pattern and Event Bus
Implementing loose coupling between components:
const EventBus = {
events: {},
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
},
publish(event, data) {
(this.events[event] || []).forEach(cb => cb(data));
}
};
// Publisher component
const Publisher = () => (
<button onClick={() => EventBus.publish('alert', 'Button clicked')}>
Trigger Event
</button>
);
// Subscriber component
const Subscriber = () => {
const [message, setMessage] = useState('');
useEffect(() => {
EventBus.subscribe('alert', setMessage);
return () => EventBus.unsubscribe('alert', setMessage);
}, []);
return <div>{message}</div>;
};
Factory Pattern for Dynamic Components
Generating components based on configuration:
const componentMap = {
text: ({ value }) => <span>{value}</span>,
image: ({ src }) => <img src={src} alt="" />,
button: ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
)
};
const DynamicComponent = ({ type, ...props }) => {
const Component = componentMap[type] || componentMap.text;
return <Component {...props} />;
};
// Usage example
const configs = [
{ type: 'text', value: 'Static text' },
{ type: 'button', label: 'Submit', onClick: () => alert('Clicked') }
];
const ComponentFactory = () => (
<div>
{configs.map((config, i) => (
<DynamicComponent key={i} {...config} />
))}
</div>
);
Memento Pattern and State Snapshots
Implementing undo/redo functionality:
const useHistory = (initialState) => {
const [history, setHistory] = useState([initialState]);
const [index, setIndex] = useState(0);
const current = history[index];
const push = (newState) => {
const newHistory = history.slice(0, index + 1);
setHistory([...newHistory, newState]);
setIndex(newHistory.length);
};
const undo = () => index > 0 && setIndex(i => i - 1);
const redo = () => index < history.length - 1 && setIndex(i => i + 1);
return [current, push, { undo, redo, canUndo: index > 0, canRedo: index < history.length - 1 }];
};
// Usage example
const SketchPad = () => {
const [paths, setPaths, { undo, redo }] = useHistory([]);
const handleDraw = (newPath) => {
setPaths([...paths, newPath]);
};
return (
<div>
<Canvas onDraw={handleDraw} paths={paths} />
<button onClick={undo} disabled={!canUndo}>Undo</button>
<button onClick={redo} disabled={!canRedo}>Redo</button>
</div>
);
};
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:Vue的响应式系统与设计模式