React Component Patterns

Master advanced React patterns to build scalable, maintainable, and reusable components. Learn when and how to apply each pattern for maximum effectiveness.

Advertisement Space - top-patterns

Google AdSense: horizontal

Compound Components Pattern

Build flexible components that work together while maintaining clean APIs

Compound components allow you to create a set of components that work together to form a complete UI while giving consumers flexibility in how they compose them.

Implementation

1// Compound Components - Flexible component composition
2import React, { createContext, useContext, useState, ReactNode } from 'react';
3
4// Context for sharing state between compound components
5interface AccordionContextType {
6 openItems: Set<string>;
7 toggleItem: (id: string) => void;
8 allowMultiple: boolean;
9}
10
11const AccordionContext = createContext<AccordionContextType | undefined>(undefined);
12
13// Custom hook to access accordion context
14const useAccordionContext = () => {
15 const context = useContext(AccordionContext);
16 if (!context) {
17 throw new Error('Accordion components must be used within an Accordion provider');
18 }
19 return context;
20};
21
22// Main Accordion component
23interface AccordionProps {
24 children: ReactNode;
25 allowMultiple?: boolean;
26 defaultOpen?: string[];
27}
28
29export const Accordion = ({
30 children,
31 allowMultiple = false,
32 defaultOpen = []
33}: AccordionProps) => {
34 const [openItems, setOpenItems] = useState<Set<string>>(
35 new Set(defaultOpen)
36 );
37
38 const toggleItem = (id: string) => {
39 setOpenItems(prev => {
40 const newSet = new Set(prev);
41
42 if (newSet.has(id)) {
43 newSet.delete(id);
44 } else {
45 if (!allowMultiple) {
46 newSet.clear();
47 }
48 newSet.add(id);
49 }
50
51 return newSet;
52 });
53 };
54
55 return (
56 <AccordionContext.Provider value={{ openItems, toggleItem, allowMultiple }}>
57 <div className="accordion">
58 {children}
59 </div>
60 </AccordionContext.Provider>
61 );
62};
63
64// Accordion Item component
65interface AccordionItemProps {
66 children: ReactNode;
67 id: string;
68}
69
70Accordion.Item = ({ children, id }: AccordionItemProps) => {
71 const { openItems } = useAccordionContext();
72 const isOpen = openItems.has(id);
73
74 return (
75 <div className="accordion-item" data-open={isOpen}>
76 {React.Children.map(children, child => {
77 if (React.isValidElement(child)) {
78 return React.cloneElement(child, { id, isOpen } as any);
79 }
80 return child;
81 })}
82 </div>
83 );
84};
85
86// Accordion Header component
87interface AccordionHeaderProps {
88 children: ReactNode;
89 id?: string;
90 isOpen?: boolean;
91}
92
93Accordion.Header = ({ children, id, isOpen }: AccordionHeaderProps) => {
94 const { toggleItem } = useAccordionContext();
95
96 return (
97 <button
98 className="accordion-header"
99 onClick={() => id && toggleItem(id)}
100 aria-expanded={isOpen}
101 >
102 {children}
103 <span className="accordion-icon">{isOpen ? '−' : '+'}</span>
104 </button>
105 );
106};
107
108// Accordion Panel component
109interface AccordionPanelProps {
110 children: ReactNode;
111 isOpen?: boolean;
112}
113
114Accordion.Panel = ({ children, isOpen }: AccordionPanelProps) => {
115 if (!isOpen) return null;
116
117 return (
118 <div className="accordion-panel">
119 {children}
120 </div>
121 );
122};
123
124// Usage example
125const FAQSection = () => {
126 return (
127 <Accordion allowMultiple defaultOpen={['q1']}>
128 <Accordion.Item id="q1">
129 <Accordion.Header>What is React?</Accordion.Header>
130 <Accordion.Panel>
131 React is a JavaScript library for building user interfaces.
132 </Accordion.Panel>
133 </Accordion.Item>
134
135 <Accordion.Item id="q2">
136 <Accordion.Header>Why use compound components?</Accordion.Header>
137 <Accordion.Panel>
138 Compound components provide flexibility while maintaining a clean API.
139 </Accordion.Panel>
140 </Accordion.Item>
141
142 <Accordion.Item id="q3">
143 <Accordion.Header>How do they work?</Accordion.Header>
144 <Accordion.Panel>
145 They share state through React Context and work together seamlessly.
146 </Accordion.Panel>
147 </Accordion.Item>
148 </Accordion>
149 );
150};

Advanced Example

1// Advanced Compound Component - Tab System
2interface TabsContextType {
3 activeTab: string;
4 setActiveTab: (id: string) => void;
5 orientation: 'horizontal' | 'vertical';
6}
7
8const TabsContext = createContext<TabsContextType | undefined>(undefined);
9
10export const Tabs = ({
11 children,
12 defaultTab,
13 orientation = 'horizontal',
14 onChange
15}: {
16 children: ReactNode;
17 defaultTab?: string;
18 orientation?: 'horizontal' | 'vertical';
19 onChange?: (tabId: string) => void;
20}) => {
21 const [activeTab, setActiveTab] = useState(defaultTab || '');
22
23 const handleTabChange = (id: string) => {
24 setActiveTab(id);
25 onChange?.(id);
26 };
27
28 return (
29 <TabsContext.Provider value={{
30 activeTab,
31 setActiveTab: handleTabChange,
32 orientation
33 }}>
34 <div className={`tabs tabs-${orientation}`}>
35 {children}
36 </div>
37 </TabsContext.Provider>
38 );
39};
40
41Tabs.List = ({ children }: { children: ReactNode }) => {
42 const { orientation } = useContext(TabsContext)!;
43
44 return (
45 <div className={`tab-list tab-list-${orientation}`} role="tablist">
46 {children}
47 </div>
48 );
49};
50
51Tabs.Tab = ({
52 children,
53 id
54}: {
55 children: ReactNode;
56 id: string;
57}) => {
58 const { activeTab, setActiveTab } = useContext(TabsContext)!;
59 const isActive = activeTab === id;
60
61 return (
62 <button
63 className={`tab ${isActive ? 'active' : ''}`}
64 onClick={() => setActiveTab(id)}
65 role="tab"
66 aria-selected={isActive}
67 aria-controls={`panel-${id}`}
68 >
69 {children}
70 </button>
71 );
72};
73
74Tabs.Panels = ({ children }: { children: ReactNode }) => {
75 return <div className="tab-panels">{children}</div>;
76};
77
78Tabs.Panel = ({
79 children,
80 id
81}: {
82 children: ReactNode;
83 id: string;
84}) => {
85 const { activeTab } = useContext(TabsContext)!;
86 const isActive = activeTab === id;
87
88 if (!isActive) return null;
89
90 return (
91 <div
92 className="tab-panel"
93 role="tabpanel"
94 id={`panel-${id}`}
95 >
96 {children}
97 </div>
98 );
99};

