JavaScript and React course Β· Module 8: Advanced React Patterns
Design patterns and logic reusability
In this lesson5
On our cosmic journey through the world of React, efficient use of resources is key to mission success. Just as space engineers use proven designs for different spaceship modules, we too can leverage design patterns in React to maximize code reusability and create more scalable applications.
Rendering and composition patterns
1. Higher-Order Components (HOC)
Higher-Order Components (HOC) are functions that take a component and return a new, enhanced component. This is a pattern based on the principle of composition, not inheritance.
1// Simple HOC adding time tracking functionality
2function withTimer(WrappedComponent) {
3 return function WithTimer(props) {
4 const [startTime] = useState(Date.now());
5 const [currentTime, setCurrentTime] = useState(Date.now());
6
7 useEffect(() => {
8 const timer = setInterval(() => {
9 setCurrentTime(Date.now());
10 }, 1000);
11
12 return () => clearInterval(timer);
13 }, []);
14
15 const elapsedTime = (currentTime - startTime) / 1000;
16
17 return (
18 <WrappedComponent
19 {...props}
20 elapsedTime={elapsedTime}
21 />
22 );
23 };
24}
25
26// Using the HOC
27function MissionControl({ missionName, elapsedTime }) {
28 return (
29 <div className="mission-control">
30 <h2>Mission: {missionName}</h2>
31 <p>Duration: {elapsedTime.toFixed(0)} seconds</p>
32 {/* rest of component */}
33 </div>
34 );
35}
36
37const MissionControlWithTimer = withTimer(MissionControl);
38
39// Rendering the enhanced component
40function App() {
41 return <MissionControlWithTimer missionName="Apollo" />;
42}2. Render Props
Render Props is a pattern where a component receives a function that tells it what to render. This allows logic to be shared between components.
1function MouseTracker({ render }) {
2 const [position, setPosition] = useState({ x: 0, y: 0 });
3
4 useEffect(() => {
5 function handleMouseMove(event) {
6 setPosition({
7 x: event.clientX,
8 y: event.clientY
9 });
10 }
11
12 window.addEventListener('mousemove', handleMouseMove);
13
14 return () => {
15 window.removeEventListener('mousemove', handleMouseMove);
16 };
17 }, []);
18
19 return render(position);
20}
21
22// Using render props
23function App() {
24 return (
25 <MouseTracker
26 render={({ x, y }) => (
27 <div className="space-map">
28 <h2>Star Map</h2>
29 <p>Cursor coordinates: ({x}, {y})</p>
30 <div
31 className="space-ship"
32 style={{
33 position: 'absolute',
34 left: x,
35 top: y
36 }}
37 >
38 β²
39 </div>
40 </div>
41 )}
42 />
43 );
44}3. Compound Components Pattern
This pattern allows you to create components that work together while maintaining flexibility and code readability.
1function Spaceship({ children }) {
2 const [power, setPower] = useState(0);
3 const [shields, setShields] = useState(false);
4
5 const contextValue = {
6 power,
7 setPower,
8 shields,
9 setShields
10 };
11
12 return (
13 <SpaceshipContext.Provider value={contextValue}>
14 <div className="spaceship">
15 {children}
16 </div>
17 </SpaceshipContext.Provider>
18 );
19}
20
21// Child components
22Spaceship.Engine = function Engine() {
23 const { power, setPower } = useContext(SpaceshipContext);
24
25 return (
26 <div className="engine">
27 <h3>Engine</h3>
28 <p>Power: {power}%</p>
29 <input
30 type="range"
31 min="0"
32 max="100"
33 value={power}
34 onChange={e => setPower(Number(e.target.value))}
35 />
36 </div>
37 );
38};
39
40Spaceship.Shields = function Shields() {
41 const { shields, setShields } = useContext(SpaceshipContext);
42
43 return (
44 <div className="shields">
45 <h3>Shields</h3>
46 <button onClick={() => setShields(!shields)}>
47 {shields ? 'Deactivate' : 'Activate'} shields
48 </button>
49 <p>Status: {shields ? 'Active' : 'Inactive'}</p>
50 </div>
51 );
52};
53
54// Usage
55function App() {
56 return (
57 <Spaceship>
58 <h2>Spaceship Control Panel</h2>
59 <Spaceship.Engine />
60 <Spaceship.Shields />
61 </Spaceship>
62 );
63}State management patterns
1. Flux / Redux
Redux implements the Flux pattern, which assumes a unidirectional data flow. The application state is stored in a single object (store), and state modifications can only occur by dispatching actions that are processed by reducers.
1// Actions
2const ADD_CREWMEMBER = 'ADD_CREWMEMBER';
3const REMOVE_CREWMEMBER = 'REMOVE_CREWMEMBER';
4
5// Action creators
6function addCrewmember(member) {
7 return {
8 type: ADD_CREWMEMBER,
9 payload: member
10 };
11}
12
13function removeCrewmember(id) {
14 return {
15 type: REMOVE_CREWMEMBER,
16 payload: id
17 };
18}
19
20// Reducer
21function crewReducer(state = [], action) {
22 switch (action.type) {
23 case ADD_CREWMEMBER:
24 return [...state, action.payload];
25 case REMOVE_CREWMEMBER:
26 return state.filter(member => member.id !== action.payload);
27 default:
28 return state;
29 }
30}
31
32// In a component with react-redux
33function CrewManagement() {
34 const crew = useSelector(state => state.crew);
35 const dispatch = useDispatch();
36
37 function handleAddCrewmember() {
38 const newMember = {
39 id: Date.now(),
40 name: 'New crew member',
41 role: 'Cadet'
42 };
43 dispatch(addCrewmember(newMember));
44 }
45
46 return (
47 <div>
48 <h2>Crew Management</h2>
49 <button onClick={handleAddCrewmember}>Add crew member</button>
50 <ul>
51 {crew.map(member => (
52 <li key={member.id}>
53 {member.name} - {member.role}
54 <button onClick={() => dispatch(removeCrewmember(member.id))}>
55 Remove
56 </button>
57 </li>
58 ))}
59 </ul>
60 </div>
61 );
62}2. Context API
Context API allows passing data through the component tree without the need to pass props at every level.
1// Creating context
2const MissionContext = createContext();
3
4// Provider
5function MissionProvider({ children }) {
6 const [mission, setMission] = useState({
7 name: 'Apollo 11',
8 status: 'In progress',
9 crewMembers: [
10 { id: 1, name: 'Neil Armstrong', role: 'Commander' },
11 { id: 2, name: 'Buzz Aldrin', role: 'Lunar Module Pilot' },
12 { id: 3, name: 'Michael Collins', role: 'Command Module Pilot' }
13 ]
14 });
15
16 function updateMissionStatus(newStatus) {
17 setMission(prev => ({ ...prev, status: newStatus }));
18 }
19
20 function addCrewMember(member) {
21 setMission(prev => ({
22 ...prev,
23 crewMembers: [...prev.crewMembers, member]
24 }));
25 }
26
27 const value = {
28 mission,
29 updateMissionStatus,
30 addCrewMember
31 };
32
33 return (
34 <MissionContext.Provider value={value}>
35 {children}
36 </MissionContext.Provider>
37 );
38}
39
40// Custom hook for easier usage
41function useMission() {
42 const context = useContext(MissionContext);
43 if (!context) {
44 throw new Error('useMission must be used within a MissionProvider');
45 }
46 return context;
47}
48
49// Usage in components
50function MissionDashboard() {
51 const { mission } = useMission();
52
53 return (
54 <div>
55 <h1>Mission: {mission.name}</h1>
56 <p>Status: {mission.status}</p>
57 </div>
58 );
59}
60
61function MissionControl() {
62 const { updateMissionStatus } = useMission();
63
64 return (
65 <div>
66 <h2>Mission Control Center</h2>
67 <button onClick={() => updateMissionStatus('Completed successfully')}>
68 Complete mission
69 </button>
70 <button onClick={() => updateMissionStatus('Aborted')}>
71 Abort mission
72 </button>
73 </div>
74 );
75}
76
77function CrewList() {
78 const { mission, addCrewMember } = useMission();
79
80 return (
81 <div>
82 <h2>Mission Crew</h2>
83 <ul>
84 {mission.crewMembers.map(member => (
85 <li key={member.id}>
86 {member.name} - {member.role}
87 </li>
88 ))}
89 </ul>
90 <button
91 onClick={() =>
92 addCrewMember({
93 id: Date.now(),
94 name: 'New astronaut',
95 role: 'Mission Specialist'
96 })
97 }
98 >
99 Add crew member
100 </button>
101 </div>
102 );
103}
104
105// Putting it all together
106function App() {
107 return (
108 <MissionProvider>
109 <div className="mission-app">
110 <MissionDashboard />
111 <MissionControl />
112 <CrewList />
113 </div>
114 </MissionProvider>
115 );
116}3. Custom hooks for state
As we saw in the previous module, custom hooks are a powerful tool for encapsulating and reusing state-related logic.
1function useSpaceshipSystems() {
2 const [engines, setEngines] = useState({
3 mainEngine: { power: 0, status: 'offline' },
4 leftThruster: { power: 0, status: 'offline' },
5 rightThruster: { power: 0, status: 'offline' }
6 });
7
8 const [lifeSupportSystem, setLifeSupportSystem] = useState({
9 oxygen: 100,
10 temperature: 21,
11 pressure: 1
12 });
13
14 function startEngine(engineName) {
15 setEngines(prev => ({
16 ...prev,
17 [engineName]: {
18 ...prev[engineName],
19 status: 'online',
20 power: 10
21 }
22 }));
23 }
24
25 function setEnginePower(engineName, power) {
26 if (engines[engineName].status === 'offline') {
27 console.warn(`Cannot set power - engine ${engineName} is offline`);
28 return;
29 }
30
31 setEngines(prev => ({
32 ...prev,
33 [engineName]: {
34 ...prev[engineName],
35 power: Math.max(0, Math.min(100, power))
36 }
37 }));
38 }
39
40 function stopEngine(engineName) {
41 setEngines(prev => ({
42 ...prev,
43 [engineName]: {
44 ...prev[engineName],
45 status: 'offline',
46 power: 0
47 }
48 }));
49 }
50
51 function adjustLifeSupport({ oxygen, temperature, pressure }) {
52 setLifeSupportSystem(prev => ({
53 ...prev,
54 ...(oxygen !== undefined ? { oxygen } : {}),
55 ...(temperature !== undefined ? { temperature } : {}),
56 ...(pressure !== undefined ? { pressure } : {})
57 }));
58 }
59
60 return {
61 engines,
62 lifeSupportSystem,
63 startEngine,
64 setEnginePower,
65 stopEngine,
66 adjustLifeSupport
67 };
68}
69
70// Usage
71function SpaceshipControls() {
72 const {
73 engines,
74 lifeSupportSystem,
75 startEngine,
76 setEnginePower,
77 stopEngine,
78 adjustLifeSupport
79 } = useSpaceshipSystems();
80
81 return (
82 <div className="spaceship-controls">
83 <h2>Ship Systems Control</h2>
84
85 <div className="engines-section">
86 <h3>Engines</h3>
87 {Object.entries(engines).map(([name, engine]) => (
88 <div key={name} className="engine-control">
89 <h4>{formatEngineName(name)}</h4>
90 <p>Status: {engine.status}</p>
91 <p>Power: {engine.power}%</p>
92
93 {engine.status === 'offline' ? (
94 <button onClick={() => startEngine(name)}>Start</button>
95 ) : (
96 <>
97 <input
98 type="range"
99 min="0"
100 max="100"
101 value={engine.power}
102 onChange={e => setEnginePower(name, Number(e.target.value))}
103 />
104 <button onClick={() => stopEngine(name)}>Stop</button>
105 </>
106 )}
107 </div>
108 ))}
109 </div>
110
111 <div className="life-support">
112 <h3>Life Support System</h3>
113 <p>Oxygen level: {lifeSupportSystem.oxygen}%</p>
114 <p>Temperature: {lifeSupportSystem.temperature}Β°C</p>
115 <p>Pressure: {lifeSupportSystem.pressure} atm</p>
116
117 <div className="controls">
118 <button
119 onClick={() =>
120 adjustLifeSupport({
121 oxygen: lifeSupportSystem.oxygen + 5
122 })
123 }
124 >
125 Increase oxygen level
126 </button>
127 <button
128 onClick={() =>
129 adjustLifeSupport({
130 temperature: lifeSupportSystem.temperature - 1
131 })
132 }
133 >
134 Lower temperature
135 </button>
136 </div>
137 </div>
138 </div>
139 );
140}
141
142function formatEngineName(name) {
143 return name
144 .replace(/([A-Z])/g, ' $1')
145 .replace(/^./, str => str.toUpperCase());
146}UI component patterns
1. Container / Presentational Pattern
This pattern separates business logic (container) from presentation (presentational component), which increases component reusability and simplifies testing.
1// Container component - handles logic and data fetching
2function PlanetDataContainer({ planetId, children }) {
3 const [planet, setPlanet] = useState(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState(null);
6
7 useEffect(() => {
8 setLoading(true);
9
10 fetch(`https://api.planets.com/${planetId}`)
11 .then(response => {
12 if (!response.ok) throw new Error('Failed to fetch planet data');
13 return response.json();
14 })
15 .then(data => {
16 setPlanet(data);
17 setLoading(false);
18 })
19 .catch(err => {
20 setError(err.message);
21 setLoading(false);
22 });
23 }, [planetId]);
24
25 // Call children function with data
26 return children({ planet, loading, error });
27}
28
29// Presentational component - only handles display
30function PlanetInfo({ planet, loading, error }) {
31 if (loading) return <div>Loading planet data...</div>;
32 if (error) return <div>Error: {error}</div>;
33 if (!planet) return null;
34
35 return (
36 <div className="planet-info">
37 <h2>{planet.name}</h2>
38 <div className="planet-details">
39 <p>Type: {planet.type}</p>
40 <p>Diameter: {planet.diameter} km</p>
41 <p>Distance from Sun: {planet.distanceFromSun} million km</p>
42 <p>Orbital period: {planet.orbitalPeriod} days</p>
43 <p>Surface temperature: {planet.surfaceTemperature}Β°C</p>
44 </div>
45
46 {planet.moons && planet.moons.length > 0 && (
47 <div className="moons">
48 <h3>Moons ({planet.moons.length}):</h3>
49 <ul>
50 {planet.moons.map(moon => (
51 <li key={moon.id}>{moon.name}</li>
52 ))}
53 </ul>
54 </div>
55 )}
56 </div>
57 );
58}
59
60// Usage
61function SolarSystemExplorer() {
62 const [selectedPlanet, setSelectedPlanet] = useState('earth');
63
64 return (
65 <div className="solar-system-explorer">
66 <h1>Solar System Exploration</h1>
67
68 <div className="planet-selector">
69 <label>Select a planet: </label>
70 <select
71 value={selectedPlanet}
72 onChange={e => setSelectedPlanet(e.target.value)}
73 >
74 <option value="mercury">Mercury</option>
75 <option value="venus">Venus</option>
76 <option value="earth">Earth</option>
77 <option value="mars">Mars</option>
78 <option value="jupiter">Jupiter</option>
79 <option value="saturn">Saturn</option>
80 <option value="uranus">Uranus</option>
81 <option value="neptune">Neptune</option>
82 </select>
83 </div>
84
85 <PlanetDataContainer planetId={selectedPlanet}>
86 {({ planet, loading, error }) => (
87 <PlanetInfo planet={planet} loading={loading} error={error} />
88 )}
89 </PlanetDataContainer>
90 </div>
91 );
92}2. Composition vs Inheritance
React prefers composition over inheritance. This pattern allows creating flexible components through nesting and passing props.
1// Base component - Card
2function Card({ title, children, footer, className }) {
3 return (
4 <div className={`card ${className || ''}`}>
5 {title && <div className="card-header">{title}</div>}
6 <div className="card-body">{children}</div>
7 {footer && <div className="card-footer">{footer}</div>}
8 </div>
9 );
10}
11
12// Specialized components using composition
13function MissionCard({ mission, onComplete }) {
14 return (
15 <Card
16 title={<h3>{mission.name}</h3>}
17 footer={
18 <button onClick={() => onComplete(mission.id)}>
19 Complete mission
20 </button>
21 }
22 className="mission-card"
23 >
24 <p><strong>Objective:</strong> {mission.objective}</p>
25 <p><strong>Status:</strong> {mission.status}</p>
26 <p><strong>Progress:</strong> {mission.progress}%</p>
27 <div className="progress-bar">
28 <div
29 className="progress"
30 style={{ width: `${mission.progress}%` }}
31 />
32 </div>
33 </Card>
34 );
35}
36
37function AstronautCard({ astronaut, onAssign }) {
38 return (
39 <Card
40 title={<h3>{astronaut.name}</h3>}
41 footer={
42 <button onClick={() => onAssign(astronaut.id)}>
43 Assign to mission
44 </button>
45 }
46 className="astronaut-card"
47 >
48 <div className="astronaut-info">
49 <img
50 src={astronaut.avatar}
51 alt={astronaut.name}
52 className="astronaut-avatar"
53 />
54 <div>
55 <p><strong>Rank:</strong> {astronaut.rank}</p>
56 <p><strong>Specialization:</strong> {astronaut.specialization}</p>
57 <p><strong>Missions:</strong> {astronaut.missionCount}</p>
58 </div>
59 </div>
60 </Card>
61 );
62}
63
64// Usage
65function Dashboard() {
66 const missions = [
67 { id: 1, name: 'Mars Exploration', objective: 'Explore the surface', status: 'In progress', progress: 65 },
68 { id: 2, name: 'ISS Space Station', objective: 'Module maintenance', status: 'Planned', progress: 0 }
69 ];
70
71 const astronauts = [
72 { id: 1, name: 'John Smith', rank: 'Captain', specialization: 'Pilot', missionCount: 5, avatar: '/avatars/js.png' },
73 { id: 2, name: 'Anna Johnson', rank: 'Lieutenant', specialization: 'Engineer', missionCount: 3, avatar: '/avatars/aj.png' }
74 ];
75
76 return (
77 <div className="dashboard">
78 <h1>Space Center Control Panel</h1>
79
80 <section className="missions-section">
81 <h2>Active Missions</h2>
82 <div className="missions-grid">
83 {missions.map(mission => (
84 <MissionCard
85 key={mission.id}
86 mission={mission}
87 onComplete={id => console.log(`Completed mission ${id}`)}
88 />
89 ))}
90 </div>
91 </section>
92
93 <section className="astronauts-section">
94 <h2>Available Astronauts</h2>
95 <div className="astronauts-grid">
96 {astronauts.map(astronaut => (
97 <AstronautCard
98 key={astronaut.id}
99 astronaut={astronaut}
100 onAssign={id => console.log(`Assigned astronaut ${id}`)}
101 />
102 ))}
103 </div>
104 </section>
105 </div>
106 );
107}3. Headless Components Pattern
This pattern separates the behavioral logic of a component from its appearance, allowing maximum styling flexibility.
1// Headless component - contains only logic, no UI
2function useTabControl(initialTab = 0) {
3 const [activeTab, setActiveTab] = useState(initialTab);
4
5 function activateTab(index) {
6 setActiveTab(index);
7 }
8
9 return {
10 activeTab,
11 activateTab
12 };
13}
14
15// Usage with any UI
16function MissionTabs() {
17 const tabs = [
18 { id: 'overview', label: 'Overview' },
19 { id: 'crew', label: 'Crew' },
20 { id: 'timeline', label: 'Timeline' },
21 { id: 'systems', label: 'Systems' }
22 ];
23
24 const { activeTab, activateTab } = useTabControl();
25
26 return (
27 <div className="mission-tabs">
28 <div className="tabs-header">
29 {tabs.map((tab, index) => (
30 <button
31 key={tab.id}
32 className={`tab-button ${activeTab === index ? 'active' : ''}`}
33 onClick={() => activateTab(index)}
34 >
35 {tab.label}
36 </button>
37 ))}
38 </div>
39
40 <div className="tab-content">
41 {activeTab === 0 && (
42 <div className="overview-tab">
43 <h3>Mission Overview</h3>
44 <p>The Apollo 11 mission was the first crewed mission to the Moon...</p>
45 </div>
46 )}
47
48 {activeTab === 1 && (
49 <div className="crew-tab">
50 <h3>Crew</h3>
51 <ul>
52 <li>Neil Armstrong - Commander</li>
53 <li>Edwin "Buzz" Aldrin - Lunar Module Pilot</li>
54 <li>Michael Collins - Command Module Pilot</li>
55 </ul>
56 </div>
57 )}
58
59 {activeTab === 2 && (
60 <div className="timeline-tab">
61 <h3>Timeline</h3>
62 <div className="timeline">
63 <div className="timeline-event">
64 <span className="date">July 16, 1969</span>
65 <span className="description">Launch from Cape Kennedy</span>
66 </div>
67 <div className="timeline-event">
68 <span className="date">July 20, 1969</span>
69 <span className="description">Moon landing</span>
70 </div>
71 <div className="timeline-event">
72 <span className="date">July 21, 1969</span>
73 <span className="description">First step on the Moon</span>
74 </div>
75 <div className="timeline-event">
76 <span className="date">July 24, 1969</span>
77 <span className="description">Return to Earth</span>
78 </div>
79 </div>
80 </div>
81 )}
82
83 {activeTab === 3 && (
84 <div className="systems-tab">
85 <h3>Ship Systems</h3>
86 <p>Command Module: Columbia</p>
87 <p>Lunar Module: Eagle</p>
88 <p>Launch Vehicle: Saturn V</p>
89 </div>
90 )}
91 </div>
92 </div>
93 );
94}Optimization patterns
1. Memoization
Memoization helps avoid unnecessary renders by remembering the results of expensive computations.
1// Component memoization
2const MemoizedTelemetryChart = React.memo(function TelemetryChart({ data }) {
3 // Simulation of expensive computations
4 const processedData = useMemo(() => {
5 console.log('Processing telemetry data...');
6
7 // Expensive data processing
8 return data.map(point => ({
9 x: new Date(point.timestamp).toLocaleTimeString(),
10 y: calculateAverage(point.readings),
11 anomaly: detectAnomaly(point.readings)
12 }));
13 }, [data]); // Recalculate only when data changes
14
15 return (
16 <div className="telemetry-chart">
17 <h3>Telemetry Chart</h3>
18 <div className="chart-container">
19 {/* Chart with processed data */}
20 {processedData.map((point, index) => (
21 <div
22 key={index}
23 className={`data-point ${point.anomaly ? 'anomaly' : ''}`}
24 style={{
25 height: `${point.y * 2}px`,
26 backgroundColor: point.anomaly ? 'red' : 'blue'
27 }}
28 title={`${point.x}: ${point.y} ${point.anomaly ? '(anomaly)' : ''}`}
29 />
30 ))}
31 </div>
32 </div>
33 );
34}, (prevProps, nextProps) => {
35 // Custom props comparison logic
36 if (prevProps.data.length !== nextProps.data.length) {
37 return false; // re-render if data size changed
38 }
39
40 // Compare only the last 5 data points
41 const prevLastFive = prevProps.data.slice(-5);
42 const nextLastFive = nextProps.data.slice(-5);
43
44 for (let i = 0; i < 5; i++) {
45 if (!prevLastFive[i] || !nextLastFive[i]) continue;
46
47 if (
48 prevLastFive[i].timestamp !== nextLastFive[i].timestamp ||
49 !arraysEqual(prevLastFive[i].readings, nextLastFive[i].readings)
50 ) {
51 return false; // re-render if data differs
52 }
53 }
54
55 return true; // don't re-render
56});
57
58// Helper functions
59function calculateAverage(readings) {
60 return readings.reduce((sum, val) => sum + val, 0) / readings.length;
61}
62
63function detectAnomaly(readings) {
64 const avg = calculateAverage(readings);
65 const threshold = avg * 1.5;
66 return readings.some(val => val > threshold);
67}
68
69function arraysEqual(a, b) {
70 if (a.length !== b.length) return false;
71 for (let i = 0; i < a.length; i++) {
72 if (a[i] !== b[i]) return false;
73 }
74 return true;
75}2. List virtualization
For large data lists, virtualization allows rendering only the elements that are currently visible on screen.
1// Simple virtualization implementation (in real applications
2// it's worth using libraries like react-window or react-virtualized)
3function VirtualizedAsteroidList({ asteroids, rowHeight = 50, visibleRows = 10 }) {
4 const [scrollTop, setScrollTop] = useState(0);
5 const containerRef = useRef();
6
7 const totalHeight = asteroids.length * rowHeight;
8 const startIndex = Math.floor(scrollTop / rowHeight);
9 const endIndex = Math.min(
10 startIndex + visibleRows + 1,
11 asteroids.length
12 );
13
14 const visibleAsteroids = useMemo(() => {
15 return asteroids.slice(startIndex, endIndex);
16 }, [asteroids, startIndex, endIndex]);
17
18 const handleScroll = useCallback(() => {
19 if (containerRef.current) {
20 setScrollTop(containerRef.current.scrollTop);
21 }
22 }, []);
23
24 return (
25 <div className="asteroids-list">
26 <h2>Asteroid Catalog</h2>
27 <p>Number of asteroids: {asteroids.length}</p>
28
29 <div
30 ref={containerRef}
31 className="virtual-list-container"
32 style={{ height: rowHeight * visibleRows, overflow: 'auto' }}
33 onScroll={handleScroll}
34 >
35 <div
36 className="total-height"
37 style={{ height: totalHeight }}
38 >
39 <div
40 className="visible-items"
41 style={{
42 transform: `translateY(${startIndex * rowHeight}px)`
43 }}
44 >
45 {visibleAsteroids.map(asteroid => (
46 <div
47 key={asteroid.id}
48 className="asteroid-item"
49 style={{ height: rowHeight }}
50 >
51 <span className="name">{asteroid.name}</span>
52 <span className="diameter">Diameter: {asteroid.diameter} km</span>
53 <span className="distance">
54 Distance: {asteroid.distance} million km
55 </span>
56 <span
57 className={`hazard-level ${asteroid.hazardLevel}`}
58 >
59 {formatHazardLevel(asteroid.hazardLevel)}
60 </span>
61 </div>
62 ))}
63 </div>
64 </div>
65 </div>
66 </div>
67 );
68}
69
70function formatHazardLevel(level) {
71 switch (level) {
72 case 'high': return 'High threat';
73 case 'medium': return 'Medium threat';
74 case 'low': return 'Low threat';
75 default: return 'No threat';
76 }
77}3. Lazy loading
Lazy loading of components allows deferring the loading of parts of the application until they are needed, which can significantly speed up the initial load of the application.
1// Lazy loading components
2const LazyMarsExplorer = lazy(() => import('./MarsExplorer'));
3const LazyJupiterMoons = lazy(() => import('./JupiterMoons'));
4const LazySaturnRings = lazy(() => import('./SaturnRings'));
5
6function SolarSystemExplorer() {
7 const [activeSection, setActiveSection] = useState(null);
8
9 return (
10 <div className="solar-system-explorer">
11 <h1>Solar System Exploration</h1>
12
13 <div className="planet-selector">
14 <button onClick={() => setActiveSection('mars')}>
15 Mars Exploration
16 </button>
17 <button onClick={() => setActiveSection('jupiter')}>
18 Jupiter's Moons
19 </button>
20 <button onClick={() => setActiveSection('saturn')}>
21 Saturn's Rings
22 </button>
23 </div>
24
25 <Suspense fallback={<div className="loading">Loading exploration module...</div>}>
26 {activeSection === 'mars' && <LazyMarsExplorer />}
27 {activeSection === 'jupiter' && <LazyJupiterMoons />}
28 {activeSection === 'saturn' && <LazySaturnRings />}
29 </Suspense>
30 </div>
31 );
32}Summary
Design patterns in React provide proven solutions for common programming problems. They allow you to create code that is:
- Reusable - components and logic can be easily used across different parts of the application
- Testable - separation of concerns makes writing tests easier
- Scalable - the application can grow without a drastic increase in complexity
- Maintainable - clear structure and organization make it easier to introduce changes
On our cosmic journey through React, these patterns are like proven interstellar routes that allow you to reach your destination faster and more safely. Experiment with different patterns and choose those that best fit the specific needs of your application.
Code for this lesson: App.jsx
1import React, { useState, useEffect, useCallback } from 'react';
2
3// Custom Hook: useLocalStorage
4function useLocalStorage(key, initialValue) {
5 // State for storing values
6 const [storedValue, setStoredValue] = useState(() => {
7 try {
8 const item = window.localStorage.getItem(key);
9 return item ? JSON.parse(item) : initialValue;
10 } catch (error) {
11 console.log('Error reading localStorage:', error);
12 return initialValue;
13 }
14 });
15
16 // Function for updating values
17 const setValue = useCallback((value) => {
18 setStoredValue((prev) => (value instanceof Function ? value(prev) : value));
19 }, []);
20
21 // Save to localStorage on every change
22 useEffect(() => {
23 try {
24 window.localStorage.setItem(key, JSON.stringify(storedValue));
25 } catch (error) {
26 console.log('Error saving to localStorage:', error);
27 }
28 }, [key, storedValue]);
29
30 return [storedValue, setValue];
31}
32
33// Custom Hook: useToggle
34function useToggle(initialValue = false) {
35 const [value, setValue] = useState(initialValue);
36
37 const toggle = useCallback(() => {
38 setValue(v => !v);
39 }, []);
40
41 const setTrue = useCallback(() => {
42 setValue(true);
43 }, []);
44
45 const setFalse = useCallback(() => {
46 setValue(false);
47 }, []);
48
49 return { value, toggle, setTrue, setFalse };
50}
51
52// Custom Hook: useCounter
53function useCounter(initialValue = 0, step = 1) {
54 const [count, setCount] = useState(initialValue);
55
56 const increment = useCallback(() => {
57 setCount(c => c + step);
58 }, [step]);
59
60 const decrement = useCallback(() => {
61 setCount(c => c - step);
62 }, [step]);
63
64 const reset = useCallback(() => {
65 setCount(initialValue);
66 }, [initialValue]);
67
68 return { count, increment, decrement, reset, setCount };
69}
70
71// Custom hooks demonstration
72function CustomHooksDemo() {
73 // useLocalStorage - storing settings
74 const [username, setUsername] = useLocalStorage('username', '');
75 const [settings, setSettings] = useLocalStorage('settings', {
76 darkMode: true,
77 notifications: true,
78 language: 'pl'
79 });
80
81 // useToggle - toggles
82 const darkMode = useToggle(settings.darkMode);
83 const showPanel = useToggle(false);
84 const isEditing = useToggle(false);
85
86 // useCounter - counters
87 const visits = useCounter(0);
88 const score = useCounter(100, 10);
89
90 // Effect for synchronizing with localStorage
91 useEffect(() => {
92 setSettings(prev => ({ ...prev, darkMode: darkMode.value }));
93 }, [darkMode.value, setSettings]);
94
95 const containerStyle = {
96 minHeight: '100vh',
97 background: darkMode.value
98 ? 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)'
99 : 'linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)',
100 padding: '30px',
101 fontFamily: "'Segoe UI', sans-serif",
102 color: darkMode.value ? '#fff' : '#1a1a2e',
103 transition: 'all 0.5s ease',
104 };
105
106 const cardStyle = {
107 background: darkMode.value ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)',
108 borderRadius: '15px',
109 padding: '25px',
110 marginBottom: '25px',
111 border: '1px solid ' + (darkMode.value ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'),
112 };
113
114 return (
115 <div style={containerStyle}>
116 <header style={{ textAlign: 'center', marginBottom: '40px' }}>
117 <h1 style={{ color: '#00d4ff', fontSize: '2.5em' }}>
118 Custom Hooks Demo
119 </h1>
120 <p style={{ opacity: 0.7 }}>Creating custom hooks in React</p>
121 </header>
122
123 <div style={{ maxWidth: '900px', margin: '0 auto' }}>
124 {/* useLocalStorage Demo */}
125 <div style={cardStyle}>
126 <h2 style={{ color: '#ffd700', marginBottom: '20px' }}>
127 useLocalStorage
128 </h2>
129 <p style={{ marginBottom: '15px' }}>
130 Stores data in localStorage with automatic synchronization
131 </p>
132
133 <div style={{ marginBottom: '15px' }}>
134 <label style={{ display: 'block', marginBottom: '5px' }}>
135 Username (saved in localStorage):
136 </label>
137 <input
138 type="text"
139 value={username}
140 onChange={(e) => setUsername(e.target.value)}
141 placeholder="Enter your name"
142 style={{
143 padding: '10px 15px',
144 borderRadius: '8px',
145 border: '2px solid #00d4ff',
146 background: 'transparent',
147 color: 'inherit',
148 width: '300px',
149 }}
150 />
151 </div>
152
153 <p style={{ fontSize: '14px', opacity: 0.7 }}>
154 Refresh the page - the value will be preserved!
155 </p>
156 </div>
157
158 {/* useToggle Demo */}
159 <div style={cardStyle}>
160 <h2 style={{ color: '#ffd700', marginBottom: '20px' }}>
161 useToggle
162 </h2>
163
164 <div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap' }}>
165 <div style={{ flex: '1', minWidth: '200px' }}>
166 <p style={{ marginBottom: '10px' }}>Dark Mode:</p>
167 <button
168 onClick={darkMode.toggle}
169 style={{
170 padding: '10px 20px',
171 background: darkMode.value ? '#4CAF50' : '#f44336',
172 color: 'white',
173 border: 'none',
174 borderRadius: '8px',
175 cursor: 'pointer',
176 }}
177 >
178 {darkMode.value ? 'ON' : 'OFF'}
179 </button>
180 </div>
181
182 <div style={{ flex: '1', minWidth: '200px' }}>
183 <p style={{ marginBottom: '10px' }}>Show Panel:</p>
184 <button
185 onClick={showPanel.toggle}
186 style={{
187 padding: '10px 20px',
188 background: showPanel.value ? '#4CAF50' : '#9e9e9e',
189 color: 'white',
190 border: 'none',
191 borderRadius: '8px',
192 cursor: 'pointer',
193 }}
194 >
195 {showPanel.value ? 'Hide' : 'Show'}
196 </button>
197 {showPanel.value && (
198 <div style={{
199 marginTop: '15px',
200 padding: '15px',
201 background: 'rgba(0,212,255,0.1)',
202 borderRadius: '8px',
203 }}>
204 Panel visible!
205 </div>
206 )}
207 </div>
208 </div>
209 </div>
210
211 {/* useCounter Demo */}
212 <div style={cardStyle}>
213 <h2 style={{ color: '#ffd700', marginBottom: '20px' }}>
214 useCounter
215 </h2>
216
217 <div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap' }}>
218 <div>
219 <p style={{ marginBottom: '10px' }}>Visits (step: 1):</p>
220 <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
221 <button onClick={visits.decrement} style={btnStyle}>-</button>
222 <span style={{ fontSize: '2em', minWidth: '60px', textAlign: 'center' }}>
223 {visits.count}
224 </span>
225 <button onClick={visits.increment} style={btnStyle}>+</button>
226 <button onClick={visits.reset} style={{ ...btnStyle, background: '#ff9800' }}>
227 Reset
228 </button>
229 </div>
230 </div>
231
232 <div>
233 <p style={{ marginBottom: '10px' }}>Score (step: 10):</p>
234 <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
235 <button onClick={score.decrement} style={btnStyle}>-10</button>
236 <span style={{ fontSize: '2em', minWidth: '80px', textAlign: 'center' }}>
237 {score.count}
238 </span>
239 <button onClick={score.increment} style={btnStyle}>+10</button>
240 <button onClick={score.reset} style={{ ...btnStyle, background: '#ff9800' }}>
241 Reset
242 </button>
243 </div>
244 </div>
245 </div>
246 </div>
247 </div>
248
249 <footer style={{ textAlign: 'center', marginTop: '40px', opacity: 0.7 }}>
250 <p>Custom hooks allow extracting and sharing logic</p>
251 </footer>
252 </div>
253 );
254}
255
256const btnStyle = {
257 padding: '10px 20px',
258 background: '#00d4ff',
259 color: '#000',
260 border: 'none',
261 borderRadius: '8px',
262 cursor: 'pointer',
263 fontWeight: 'bold',
264 fontSize: '16px',
265};
266
267export default CustomHooksDemo;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which of the following is a rule for using hooks?
2. What characterizes the Render Props pattern?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Custom hook for the space station
- Horizontal ordering
Arrange the syntax for calling a Higher-Order Component:
- Vertical ordering
Arrange the custom hook structure:
- Code editor
Advanced data fetching hook
- Code editor
HOC with Authorization and Monitoring
- Click in order
Arrange the correct Higher-Order Component syntax:
- Code editor
Render Props for Sensor Network