Kurs JavaScript i React · Moduł 8: Zaawansowane wzorce React

Wzorce projektowe i reużywalność logiki

14 min czytania
W tej lekcji5

W kosmicznej podróży przez świat Reacta, efektywne wykorzystanie zasobów jest kluczowe dla sukcesu misji. Podobnie jak inżynierowie kosmiczni używają sprawdzonych projektów dla różnych modułów statku kosmicznego, tak i my możemy korzystać z wzorców projektowych w React, aby maksymalizować reużywalność kodu i tworzyć bardziej skalowalne aplikacje.

Wzorce renderowania i kompozycji

1. Komponenty wysokiego rzędu (HOC)

Higher-Order Components (HOC) to funkcje, które przyjmują komponent i zwracają nowy, wzbogacony komponent. To wzorzec oparty na zasadzie kompozycji, a nie dziedziczenia.

1// Prosty HOC dodający funkcjonalność śledzenia czasu
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// Użycie HOC
27function MissionControl({ missionName, elapsedTime }) {
28  return (
29    <div className="mission-control">
30      <h2>Misja: {missionName}</h2>
31      <p>Czas trwania: {elapsedTime.toFixed(0)} sekund</p>
32      {/* reszta komponentu */}
33    </div>
34  );
35}
36
37const MissionControlWithTimer = withTimer(MissionControl);
38
39// Renderowanie wzbogaconego komponentu
40function App() {
41  return <MissionControlWithTimer missionName="Apollo" />;
42}

2. Render Props