Render Props Pattern

Share component logic using a prop whose value is a function

The Render Props pattern allows you to share code between components using a prop whose value is a function that returns React elements.

Implementation

1// Render Props - Share logic between components
2import React, { useState, useEffect, ReactNode } from 'react';
3
4// Mouse position tracker using render props
5interface MousePosition {
6 x: number;
7 y: number;
8}
9
10interface MouseTrackerProps {
11 children: (position: MousePosition) => ReactNode;
12 // Alternative: use 'render' prop
13 // render: (position: MousePosition) => ReactNode;
14}
15
16const MouseTracker: React.FC<MouseTrackerProps> = ({ children }) => {
17 const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
18
19 useEffect(() => {
20 const handleMouseMove = (event: MouseEvent) => {
21 setPosition({
22 x: event.clientX,
23 y: event.clientY
24 });
25 };
26
27 window.addEventListener('mousemove', handleMouseMove);
28
29 return () => {
30 window.removeEventListener('mousemove', handleMouseMove);
31 };
32 }, []);
33
34 // Call children function with current position
35 return <>{children(position)}</>;
36};
37
38// Usage examples
39const MouseApp = () => {
40 return (
41 <div className="mouse-demo">
42 {/* Example 1: Tooltip that follows mouse */}
43 <MouseTracker>
44 {({ x, y }) => (
45 <div
46 className="tooltip"
47 style={{
48 position: 'fixed',
49 left: x + 10,
50 top: y + 10,
51 background: 'rgba(0,0,0,0.8)',
52 color: 'white',
53 padding: '4px 8px',
54 borderRadius: '4px',
55 pointerEvents: 'none'
56 }}
57 >
58 Position: {x}, {y}
59 </div>
60 )}
61 </MouseTracker>
62
63 {/* Example 2: Custom cursor */}
64 <MouseTracker>
65 {({ x, y }) => (
66 <div
67 className="custom-cursor"
68 style={{
69 position: 'fixed',
70 left: x - 10,
71 top: y - 10,
72 width: 20,
73 height: 20,
74 borderRadius: '50%',
75 border: '2px solid purple',
76 pointerEvents: 'none',
77 transform: 'translate(-50%, -50%)'
78 }}
79 />
80 )}
81 </MouseTracker>
82 </div>
83 );
84};
85
86// Data fetcher with render props
87interface FetchState<T> {
88 data: T | null;
89 loading: boolean;
90 error: Error | null;
91 refetch: () => void;
92}
93
94interface DataFetcherProps<T> {
95 url: string;
96 children: (state: FetchState<T>) => ReactNode;
97 // Optional transform function
98 transform?: (data: any) => T;
99}
100
101function DataFetcher<T = any>({
102 url,
103 children,
104 transform = (data) => data
105}: DataFetcherProps<T>) {
106 const [state, setState] = useState<FetchState<T>>({
107 data: null,
108 loading: true,
109 error: null,
110 refetch: () => {}
111 });
112
113 const fetchData = async () => {
114 setState(prev => ({ ...prev, loading: true, error: null }));
115
116 try {
117 const response = await fetch(url);
118 if (!response.ok) throw new Error('Failed to fetch');
119
120 const rawData = await response.json();
121 const transformedData = transform(rawData);
122
123 setState({
124 data: transformedData,
125 loading: false,
126 error: null,
127 refetch: fetchData
128 });
129 } catch (error) {
130 setState({
131 data: null,
132 loading: false,
133 error: error as Error,
134 refetch: fetchData
135 });
136 }
137 };
138
139 useEffect(() => {
140 fetchData();
141 }, [url]);
142
143 return <>{children(state)}</>;
144}
145
146// Usage with data fetcher
147const UserProfile = ({ userId }: { userId: string }) => {
148 return (
149 <DataFetcher<User>
150 url={`/api/users/${userId}`}
151 transform={(data) => ({
152 ...data,
153 fullName: `${data.firstName} ${data.lastName}`
154 })}
155 >
156 {({ data, loading, error, refetch }) => {
157 if (loading) return <div>Loading user...</div>;
158 if (error) return (
159 <div>
160 Error: {error.message}
161 <button onClick={refetch}>Retry</button>
162 </div>
163 );
164 if (!data) return <div>No user found</div>;
165
166 return (
167 <div className="user-profile">
168 <h2>{data.fullName}</h2>
169 <p>{data.email}</p>
170 <button onClick={refetch}>Refresh</button>
171 </div>
172 );
173 }}
174 </DataFetcher>
175 );
176};

Advanced Example

