JavaScript and React course Β· Module 8: Advanced React Patterns

Design patterns and logic reusability

14 min read
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:

  1. Reusable - components and logic can be easily used across different parts of the application
  2. Testable - separation of concerns makes writing tests easier
  3. Scalable - the application can grow without a drastic increase in complexity
  4. 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. 1. Which of the following is a rule for using hooks?

  2. 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

Useful articles