Render Props to wzorzec, w którym komponent otrzymuje funkcję, która mówi mu, co ma renderować. Dzięki temu logika może być współdzielona między komponentami.

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// Użycie render props
23function App() {
24  return (
25    <MouseTracker
26      render={({ x, y }) => (
27        <div className="space-map">
28          <h2>Mapa gwiezdna</h2>
29          <p>Współrzędne kursora: ({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. Wzorzec Compound Components

Ten wzorzec pozwala tworzyć komponenty, które współpracują ze sobą, zachowując elastyczność i czytelność kodu.

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// Komponenty potomne
22Spaceship.Engine = function Engine() {
23  const { power, setPower } = useContext(SpaceshipContext);
24
25  return (
26    <div className="engine">
27      <h3>Silnik</h3>
28      <p>Moc: {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>Tarcze</h3>
46      <button onClick={() => setShields(!shields)}>
47        {shields ? 'Dezaktywuj' : 'Aktywuj'} tarcze
48      </button>
49      <p>Status: {shields ? 'Aktywne' : 'Nieaktywne'}</p>
50    </div>
51  );
52};
53
54// Użycie
55function App() {
56  return (
57    <Spaceship>
58      <h2>Panel kontrolny statku kosmicznego</h2>
59      <Spaceship.Engine />
60      <Spaceship.Shields />
61    </Spaceship>
62  );
63}

Wzorce zarządzania stanem

1. Flux / Redux

Redux implementuje wzorzec Flux, który zakłada jednokierunkowy przepływ danych. Stan aplikacji jest przechowywany w jednym obiekcie (store), a modyfikacje stanu mogą nastąpić tylko przez emisję akcji, które są przetwarzane przez reduktory.

1// Akcje
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// Reduktor
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// W komponencie z 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: 'Nowy członek załogi',
41      role: 'Kadet'
42    };
43    dispatch(addCrewmember(newMember));
44  }
45
46  return (
47    <div>
48      <h2>Zarządzanie załogą</h2>
49      <button onClick={handleAddCrewmember}>Dodaj członka załogi</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              Usuń
56            </button>
57          </li>
58        ))}
59      </ul>
60    </div>
61  );
62}

2. Context API

Context API pozwala na przekazywanie danych przez drzewo komponentów bez konieczności przekazywania props na każdym poziomie.

1// Stworzenie kontekstu
2const MissionContext = createContext();
3
4// Provider
5function MissionProvider({ children }) {
6  const [mission, setMission] = useState({
7    name: 'Apollo 11',
8    status: 'W trakcie',
9    crewMembers: [
10      { id: 1, name: 'Neil Armstrong', role: 'Dowódca' },
11      { id: 2, name: 'Buzz Aldrin', role: 'Pilot modułu księżycowego' },
12      { id: 3, name: 'Michael Collins', role: 'Pilot modułu dowodzenia' }
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 dla łatwiejszego użycia
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// Użycie w komponentach
50function MissionDashboard() {
51  const { mission } = useMission();
52
53  return (
54    <div>
55      <h1>Misja: {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>Centrum kontroli misji</h2>
67      <button onClick={() => updateMissionStatus('Zakończona pomyślnie')}>
68        Zakończ misję
69      </button>
70      <button onClick={() => updateMissionStatus('Przerwana')}>
71        Przerwij misję
72      </button>
73    </div>
74  );
75}
76
77function CrewList() {
78  const { mission, addCrewMember } = useMission();
79
80  return (
81    <div>
82      <h2>Załoga misji</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: 'Nowy astronauta',
95            role: 'Specjalista misji'
96          })
97        }
98      >
99        Dodaj członka załogi
100      </button>
101    </div>
102  );
103}
104
105// Połączenie wszystkiego
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 dla stanu

Jak widzieliśmy w poprzednim module, custom hooki są potężnym narzędziem do enkapsulacji i ponownego wykorzystania logiki związanej ze stanem.

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(`Nie można ustawić mocy - silnik ${engineName} jest wyłączony`);
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// Użycie
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>Kontrola systemów statku</h2>
84
85      <div className="engines-section">
86        <h3>Silniki</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>Moc: {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>System podtrzymywania życia</h3>
113        <p>Poziom tlenu: {lifeSupportSystem.oxygen}%</p>
114        <p>Temperatura: {lifeSupportSystem.temperature}°C</p>
115        <p>Ciśnienie: {lifeSupportSystem.pressure} atm</p>
116
117        <div className="controls">
118          <button
119            onClick={() =>
120              adjustLifeSupport({
121                oxygen: lifeSupportSystem.oxygen + 5
122              })
123            }
124          >
125            Zwiększ poziom tlenu
126          </button>
127          <button
128            onClick={() =>
129              adjustLifeSupport({
130                temperature: lifeSupportSystem.temperature - 1
131              })
132            }
133          >
134            Obniż temperaturę
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}

Wzorce komponentów UI

1. Container / Presentational Pattern

Ten wzorzec rozdziela logikę biznesową (kontener) od prezentacji (komponent prezentacyjny), co zwiększa reużywalność komponentów i ułatwia testowanie.

1// Komponent kontenerowy - zajmuje się logiką i pobieraniem danych
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('Nie udało się pobrać danych planety');
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  // Wywołanie funkcji children z danymi
26  return children({ planet, loading, error });
27}
28
29// Komponent prezentacyjny - zajmuje się tylko wyświetlaniem
30function PlanetInfo({ planet, loading, error }) {
31  if (loading) return <div>Ładowanie danych planety...</div>;
32  if (error) return <div>Błąd: {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>Typ: {planet.type}</p>
40        <p>Średnica: {planet.diameter} km</p>
41        <p>Odległość od Słońca: {planet.distanceFromSun} mln km</p>
42        <p>Okres orbitalny: {planet.orbitalPeriod} dni</p>
43        <p>Temperatura powierzchni: {planet.surfaceTemperature}°C</p>
44      </div>
45
46      {planet.moons && planet.moons.length > 0 && (
47        <div className="moons">
48          <h3>Księżyce ({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// Użycie
61function SolarSystemExplorer() {
62  const [selectedPlanet, setSelectedPlanet] = useState('earth');
63
64  return (
65    <div className="solar-system-explorer">
66      <h1>Eksploracja Układu Słonecznego</h1>
67
68      <div className="planet-selector">
69        <label>Wybierz planetę: </label>
70        <select
71          value={selectedPlanet}
72          onChange={e => setSelectedPlanet(e.target.value)}
73        >
74          <option value="mercury">Merkury</option>
75          <option value="venus">Wenus</option>
76          <option value="earth">Ziemia</option>
77          <option value="mars">Mars</option>
78          <option value="jupiter">Jowisz</option>
79          <option value="saturn">Saturn</option>
80          <option value="uranus">Uran</option>
81          <option value="neptune">Neptun</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 preferuje kompozycję nad dziedziczeniem. Ten wzorzec pozwala tworzyć elastyczne komponenty poprzez zagnieżdżanie i przekazywanie props.

1// Komponent bazowy - Karta
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// Specjalizowane komponenty przy użyciu kompozycji
13function MissionCard({ mission, onComplete }) {
14  return (
15    <Card
16      title={<h3>{mission.name}</h3>}
17      footer={
18        <button onClick={() => onComplete(mission.id)}>
19          Zakończ misję
20        </button>
21      }
22      className="mission-card"
23    >
24      <p><strong>Cel:</strong> {mission.objective}</p>
25      <p><strong>Status:</strong> {mission.status}</p>
26      <p><strong>Postęp:</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          Przydziel do misji
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>Ranga:</strong> {astronaut.rank}</p>
56          <p><strong>Specjalizacja:</strong> {astronaut.specialization}</p>
57          <p><strong>Misje:</strong> {astronaut.missionCount}</p>
58        </div>
59      </div>
60    </Card>
61  );
62}
63
64// Użycie
65function Dashboard() {
66  const missions = [
67    { id: 1, name: 'Eksploracja Marsa', objective: 'Zbadać powierzchnię', status: 'W toku', progress: 65 },
68    { id: 2, name: 'Stacja kosmiczna ISS', objective: 'Konserwacja modułów', status: 'Zaplanowana', progress: 0 }
69  ];
70
71  const astronauts = [
72    { id: 1, name: 'Jan Kowalski', rank: 'Kapitan', specialization: 'Pilot', missionCount: 5, avatar: '/avatars/jk.png' },
73    { id: 2, name: 'Anna Nowak', rank: 'Porucznik', specialization: 'Inżynier', missionCount: 3, avatar: '/avatars/an.png' }
74  ];
75
76  return (
77    <div className="dashboard">
78      <h1>Panel kontrolny centrum kosmicznego</h1>
79
80      <section className="missions-section">
81        <h2>Aktywne misje</h2>
82        <div className="missions-grid">
83          {missions.map(mission => (
84            <MissionCard
85              key={mission.id}
86              mission={mission}
87              onComplete={id => console.log(`Zakończono misję ${id}`)}
88            />
89          ))}
90        </div>
91      </section>
92
93      <section className="astronauts-section">
94        <h2>Dostępni astronauci</h2>
95        <div className="astronauts-grid">
96          {astronauts.map(astronaut => (
97            <AstronautCard
98              key={astronaut.id}
99              astronaut={astronaut}
100              onAssign={id => console.log(`Przydzielono astronautę ${id}`)}
101            />
102          ))}
103        </div>
104      </section>
105    </div>
106  );
107}

3. Wzorzec Headless Components

Ten wzorzec rozdziela logikę zachowania komponentu od jego wyglądu, pozwalając na maksymalną elastyczność stylistyczną.

1// Komponent bezgłowy (Headless) - zawiera tylko logikę, bez 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// Użycie z dowolnym UI
16function MissionTabs() {
17  const tabs = [
18    { id: 'overview', label: 'Przegląd' },
19    { id: 'crew', label: 'Załoga' },
20    { id: 'timeline', label: 'Harmonogram' },
21    { id: 'systems', label: 'Systemy' }
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>Przegląd misji</h3>
44            <p>Misja Apollo 11 była pierwszą załogową misją na Księżyc...</p>
45          </div>
46        )}
47
48        {activeTab === 1 && (
49          <div className="crew-tab">
50            <h3>Załoga</h3>
51            <ul>
52              <li>Neil Armstrong - Dowódca</li>
53              <li>Edwin "Buzz" Aldrin - Pilot modułu księżycowego</li>
54              <li>Michael Collins - Pilot modułu dowodzenia</li>
55            </ul>
56          </div>
57        )}
58
59        {activeTab === 2 && (
60          <div className="timeline-tab">
61            <h3>Harmonogram</h3>
62            <div className="timeline">
63              <div className="timeline-event">
64                <span className="date">16 lipca 1969</span>
65                <span className="description">Start z Przylądka Kennedy'ego</span>
66              </div>
67              <div className="timeline-event">
68                <span className="date">20 lipca 1969</span>
69                <span className="description">Lądowanie na Księżycu</span>
70              </div>
71              <div className="timeline-event">
72                <span className="date">21 lipca 1969</span>
73                <span className="description">Pierwszy krok na Księżycu</span>
74              </div>
75              <div className="timeline-event">
76                <span className="date">24 lipca 1969</span>
77                <span className="description">Powrót na Ziemię</span>
78              </div>
79            </div>
80          </div>
81        )}
82
83        {activeTab === 3 && (
84          <div className="systems-tab">
85            <h3>Systemy statku</h3>
86            <p>Moduł dowodzenia: Columbia</p>
87            <p>Moduł księżycowy: Eagle</p>
88            <p>Rakieta nośna: Saturn V</p>
89          </div>
90        )}
91      </div>
92    </div>
93  );
94}

Wzorce optymalizacyjne

1. Memoizacja

Memoizacja pomaga unikać niepotrzebnych renderowań przez zapamiętywanie rezultatów kosztownych obliczeń.

1// Memoizacja komponentu
2const MemoizedTelemetryChart = React.memo(function TelemetryChart({ data }) {
3  // Symulacja kosztownych obliczeń
4  const processedData = useMemo(() => {
5    console.log('Przetwarzanie danych telemetrycznych...');
6
7    // Kosztowne przetwarzanie danych
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>Wykres telemetrii</h3>
18      <div className="chart-container">
19        {/* Wykres z przetworzonymi danymi */}
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 ? '(anomalia)' : ''}`}
29          />
30        ))}
31      </div>
32    </div>
33  );
34}, (prevProps, nextProps) => {
35  // Własna logika porównania props
36  if (prevProps.data.length !== nextProps.data.length) {
37    return false; // renderuj ponownie jeśli zmienił się rozmiar danych
38  }
39
40  // Porównaj tylko ostatnie 5 punktów danych
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; // renderuj ponownie jeśli dane się różnią
52    }
53  }
54
55  return true; // nie renderuj ponownie
56});
57
58// Funkcje pomocnicze
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. Wirtualizacja list

Dla dużych list danych, wirtualizacja pozwala renderować tylko te elementy, które są aktualnie widoczne na ekranie.

1// Implementacja prostej wirtualizacji (w rzeczywistych aplikacjach
2// warto użyć bibliotek jak react-window lub 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>Katalog asteroid</h2>
27      <p>Liczba asteroid: {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">Średnica: {asteroid.diameter} km</span>
53                <span className="distance">
54                  Odległość: {asteroid.distance} mln 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 'Wysokie zagrożenie';
73    case 'medium': return 'Średnie zagrożenie';
74    case 'low': return 'Niskie zagrożenie';
75    default: return 'Brak zagrożenia';
76  }
77}

3. Lazy loading

Leniwne ładowanie komponentów pozwala odroczyć ładowanie części aplikacji, dopóki nie są potrzebne, co może znacznie przyspieszyć inicjalne ładowanie aplikacji.

1// Lazy loading komponentów
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>Eksploracja układu słonecznego</h1>
12
13      <div className="planet-selector">
14        <button onClick={() => setActiveSection('mars')}>
15          Eksploracja Marsa
16        </button>
17        <button onClick={() => setActiveSection('jupiter')}>
18          Księżyce Jowisza
19        </button>
20        <button onClick={() => setActiveSection('saturn')}>
21          Pierścienie Saturna
22        </button>
23      </div>
24
25      <Suspense fallback={<div className="loading">Ładowanie modułu eksploracji...</div>}>
26        {activeSection === 'mars' && <LazyMarsExplorer />}
27        {activeSection === 'jupiter' && <LazyJupiterMoons />}
28        {activeSection === 'saturn' && <LazySaturnRings />}
29      </Suspense>
30    </div>
31  );
32}

Podsumowanie

Wzorce projektowe w React dostarczają sprawdzonych rozwiązań dla typowych problemów programistycznych. Pozwalają na tworzenie kodu, który jest:

  1. Reużywalny - komponenty i logikę można łatwo wykorzystać w różnych częściach aplikacji
  2. Testowalny - separacja obowiązków ułatwia pisanie testów
  3. Skalowalny - aplikacja może rosnąć bez drastycznego wzrostu złożoności
  4. Łatwy w utrzymaniu - jasna struktura i organizacja ułatwiają wprowadzanie zmian

W kosmicznej podróży przez React, wzorce te są jak sprawdzone trasy międzygwiezdne, które pozwalają dotrzeć do celu szybciej i bezpieczniej. Eksperymentuj z różnymi wzorcami i wybieraj te, które najlepiej pasują do konkretnych potrzeb Twojej aplikacji.

Kod do tej lekcji: App.jsx
1import React, { useState, useEffect, useCallback } from 'react';
2
3// Custom Hook: useLocalStorage
4function useLocalStorage(key, initialValue) {
5  // Stan do przechowywania wartości
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  // Funkcja do aktualizacji wartości
17  const setValue = useCallback((value) => {
18    setStoredValue((prev) => (value instanceof Function ? value(prev) : value));
19  }, []);
20
21  // Zapisz do localStorage przy każdej zmianie
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// Demonstracja custom hooks
72function CustomHooksDemo() {
73  // useLocalStorage - przechowywanie ustawień
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 - przełączniki
82  const darkMode = useToggle(settings.darkMode);
83  const showPanel = useToggle(false);
84  const isEditing = useToggle(false);
85
86  // useCounter - liczniki
87  const visits = useCounter(0);
88  const score = useCounter(100, 10);
89
90  // Efekt synchronizacji z 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 }}>Tworzenie własnych hooków w 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            Przechowuje dane w localStorage z automatyczną synchronizacją
131          </p>
132
133          <div style={{ marginBottom: '15px' }}>
134            <label style={{ display: 'block', marginBottom: '5px' }}>
135              Nazwa użytkownika (zapisana w localStorage):
136            </label>
137            <input
138              type="text"
139              value={username}
140              onChange={(e) => setUsername(e.target.value)}
141              placeholder="Wpisz swoją nazwę"
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            Odśwież stronę - wartość zostanie zachowana!
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 ? 'Ukryj' : 'Pokaż'}
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 widoczny!
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 pozwalają na wydzielenie i współdzielenie logiki</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;

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Które z poniższych jest zasadą korzystania z hooks?

  2. 2. Czym charakteryzuje się wzorzec Render Props?

To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Custom hook dla stacji kosmicznej

  • Układanie w poziomie

    Ułóż składnię wywołania Higher-Order Component:

  • Układanie w pionie

    Uporządkuj strukturę custom hook:

  • Edytor kodu

    Zaawansowany data fetching hook

  • Edytor kodu

    HOC z autoryzacją i monitoringiem

  • Klikanie w kolejności

    Ułóż poprawną składnię Higher-Order Component:

  • Edytor kodu

    Render Props dla sieci sensorów

Przydatne artykuły