1// Advanced Render Props - Form Field Wrapper
2interface FieldState {
3 value: string;
4 error: string | null;
5 touched: boolean;
6 dirty: boolean;
7}
8
9interface FieldProps {
10 name: string;
11 validate?: (value: string) => string | null;
12 children: (props: {
13 field: {
14 value: string;
15 onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
16 onBlur: () => void;
17 name: string;
18 };
19 meta: FieldState;
20 }) => ReactNode;
21}
22
23const Field: React.FC<FieldProps> = ({ name, validate, children }) => {
24 const [state, setState] = useState<FieldState>({
25 value: '',
26 error: null,
27 touched: false,
28 dirty: false
29 });
30
31 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
32 const { value } = e.target;
33 const error = validate ? validate(value) : null;
34
35 setState(prev => ({
36 ...prev,
37 value,
38 error,
39 dirty: true
40 }));
41 };
42
43 const handleBlur = () => {
44 setState(prev => ({ ...prev, touched: true }));
45 };
46
47 const field = {
48 value: state.value,
49 onChange: handleChange,
50 onBlur: handleBlur,
51 name
52 };
53
54 return <>{children({ field, meta: state })}</>;
55};
56
57// Usage
58const SignupForm = () => {
59 return (
60 <form>
61 <Field
62 name="email"
63 validate={(value) => {
64 if (!value) return 'Email is required';
65 if (!value.includes('@')) return 'Invalid email';
66 return null;
67 }}
68 >
69 {({ field, meta }) => (
70 <div className="form-field">
71 <input
72 {...field}
73 type="email"
74 placeholder="Email"
75 className={meta.error && meta.touched ? 'error' : ''}
76 />
77 {meta.error && meta.touched && (
78 <span className="error-message">{meta.error}</span>
79 )}
80 </div>
81 )}
82 </Field>
83
84 <Field
85 name="password"
86 validate={(value) => {
87 if (!value) return 'Password is required';
88 if (value.length < 8) return 'Password must be at least 8 characters';
89 return null;
90 }}
91 >
92 {({ field, meta }) => (
93 <div className="form-field">
94 <input
95 {...field}
96 type="password"
97 placeholder="Password"
98 className={meta.error && meta.touched ? 'error' : ''}
99 />
100 {meta.error && meta.touched && (
101 <span className="error-message">{meta.error}</span>
102 )}
103 {meta.dirty && !meta.error && (
104 <span className="success-message">✓ Valid password</span>
105 )}
106 </div>
107 )}
108 </Field>
109 </form>
110 );
111};

Higher-Order Components (HOCs)

Enhance components with additional functionality through wrapping

HOCs are functions that take a component and return a new component with enhanced functionality. They enable code reuse and separation of concerns.

Implementation

1// Higher-Order Components - Component enhancement
2import React, { ComponentType, useEffect, useState } from 'react';
3
4// Basic HOC - Add loading state
5interface WithLoadingProps {
6 loading: boolean;
7}
8
9function withLoading<P extends object>(
10 Component: ComponentType<P>,
11 LoadingComponent?: ComponentType
12): ComponentType<P & WithLoadingProps> {
13 const Loading = LoadingComponent || (() => <div>Loading...</div>);
14
15 return (props: P & WithLoadingProps) => {
16 if (props.loading) {
17 return <Loading />;
18 }
19
20 return <Component {...props} />;
21 };
22}
23
24// Usage
25const UserProfile = ({ user }: { user: User }) => (
26 <div>
27 <h2>{user.name}</h2>
28 <p>{user.email}</p>
29 </div>
30);
31
32const UserProfileWithLoading = withLoading(UserProfile);
33
34// Advanced HOC - Authentication
35interface WithAuthProps {
36 isAuthenticated: boolean;
37 user: User | null;
38}
39
40function withAuth<P extends object>(
41 Component: ComponentType<P>,
42 fallback?: ComponentType
43): ComponentType<Omit<P, keyof WithAuthProps>> {
44 const LoginPrompt = fallback || (() => (
45 <div>Please log in to view this content.</div>
46 ));
47
48 return (props: Omit<P, keyof WithAuthProps>) => {
49 const [authState, setAuthState] = useState<WithAuthProps>({
50 isAuthenticated: false,
51 user: null
52 });
53 const [loading, setLoading] = useState(true);
54
55 useEffect(() => {
56 // Check authentication status
57 checkAuth().then(({ isAuthenticated, user }) => {
58 setAuthState({ isAuthenticated, user });
59 setLoading(false);
60 });
61 }, []);
62
63 if (loading) {
64 return <div>Checking authentication...</div>;
65 }
66
67 if (!authState.isAuthenticated) {
68 return <LoginPrompt />;
69 }
70
71 // Pass auth props to component
72 const enhancedProps = {
73 ...props,
74 ...authState
75 } as P;
76
77 return <Component {...enhancedProps} />;
78 };
79}
80
81// HOC with configuration
82interface LoggerConfig {
83 logProps?: boolean;
84 logRenders?: boolean;
85 logMounts?: boolean;
86 prefix?: string;
87}
88
89function withLogger<P extends object>(
90 Component: ComponentType<P>,
91 config: LoggerConfig = {}
92): ComponentType<P> {
93 const {
94 logProps = true,
95 logRenders = true,
96 logMounts = true,
97 prefix = Component.displayName || Component.name || 'Component'
98 } = config;
99
100 const EnhancedComponent = (props: P) => {
101 useEffect(() => {
102 if (logMounts) {
103 console.log(`[${prefix}] Mounted`);
104 }
105
106 return () => {
107 if (logMounts) {
108 console.log(`[${prefix}] Unmounted`);
109 }
110 };
111 }, []);
112
113 useEffect(() => {
114 if (logRenders) {
115 console.log(`[${prefix}] Rendered`);
116 }
117
118 if (logProps) {
119 console.log(`[${prefix}] Props:`, props);
120 }
121 });
122
123 return <Component {...props} />;
124 };
125
126 // Preserve component name for DevTools
127 EnhancedComponent.displayName = `withLogger(${prefix})`;
128
129 return EnhancedComponent;
130}
131
132// Composing multiple HOCs
133function compose<P>(...hocs: Array<(component: ComponentType<any>) => ComponentType<any>>) {
134 return (Component: ComponentType<P>) =>
135 hocs.reduceRight((acc, hoc) => hoc(acc), Component);
136}
137
138// Usage with composition
139const enhance = compose(
140 withAuth,
141 withLogger,
142 withLoading
143);
144
145const EnhancedDashboard = enhance(Dashboard);

Advanced Example

1// HOC for data fetching with caching
2interface CacheEntry<T> {
3 data: T;
4 timestamp: number;
5}
6
7const cache = new Map<string, CacheEntry<any>>();
8
9interface WithDataProps<T> {
10 data: T | null;
11 loading: boolean;
12 error: Error | null;
13 refetch: () => void;
14}
15
16function withData<P extends WithDataProps<T>, T = any>(
17 Component: ComponentType<P>,
18 fetcher: (props: Omit<P, keyof WithDataProps<T>>) => Promise<T>,
19 options: {
20 cacheKey?: (props: Omit<P, keyof WithDataProps<T>>) => string;
21 cacheTime?: number;
22 } = {}
23): ComponentType<Omit<P, keyof WithDataProps<T>>> {
24 const { cacheKey, cacheTime = 5 * 60 * 1000 } = options;
25
26 return (props: Omit<P, keyof WithDataProps<T>>) => {
27 const [state, setState] = useState<WithDataProps<T>>({
28 data: null,
29 loading: true,
30 error: null,
31 refetch: () => {}
32 });
33
34 const fetchData = async () => {
35 setState(prev => ({ ...prev, loading: true, error: null }));
36
37 // Check cache
38 if (cacheKey) {
39 const key = cacheKey(props);
40 const cached = cache.get(key);
41
42 if (cached && Date.now() - cached.timestamp < cacheTime) {
43 setState({
44 data: cached.data,
45 loading: false,
46 error: null,
47 refetch: fetchData
48 });
49 return;
50 }
51 }
52
53 try {
54 const data = await fetcher(props);
55
56 // Update cache
57 if (cacheKey) {
58 cache.set(cacheKey(props), {
59 data,
60 timestamp: Date.now()
61 });
62 }
63
64 setState({
65 data,
66 loading: false,
67 error: null,
68 refetch: fetchData
69 });
70 } catch (error) {
71 setState({
72 data: null,
73 loading: false,
74 error: error as Error,
75 refetch: fetchData
76 });
77 }
78 };
79
80 useEffect(() => {
81 fetchData();
82 }, [JSON.stringify(props)]);
83
84 const enhancedProps = {
85 ...props,
86 ...state
87 } as P;
88
89 return <Component {...enhancedProps} />;
90 };
91}
92
93// HOC for performance monitoring
94function withPerformance<P extends object>(
95 Component: ComponentType<P>,
96 componentName?: string
97): ComponentType<P> {
98 const name = componentName || Component.displayName || Component.name || 'Unknown';
99
100 return React.memo((props: P) => {
101 const renderStartTime = useRef(performance.now());
102
103 useEffect(() => {
104 const renderTime = performance.now() - renderStartTime.current;
105
106 // Log slow renders
107 if (renderTime > 16.67) { // More than one frame
108 console.warn(`[${name}] Slow render: ${renderTime.toFixed(2)}ms`);
109 }
110
111 // Report to analytics
112 if (window.analytics) {
113 window.analytics.track('Component Render', {
114 component: name,
115 renderTime,
116 props: Object.keys(props)
117 });
118 }
119 });
120
121 return <Component {...props} />;
122 });
123}

Provider Pattern

Manage and share global application state using Context API

The Provider Pattern uses React Context to share data and functionality across component trees without prop drilling.

Implementation

1// Provider Pattern - Global state management
2import React, { createContext, useContext, useReducer, ReactNode } from 'react';
3
4// Theme Provider Example
5type Theme = 'light' | 'dark' | 'system';
6type ColorScheme = 'blue' | 'green' | 'purple' | 'orange';
7
8interface ThemeContextType {
9 theme: Theme;
10 colorScheme: ColorScheme;
11 setTheme: (theme: Theme) => void;
12 setColorScheme: (scheme: ColorScheme) => void;
13 toggleTheme: () => void;
14}
15
16const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
17
18export const useTheme = () => {
19 const context = useContext(ThemeContext);
20 if (!context) {
21 throw new Error('useTheme must be used within a ThemeProvider');
22 }
23 return context;
24};
25
26interface ThemeProviderProps {
27 children: ReactNode;
28 defaultTheme?: Theme;
29 defaultColorScheme?: ColorScheme;
30}
31
32export const ThemeProvider: React.FC<ThemeProviderProps> = ({
33 children,
34 defaultTheme = 'system',
35 defaultColorScheme = 'blue'
36}) => {
37 const [theme, setTheme] = useState<Theme>(() => {
38 // Load from localStorage
39 const saved = localStorage.getItem('theme');
40 return (saved as Theme) || defaultTheme;
41 });
42
43 const [colorScheme, setColorScheme] = useState<ColorScheme>(() => {
44 const saved = localStorage.getItem('colorScheme');
45 return (saved as ColorScheme) || defaultColorScheme;
46 });
47
48 // Apply theme to document
49 useEffect(() => {
50 const root = document.documentElement;
51 const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
52 const appliedTheme = theme === 'system' ? systemTheme : theme;
53
54 root.setAttribute('data-theme', appliedTheme);
55 root.setAttribute('data-color-scheme', colorScheme);
56
57 // Save to localStorage
58 localStorage.setItem('theme', theme);
59 localStorage.setItem('colorScheme', colorScheme);
60 }, [theme, colorScheme]);
61
62 const toggleTheme = () => {
63 setTheme(current => {
64 if (current === 'light') return 'dark';
65 if (current === 'dark') return 'system';
66 return 'light';
67 });
68 };
69
70 const value: ThemeContextType = {
71 theme,
72 colorScheme,
73 setTheme,
74 setColorScheme,
75 toggleTheme
76 };
77
78 return (
79 <ThemeContext.Provider value={value}>
80 {children}
81 </ThemeContext.Provider>
82 );
83};
84
85// Complex State Provider with Reducer
86interface AppState {
87 user: User | null;
88 notifications: Notification[];
89 settings: Settings;
90 isLoading: boolean;
91}
92
93type AppAction =
94 | { type: 'SET_USER'; payload: User | null }
95 | { type: 'ADD_NOTIFICATION'; payload: Notification }
96 | { type: 'REMOVE_NOTIFICATION'; payload: string }
97 | { type: 'UPDATE_SETTINGS'; payload: Partial<Settings> }
98 | { type: 'SET_LOADING'; payload: boolean };
99
100const appReducer = (state: AppState, action: AppAction): AppState => {
101 switch (action.type) {
102 case 'SET_USER':
103 return { ...state, user: action.payload };
104
105 case 'ADD_NOTIFICATION':
106 return {
107 ...state,
108 notifications: [...state.notifications, action.payload]
109 };
110
111 case 'REMOVE_NOTIFICATION':
112 return {
113 ...state,
114 notifications: state.notifications.filter(n => n.id !== action.payload)
115 };
116
117 case 'UPDATE_SETTINGS':
118 return {
119 ...state,
120 settings: { ...state.settings, ...action.payload }
121 };
122
123 case 'SET_LOADING':
124 return { ...state, isLoading: action.payload };
125
126 default:
127 return state;
128 }
129};
130
131interface AppContextType {
132 state: AppState;
133 actions: {
134 login: (user: User) => void;
135 logout: () => void;
136 notify: (message: string, type?: 'info' | 'success' | 'error') => void;
137 updateSettings: (settings: Partial<Settings>) => void;
138 };
139}
140
141const AppContext = createContext<AppContextType | undefined>(undefined);
142
143export const useApp = () => {
144 const context = useContext(AppContext);
145 if (!context) {
146 throw new Error('useApp must be used within an AppProvider');
147 }
148 return context;
149};
150
151export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
152 const [state, dispatch] = useReducer(appReducer, {
153 user: null,
154 notifications: [],
155 settings: {
156 language: 'en',
157 notifications: true,
158 autoSave: true
159 },
160 isLoading: false
161 });
162
163 const actions = {
164 login: (user: User) => {
165 dispatch({ type: 'SET_USER', payload: user });
166 dispatch({
167 type: 'ADD_NOTIFICATION',
168 payload: {
169 id: Date.now().toString(),
170 message: 'Welcome back!',
171 type: 'success'
172 }
173 });
174 },
175
176 logout: () => {
177 dispatch({ type: 'SET_USER', payload: null });
178 },
179
180 notify: (message: string, type: 'info' | 'success' | 'error' = 'info') => {
181 const notification: Notification = {
182 id: Date.now().toString(),
183 message,
184 type,
185 timestamp: new Date()
186 };
187
188 dispatch({ type: 'ADD_NOTIFICATION', payload: notification });
189
190 // Auto-remove after 5 seconds
191 setTimeout(() => {
192 dispatch({ type: 'REMOVE_NOTIFICATION', payload: notification.id });
193 }, 5000);
194 },
195
196 updateSettings: (settings: Partial<Settings>) => {
197 dispatch({ type: 'UPDATE_SETTINGS', payload: settings });
198 }
199 };
200
201 return (
202 <AppContext.Provider value={{ state, actions }}>
203 {children}
204 </AppContext.Provider>
205 );
206};

Advanced Example

1// Multi-Provider Pattern
2interface ProvidersProps {
3 children: ReactNode;
4}
5
6export const Providers: React.FC<ProvidersProps> = ({ children }) => {
7 return (
8 <ErrorBoundary>
9 <BrowserRouter>
10 <QueryClientProvider client={queryClient}>
11 <AuthProvider>
12 <ThemeProvider>
13 <AppProvider>
14 <NotificationProvider>
15 {children}
16 </NotificationProvider>
17 </AppProvider>
18 </ThemeProvider>
19 </AuthProvider>
20 </QueryClientProvider>
21 </BrowserRouter>
22 </ErrorBoundary>
23 );
24};
25
26// Feature-specific provider
27interface FeatureFlags {
28 newUI: boolean;
29 betaFeatures: boolean;
30 experimentalAPI: boolean;
31}
32
33interface FeatureFlagContextType {
34 flags: FeatureFlags;
35 isEnabled: (flag: keyof FeatureFlags) => boolean;
36 setFlag: (flag: keyof FeatureFlags, value: boolean) => void;
37}
38
39const FeatureFlagContext = createContext<FeatureFlagContextType | undefined>(undefined);
40
41export const FeatureFlagProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
42 const [flags, setFlags] = useState<FeatureFlags>(() => {
43 // Load from API or config
44 return {
45 newUI: false,
46 betaFeatures: false,
47 experimentalAPI: false
48 };
49 });
50
51 useEffect(() => {
52 // Fetch feature flags from API
53 fetchFeatureFlags().then(setFlags);
54 }, []);
55
56 const isEnabled = (flag: keyof FeatureFlags) => flags[flag];
57
58 const setFlag = (flag: keyof FeatureFlags, value: boolean) => {
59 setFlags(prev => ({ ...prev, [flag]: value }));
60 };
61
62 return (
63 <FeatureFlagContext.Provider value={{ flags, isEnabled, setFlag }}>
64 {children}
65 </FeatureFlagContext.Provider>
66 );
67};
68
69// Usage with feature flags
70const NewFeature = () => {
71 const { isEnabled } = useFeatureFlag();
72
73 if (!isEnabled('newUI')) {
74 return <LegacyFeature />;
75 }
76
77 return <ModernFeature />;
78};

Container/Presentational Pattern

Separate business logic from presentation for cleaner, more testable components

This pattern divides components into containers (smart components) that handle logic and state, and presentational components (dumb components) that handle rendering.

Implementation

1// Container/Presentational Pattern - Separation of concerns
2import React, { useState, useEffect } from 'react';
3
4// Presentational Component - Only handles rendering
5interface UserListProps {
6 users: User[];
7 loading: boolean;
8 error: Error | null;
9 onUserClick: (user: User) => void;
10 onRefresh: () => void;
11 selectedUserId?: string;
12}
13
14const UserList: React.FC<UserListProps> = ({
15 users,
16 loading,
17 error,
18 onUserClick,
19 onRefresh,
20 selectedUserId
21}) => {
22 if (loading) {
23 return (
24 <div className="user-list-skeleton">
25 {[...Array(5)].map((_, i) => (
26 <div key={i} className="skeleton-item" />
27 ))}
28 </div>
29 );
30 }
31
32 if (error) {
33 return (
34 <div className="error-state">
35 <p>Error loading users: {error.message}</p>
36 <button onClick={onRefresh}>Try Again</button>
37 </div>
38 );
39 }
40
41 if (users.length === 0) {
42 return (
43 <div className="empty-state">
44 <p>No users found</p>
45 <button onClick={onRefresh}>Refresh</button>
46 </div>
47 );
48 }
49
50 return (
51 <div className="user-list">
52 <div className="user-list-header">
53 <h2>Users ({users.length})</h2>
54 <button onClick={onRefresh}>Refresh</button>
55 </div>
56
57 <div className="user-grid">
58 {users.map(user => (
59 <UserCard
60 key={user.id}
61 user={user}
62 isSelected={user.id === selectedUserId}
63 onClick={() => onUserClick(user)}
64 />
65 ))}
66 </div>
67 </div>
68 );
69};
70
71// Another presentational component
72interface UserCardProps {
73 user: User;
74 isSelected: boolean;
75 onClick: () => void;
76}
77
78const UserCard: React.FC<UserCardProps> = ({ user, isSelected, onClick }) => (
79 <div
80 className={`user-card ${isSelected ? 'selected' : ''}`}
81 onClick={onClick}
82 >
83 <img src={user.avatar} alt={user.name} />
84 <h3>{user.name}</h3>
85 <p>{user.email}</p>
86 <span className={`status ${user.isActive ? 'active' : 'inactive'}`}>
87 {user.isActive ? 'Active' : 'Inactive'}
88 </span>
89 </div>
90);
91
92// Container Component - Handles all logic and state
93const UserListContainer: React.FC = () => {
94 const [users, setUsers] = useState<User[]>([]);
95 const [loading, setLoading] = useState(true);
96 const [error, setError] = useState<Error | null>(null);
97 const [selectedUserId, setSelectedUserId] = useState<string>();
98 const [filter, setFilter] = useState('');
99
100 const fetchUsers = async () => {
101 setLoading(true);
102 setError(null);
103
104 try {
105 const response = await fetch('/api/users');
106 if (!response.ok) throw new Error('Failed to fetch users');
107
108 const data = await response.json();
109 setUsers(data);
110 } catch (err) {
111 setError(err as Error);
112 } finally {
113 setLoading(false);
114 }
115 };
116
117 useEffect(() => {
118 fetchUsers();
119 }, []);
120
121 const handleUserClick = (user: User) => {
122 setSelectedUserId(user.id);
123 // Could navigate, show details, etc.
124 };
125
126 const filteredUsers = users.filter(user =>
127 user.name.toLowerCase().includes(filter.toLowerCase()) ||
128 user.email.toLowerCase().includes(filter.toLowerCase())
129 );
130
131 return (
132 <div className="user-management">
133 <SearchBar
134 value={filter}
135 onChange={setFilter}
136 placeholder="Search users..."
137 />
138
139 <UserList
140 users={filteredUsers}
141 loading={loading}
142 error={error}
143 onUserClick={handleUserClick}
144 onRefresh={fetchUsers}
145 selectedUserId={selectedUserId}
146 />
147
148 {selectedUserId && (
149 <UserDetailsContainer userId={selectedUserId} />
150 )}
151 </div>
152 );
153};
154
155// Another container for user details
156const UserDetailsContainer: React.FC<{ userId: string }> = ({ userId }) => {
157 const [user, setUser] = useState<UserDetails | null>(null);
158 const [loading, setLoading] = useState(true);
159 const [isEditing, setIsEditing] = useState(false);
160
161 useEffect(() => {
162 fetchUserDetails(userId).then(data => {
163 setUser(data);
164 setLoading(false);
165 });
166 }, [userId]);
167
168 const handleSave = async (updates: Partial<UserDetails>) => {
169 if (!user) return;
170
171 const updated = await updateUser(user.id, updates);
172 setUser(updated);
173 setIsEditing(false);
174 };
175
176 return (
177 <UserDetailsPanel
178 user={user}
179 loading={loading}
180 isEditing={isEditing}
181 onEdit={() => setIsEditing(true)}
182 onCancel={() => setIsEditing(false)}
183 onSave={handleSave}
184 />
185 );
186};

Advanced Example

1// Advanced Container Pattern with Hooks
2// Custom hook for container logic
3const useUserManagement = () => {
4 const [state, setState] = useState({
5 users: [] as User[],
6 loading: true,
7 error: null as Error | null,
8 selectedUser: null as User | null,
9 filter: {
10 search: '',
11 status: 'all' as 'all' | 'active' | 'inactive',
12 role: 'all' as 'all' | 'admin' | 'user'
13 }
14 });
15
16 const actions = {
17 setSearch: (search: string) => {
18 setState(prev => ({
19 ...prev,
20 filter: { ...prev.filter, search }
21 }));
22 },
23
24 setStatusFilter: (status: 'all' | 'active' | 'inactive') => {
25 setState(prev => ({
26 ...prev,
27 filter: { ...prev.filter, status }
28 }));
29 },
30
31 selectUser: (user: User | null) => {
32 setState(prev => ({ ...prev, selectedUser: user }));
33 },
34
35 async deleteUser(userId: string) {
36 try {
37 await api.deleteUser(userId);
38 setState(prev => ({
39 ...prev,
40 users: prev.users.filter(u => u.id !== userId),
41 selectedUser: prev.selectedUser?.id === userId ? null : prev.selectedUser
42 }));
43 } catch (error) {
44 console.error('Failed to delete user:', error);
45 }
46 }
47 };
48
49 // Computed values
50 const filteredUsers = useMemo(() => {
51 return state.users.filter(user => {
52 const matchesSearch =
53 user.name.toLowerCase().includes(state.filter.search.toLowerCase()) ||
54 user.email.toLowerCase().includes(state.filter.search.toLowerCase());
55
56 const matchesStatus =
57 state.filter.status === 'all' ||
58 (state.filter.status === 'active' && user.isActive) ||
59 (state.filter.status === 'inactive' && !user.isActive);
60
61 const matchesRole =
62 state.filter.role === 'all' ||
63 user.role === state.filter.role;
64
65 return matchesSearch && matchesStatus && matchesRole;
66 });
67 }, [state.users, state.filter]);
68
69 return {
70 ...state,
71 filteredUsers,
72 actions
73 };
74};
75
76// Container using the hook
77const UserManagementContainer: React.FC = () => {
78 const {
79 filteredUsers,
80 loading,
81 error,
82 selectedUser,
83 filter,
84 actions
85 } = useUserManagement();
86
87 return (
88 <div className="user-management-layout">
89 <UserFilters
90 filter={filter}
91 onSearchChange={actions.setSearch}
92 onStatusChange={actions.setStatusFilter}
93 />
94
95 <UserTable
96 users={filteredUsers}
97 loading={loading}
98 error={error}
99 selectedUser={selectedUser}
100 onSelectUser={actions.selectUser}
101 onDeleteUser={actions.deleteUser}
102 />
103
104 {selectedUser && (
105 <UserSidebar
106 user={selectedUser}
107 onClose={() => actions.selectUser(null)}
108 />
109 )}
110 </div>
111 );
112};

Custom Hook Pattern

Extract and reuse component logic with custom hooks

Custom hooks allow you to extract component logic into reusable functions, promoting code reuse and separation of concerns.

Implementation

1// Custom Hook Pattern - Reusable logic extraction
2import { useState, useEffect, useCallback, useRef } from 'react';
3
4// Custom hook for form handling
5interface UseFormConfig<T> {
6 initialValues: T;
7 validate?: (values: T) => Partial<Record<keyof T, string>>;
8 onSubmit: (values: T) => void | Promise<void>;
9}
10
11function useForm<T extends Record<string, any>>({
12 initialValues,
13 validate,
14 onSubmit
15}: UseFormConfig<T>) {
16 const [values, setValues] = useState<T>(initialValues);
17 const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
18 const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
19 const [isSubmitting, setIsSubmitting] = useState(false);
20
21 const handleChange = useCallback((
22 e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
23 ) => {
24 const { name, value, type } = e.target;
25
26 setValues(prev => ({
27 ...prev,
28 [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
29 }));
30
31 // Clear error when user types
32 if (errors[name as keyof T]) {
33 setErrors(prev => ({ ...prev, [name]: undefined }));
34 }
35 }, [errors]);
36
37 const handleBlur = useCallback((
38 e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
39 ) => {
40 const { name } = e.target;
41 setTouched(prev => ({ ...prev, [name]: true }));
42
43 // Validate single field
44 if (validate) {
45 const fieldErrors = validate(values);
46 setErrors(prev => ({ ...prev, [name]: fieldErrors[name as keyof T] }));
47 }
48 }, [values, validate]);
49
50 const handleSubmit = useCallback(async (e?: React.FormEvent) => {
51 e?.preventDefault();
52
53 // Mark all fields as touched
54 const allTouched = Object.keys(values).reduce(
55 (acc, key) => ({ ...acc, [key]: true }),
56 {} as Record<keyof T, boolean>
57 );
58 setTouched(allTouched);
59
60 // Validate all fields
61 if (validate) {
62 const validationErrors = validate(values);
63 setErrors(validationErrors);
64
65 if (Object.keys(validationErrors).length > 0) {
66 return;
67 }
68 }
69
70 setIsSubmitting(true);
71 try {
72 await onSubmit(values);
73 // Reset form on successful submit
74 setValues(initialValues);
75 setErrors({});
76 setTouched({});
77 } catch (error) {
78 console.error('Form submission error:', error);
79 } finally {
80 setIsSubmitting(false);
81 }
82 }, [values, validate, onSubmit, initialValues]);
83
84 const reset = useCallback(() => {
85 setValues(initialValues);
86 setErrors({});
87 setTouched({});
88 }, [initialValues]);
89
90 const setFieldValue = useCallback((name: keyof T, value: any) => {
91 setValues(prev => ({ ...prev, [name]: value }));
92 }, []);
93
94 return {
95 values,
96 errors,
97 touched,
98 isSubmitting,
99 handleChange,
100 handleBlur,
101 handleSubmit,
102 reset,
103 setFieldValue,
104 isValid: Object.keys(errors).length === 0
105 };
106}
107
108// Usage example
109const ContactForm = () => {
110 const form = useForm({
111 initialValues: {
112 name: '',
113 email: '',
114 message: ''
115 },
116 validate: (values) => {
117 const errors: Partial<typeof values> = {};
118
119 if (!values.name) errors.name = 'Name is required';
120 if (!values.email) errors.email = 'Email is required';
121 else if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(values.email)) {
122 errors.email = 'Invalid email address';
123 }
124 if (!values.message) errors.message = 'Message is required';
125
126 return errors;
127 },
128 onSubmit: async (values) => {
129 await sendContactForm(values);
130 alert('Form submitted successfully!');
131 }
132 });
133
134 return (
135 <form onSubmit={form.handleSubmit}>
136 <div className="form-group">
137 <input
138 name="name"
139 value={form.values.name}
140 onChange={form.handleChange}
141 onBlur={form.handleBlur}
142 placeholder="Your Name"
143 className={form.errors.name && form.touched.name ? 'error' : ''}
144 />
145 {form.errors.name && form.touched.name && (
146 <span className="error-message">{form.errors.name}</span>
147 )}
148 </div>
149
150 <div className="form-group">
151 <input
152 name="email"
153 type="email"
154 value={form.values.email}
155 onChange={form.handleChange}
156 onBlur={form.handleBlur}
157 placeholder="Your Email"
158 className={form.errors.email && form.touched.email ? 'error' : ''}
159 />
160 {form.errors.email && form.touched.email && (
161 <span className="error-message">{form.errors.email}</span>
162 )}
163 </div>
164
165 <div className="form-group">
166 <textarea
167 name="message"
168 value={form.values.message}
169 onChange={form.handleChange}
170 onBlur={form.handleBlur}
171 placeholder="Your Message"
172 rows={5}
173 className={form.errors.message && form.touched.message ? 'error' : ''}
174 />
175 {form.errors.message && form.touched.message && (
176 <span className="error-message">{form.errors.message}</span>
177 )}
178 </div>
179
180 <div className="form-actions">
181 <button type="submit" disabled={form.isSubmitting || !form.isValid}>
182 {form.isSubmitting ? 'Sending...' : 'Send Message'}
183 </button>
184 <button type="button" onClick={form.reset}>
185 Reset
186 </button>
187 </div>
188 </form>
189 );
190};

Advanced Example

1// Advanced custom hooks composition
2// Pagination hook
3interface UsePaginationConfig {
4 totalItems: number;
5 itemsPerPage?: number;
6 initialPage?: number;
7}
8
9function usePagination({
10 totalItems,
11 itemsPerPage = 10,
12 initialPage = 1
13}: UsePaginationConfig) {
14 const [currentPage, setCurrentPage] = useState(initialPage);
15
16 const totalPages = Math.ceil(totalItems / itemsPerPage);
17
18 const goToPage = useCallback((page: number) => {
19 setCurrentPage(Math.max(1, Math.min(page, totalPages)));
20 }, [totalPages]);
21
22 const nextPage = useCallback(() => {
23 goToPage(currentPage + 1);
24 }, [currentPage, goToPage]);
25
26 const prevPage = useCallback(() => {
27 goToPage(currentPage - 1);
28 }, [currentPage, goToPage]);
29
30 const startIndex = (currentPage - 1) * itemsPerPage;
31 const endIndex = Math.min(startIndex + itemsPerPage, totalItems);
32
33 return {
34 currentPage,
35 totalPages,
36 startIndex,
37 endIndex,
38 goToPage,
39 nextPage,
40 prevPage,
41 hasNextPage: currentPage < totalPages,
42 hasPrevPage: currentPage > 1,
43 pageNumbers: Array.from({ length: totalPages }, (_, i) => i + 1)
44 };
45}
46
47// Combine with data fetching
48function usePaginatedData<T>(
49 fetchFn: (page: number, limit: number) => Promise<{ data: T[]; total: number }>,
50 itemsPerPage = 10
51) {
52 const [data, setData] = useState<T[]>([]);
53 const [total, setTotal] = useState(0);
54 const [loading, setLoading] = useState(true);
55 const [error, setError] = useState<Error | null>(null);
56
57 const pagination = usePagination({
58 totalItems: total,
59 itemsPerPage
60 });
61
62 const loadPage = useCallback(async (page: number) => {
63 setLoading(true);
64 setError(null);
65
66 try {
67 const result = await fetchFn(page, itemsPerPage);
68 setData(result.data);
69 setTotal(result.total);
70 } catch (err) {
71 setError(err as Error);
72 } finally {
73 setLoading(false);
74 }
75 }, [fetchFn, itemsPerPage]);
76
77 useEffect(() => {
78 loadPage(pagination.currentPage);
79 }, [pagination.currentPage]);
80
81 return {
82 data,
83 loading,
84 error,
85 pagination,
86 refresh: () => loadPage(pagination.currentPage)
87 };
88}
89
90// Usage
91const UserTable = () => {
92 const { data, loading, error, pagination } = usePaginatedData(
93 async (page, limit) => {
94 const response = await fetch(`/api/users?page=${page}&limit=${limit}`);
95 return response.json();
96 },
97 20
98 );
99
100 if (loading) return <LoadingSpinner />;
101 if (error) return <ErrorMessage error={error} />;
102
103 return (
104 <div>
105 <table>
106 <thead>
107 <tr>
108 <th>Name</th>
109 <th>Email</th>
110 <th>Role</th>
111 </tr>
112 </thead>
113 <tbody>
114 {data.map(user => (
115 <tr key={user.id}>
116 <td>{user.name}</td>
117 <td>{user.email}</td>
118 <td>{user.role}</td>
119 </tr>
120 ))}
121 </tbody>
122 </table>
123
124 <Pagination {...pagination} />
125 </div>
126 );
127};

Advertisement Space - mid-patterns

Google AdSense: rectangle

Pattern Comparison Guide

PatternBest Use CaseProsConsExamples
Compound ComponentsComplex UI components with multiple parts
  • Flexible API
  • Implicit state sharing
  • Clean component interface
  • More complex setup
  • Harder to type with TypeScript
Accordions, Tabs, Modals
Render PropsSharing cross-cutting behavior between components
  • Explicit data flow
  • Dynamic composition
  • No namespace collision
  • Verbose syntax
  • Wrapper hell with multiple render props
Mouse tracking, Data fetching, Animation
Higher-Order ComponentsAdding behavior to existing components
  • Reusable logic
  • Works with class components
  • Can compose multiple HOCs
  • Props collision
  • Wrapper hell
  • Hard to debug
Authentication, Logging, Data fetching
Provider PatternGlobal state management and dependency injection
  • Avoids prop drilling
  • Centralized state
  • Good for app-wide concerns
  • Can cause unnecessary re-renders
  • Context value changes affect all consumers
Theme, Auth, App settings
Container/PresentationalSeparating logic from presentation
  • Testable components
  • Reusable UI
  • Clear separation of concerns
  • More files/components
  • Can be over-engineered for simple cases
Data tables, Forms, Complex UI sections
Custom HooksExtracting and sharing component logic
  • Composable
  • Testable
  • Works with function components
  • Clean syntax
  • Only works with hooks
  • Rules of hooks apply
Form handling, API calls, Browser APIs

Pattern Selection Guidelines

When to Use Compound Components

  • • Building a family of related components
  • • Need flexible component composition
  • • Want to avoid prop drilling within component group
  • • Creating reusable UI libraries

When to Use Custom Hooks

  • • Extracting stateful logic from components
  • • Sharing logic between multiple components
  • • Working with browser APIs or subscriptions
  • • Building on top of existing hooks

When to Use HOCs

  • • Adding behavior to third-party components
  • • Working with class components
  • • Need to manipulate props before passing
  • • Cross-cutting concerns like logging

When to Use Render Props

  • • Need maximum flexibility in rendering
  • • Sharing complex state logic
  • • Building highly customizable components
  • • Avoiding HOC limitations

Advertisement Space - bottom-patterns

Google AdSense: horizontal

Continue Learning

Master React Component Patterns

Apply these patterns to build better React applications with cleaner, more maintainable code.

Explore React Architecture →