Kurs JavaScript i React · Moduł 4: Context API i useReducer

Globalne zarządzanie stanem

12 min czytania
W tej lekcji9

Wraz z rozwojem aplikacji React, zarządzanie stanem staje się coraz bardziej złożonym wyzwaniem. W małych aplikacjach, lokalny stan komponentów i props są wystarczające, ale w miarę skalowania, potrzebujemy bardziej zaawansowanych rozwiązań do zarządzania globalnym stanem aplikacji.

W tym module przyjrzymy się różnym podejściom do globalnego zarządzania stanem w React, od wbudowanych mechanizmów po popularne biblioteki zewnętrzne.

Wyzwania związane z globalnym stanem

Zanim zagłębimy się w rozwiązania, zrozummy dlaczego zarządzanie globalnym stanem może być wyzwaniem:

  1. Współdzielenie danych - komponenty w różnych częściach drzewa komponentów mogą potrzebować dostępu do tych samych danych
  2. Synchronizacja - zmiany w jednym miejscu powinny być widoczne wszędzie, gdzie dane są używane
  3. Przewidywalność - aktualizacje stanu powinny być przewidywalne i łatwe do śledzenia
  4. Wydajność - niepotrzebne renderowanie powinno być minimalizowane
  5. Złożoność - logika stanu może być skomplikowana, z wieloma powiązanymi zmianami

Context API + useReducer jako podstawowe rozwiązanie

Jak poznaliśmy w poprzednich modułach, połączenie Context API z useReducer zapewnia solidne podstawy do globalnego zarządzania stanem:

1import React, { createContext, useReducer, useContext } from 'react';
2
3// Początkowy stan
4const initialState = {
5  missions: [],
6  activeMission: null,
7  user: null,
8  isLoading: false,
9  error: null
10};
11
12// Reducer
13function appReducer(state, action) {
14  switch (action.type) {
15    case 'LOGIN_SUCCESS':
16      return { ...state, user: action.payload, error: null };
17    case 'LOGOUT':
18      return { ...state, user: null };
19    case 'FETCH_MISSIONS_START':
20      return { ...state, isLoading: true };
21    case 'FETCH_MISSIONS_SUCCESS':
22      return { ...state, missions: action.payload, isLoading: false };
23    case 'FETCH_MISSIONS_ERROR':
24      return { ...state, error: action.payload, isLoading: false };
25    case 'SET_ACTIVE_MISSION':
26      return { ...state, activeMission: action.payload };
27    default:
28      return state;
29  }
30}
31
32// Kontekst
33const AppStateContext = createContext();
34
35// Provider
36function AppStateProvider({ children }) {
37  const [state, dispatch] = useReducer(appReducer, initialState);
38
39  // Możemy także dodać funkcje pomocnicze
40  const login = async (credentials) => {
41    try {
42      dispatch({ type: 'LOGIN_START' });
43      // API call do logowania
44      const user = await authService.login(credentials);
45      dispatch({ type: 'LOGIN_SUCCESS', payload: user });
46    } catch (error) {
47      dispatch({ type: 'LOGIN_ERROR', payload: error.message });
48    }
49  };
50
51  const fetchMissions = async () => {
52    try {
53      dispatch({ type: 'FETCH_MISSIONS_START' });
54      const missions = await missionService.getAll();
55      dispatch({ type: 'FETCH_MISSIONS_SUCCESS', payload: missions });
56    } catch (error) {
57      dispatch({ type: 'FETCH_MISSIONS_ERROR', payload: error.message });
58    }
59  };
60
61  // Wartość kontekstu
62  const value = {
63    state,
64    dispatch,
65    login,
66    fetchMissions,
67    setActiveMission: (mission) => dispatch({ type: 'SET_ACTIVE_MISSION', payload: mission })
68  };
69
70  return (
71    <AppStateContext.Provider value={value}>
72      {children}
73    </AppStateContext.Provider>
74  );
75}
76
77// Hook do korzystania z kontekstu
78function useAppState() {
79  const context = useContext(AppStateContext);
80  if (context === undefined) {
81    throw new Error('useAppState must be used within an AppStateProvider');
82  }
83  return context;
84}
85
86// Użycie w aplikacji
87function App() {
88  return (
89    <AppStateProvider>
90      <Header />
91      <Main />
92      <Footer />
93    </AppStateProvider>
94  );
95}
96
97function Header() {
98  const { state, dispatch } = useAppState();
99
100  return (
101    <header>
102      {state.user ? (
103        <>
104          <span>Witaj, {state.user.name}</span>
105          <button onClick={() => dispatch({ type: 'LOGOUT' })}>Wyloguj</button>
106        </>
107      ) : (
108        <LoginButton />
109      )}
110    </header>
111  );
112}
113
114function MissionsList() {
115  const { state, fetchMissions, setActiveMission } = useAppState();
116
117  useEffect(() => {
118    fetchMissions();
119  }, []);
120
121  if (state.isLoading) return <p>Ładowanie misji...</p>;
122  if (state.error) return <p>Błąd: {state.error}</p>;
123
124  return (
125    <div>
126      <h2>Dostępne misje</h2>
127      <ul>
128        {state.missions.map(mission => (
129          <li key={mission.id}>
130            {mission.name}
131            <button onClick={() => setActiveMission(mission)}>
132              Wybierz
133            </button>
134          </li>
135        ))}
136      </ul>
137    </div>
138  );
139}

To rozwiązanie ma kilka zalet:

  • Używa wbudowanych mechanizmów React
  • Nie wymaga zewnętrznych zależności
  • Daje dobre podstawy strukturalne dla globalnego stanu

Jednakże w miarę jak aplikacja rośnie, to rozwiązanie może napotykać ograniczenia związane z wydajnością i organizacją kodu.

Skalowanie aplikacji z wieloma kontekstami

Dla większych aplikacji zaleca się podział globalnego stanu na mniejsze, niezależne konteksty według domen:

1// Kontekst autentykacji
2const AuthContext = createContext();
3
4function AuthProvider({ children }) {
5  const [user, setUser] = useState(null);
6  const [isLoading, setIsLoading] = useState(false);
7  const [error, setError] = useState(null);
8
9  const login = async (credentials) => {
10    setIsLoading(true);
11    try {
12      const user = await authService.login(credentials);
13      setUser(user);
14      setError(null);
15    } catch (err) {
16      setError(err.message);
17    } finally {
18      setIsLoading(false);
19    }
20  };
21
22  const logout = async () => {
23    await authService.logout();
24    setUser(null);
25  };
26
27  return (
28    <AuthContext.Provider value={{ user, isLoading, error, login, logout }}>
29      {children}
30    </AuthContext.Provider>
31  );
32}
33
34// Kontekst misji
35const MissionsContext = createContext();
36
37function MissionsProvider({ children }) {
38  const [missions, setMissions] = useReducer(missionsReducer, []);
39  const [activeMission, setActiveMission] = useState(null);
40  // reszta logiki...
41
42  return (
43    <MissionsContext.Provider value={{ missions, activeMission, /*...*/ }}>
44      {children}
45    </MissionsContext.Provider>
46  );
47}
48
49// Kontekst UI
50const UIContext = createContext();
51
52function UIProvider({ children }) {
53  const [darkMode, setDarkMode] = useState(false);
54  const [sidebarOpen, setSidebarOpen] = useState(true);
55  // reszta logiki...
56
57  return (
58    <UIContext.Provider value={{ darkMode, toggleDarkMode: () => setDarkMode(!darkMode), /*...*/ }}>
59      {children}
60    </UIContext.Provider>
61  );
62}
63
64// Łączenie wszystkich providerów
65function AppProviders({ children }) {
66  return (
67    <AuthProvider>
68      <MissionsProvider>
69        <UIProvider>
70          {children}
71        </UIProvider>
72      </MissionsProvider>
73    </AuthProvider>
74  );
75}

Zalety tego podejścia:

  • Lepszy podział odpowiedzialności
  • Mniejsze konteksty = mniej niepotrzebnych renderowań
  • Łatwiejsze testowanie poszczególnych modułów
  • Komponenty mogą korzystać tylko z potrzebnych kontekstów

Optymalizacja wydajności z useMemo i memo

Jednym z wyzwań przy używaniu Context API jest wydajność - każda zmiana wartości kontekstu powoduje przerenderowanie wszystkich komponentów, które używają tego kontekstu. Oto kilka technik optymalizacji:

1function OptimizedProvider({ children }) {
2  const [state, dispatch] = useReducer(reducer, initialState);
3
4  // Memizacja wartości kontekstu
5  const contextValue = useMemo(() => {
6    return { state, dispatch };
7  }, [state, dispatch]);
8
9  return (
10    <AppContext.Provider value={contextValue}>
11      {children}
12    </AppContext.Provider>
13  );
14}
15
16// Optymalizacja komponentów konsumujących
17const MissionItem = memo(function MissionItem({ mission, onSelect }) {
18  return (
19    <li>
20      {mission.name}
21      <button onClick={() => onSelect(mission)}>Wybierz</button>
22    </li>
23  );
24});
25
26function MissionList() {
27  const { missions, selectMission } = useMissions();
28
29  // Memizacja callbacka
30  const handleSelect = useCallback((mission) => {
31    selectMission(mission);
32  }, [selectMission]);
33
34  return (
35    <ul>
36      {missions.map(mission => (
37        <MissionItem
38          key={mission.id}
39          mission={mission}
40          onSelect={handleSelect}
41        />
42      ))}
43    </ul>
44  );
45}

Wprowadzenie do Redux Toolkit

Redux to popularna biblioteka do zarządzania stanem, która oferuje bardziej ustrukturyzowane podejście do globalnego stanu. Dzisiaj nowoczesny Redux to Redux Toolkit (RTK) - oficjalny, rekomendowany zestaw narzędzi, który eliminuje większość boilerplate'u. Zamiast ręcznie pisać reducery z instrukcją switch, używamy createSlice, a zamiast createStore - configureStore. Oto podstawy Redux Toolkit z React:

1import { configureStore, createSlice } from '@reduxjs/toolkit';
2import { Provider, useSelector, useDispatch } from 'react-redux';
3
4// Slice = fragment stanu + reducery + akcje w jednym miejscu
5const spaceAppSlice = createSlice({
6  name: 'spaceApp',
7  initialState: {
8    missions: [],
9    user: null
10  },
11  reducers: {
12    // Dzięki Immerowi możemy "mutować" stan - RTK robi to bezpiecznie i niemutowalnie
13    setUser: (state, action) => {
14      state.user = action.payload;
15    },
16    setMissions: (state, action) => {
17      state.missions = action.payload;
18    }
19  }
20});
21
22// createSlice automatycznie generuje kreatory akcji
23export const { setUser, setMissions } = spaceAppSlice.actions;
24
25// Store - configureStore sam dodaje DevTools i middleware
26const store = configureStore({
27  reducer: spaceAppSlice.reducer
28});
29
30// Provider
31function App() {
32  return (
33    <Provider store={store}>
34      <Main />
35    </Provider>
36  );
37}
38
39// Użycie w komponentach
40function UserProfile() {
41  // Wybieranie części stanu
42  const user = useSelector(state => state.user);
43  const dispatch = useDispatch();
44
45  if (!user) return <p>Użytkownik nie zalogowany</p>;
46
47  return (
48    <div>
49      <h2>{user.name}</h2>
50      <button onClick={() => dispatch(setUser(null))}>
51        Wyloguj
52      </button>
53    </div>
54  );
55}

Uwaga historyczna: Klasyczny Redux używał createStore i ręcznych reducerów z instrukcją switch. To podejście (import { createStore } from 'redux') jest dziś przestarzałe (deprecated) - oficjalna dokumentacja zaleca Redux Toolkit. Dlatego od razu uczymy się nowoczesnego sposobu.

Organizacja kodu w Redux Toolkit

W większych aplikacjach każdy fragment stanu to osobny slice. configureStore łączy je automatycznie - wystarczy podać mapę reducerów (nie potrzeba już ręcznego combineReducers):

1// /store/index.js
2import { configureStore } from '@reduxjs/toolkit';
3import authReducer from './auth/authSlice';
4import missionsReducer from './missions/missionsSlice';
5import uiReducer from './ui/uiSlice';
6
7// configureStore łączy reducery sam - bez combineReducers
8const store = configureStore({
9  reducer: {
10    auth: authReducer,
11    missions: missionsReducer,
12    ui: uiReducer
13  }
14});
15export default store;
16
17// /store/missions/missionsSlice.js
18import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
19
20// Operacje asynchroniczne obsługuje createAsyncThunk
21export const fetchMissions = createAsyncThunk(
22  'missions/fetchMissions',
23  async () => {
24    const missions = await api.fetchMissions();
25    return missions;
26  }
27);
28
29const missionsSlice = createSlice({
30  name: 'missions',
31  initialState: {
32    list: [],
33    active: null,
34    loading: false,
35    error: null
36  },
37  reducers: {
38    setActiveMission: (state, action) => {
39      state.active = action.payload;
40    }
41  },
42  // extraReducers reagują na stany thunka (pending/fulfilled/rejected)
43  extraReducers: (builder) => {
44    builder
45      .addCase(fetchMissions.pending, (state) => {
46        state.loading = true;
47      })
48      .addCase(fetchMissions.fulfilled, (state, action) => {
49        state.list = action.payload;
50        state.loading = false;
51      })
52      .addCase(fetchMissions.rejected, (state, action) => {
53        state.error = action.error.message;
54        state.loading = false;
55      });
56  }
57});
58
59export const { setActiveMission } = missionsSlice.actions;
60export default missionsSlice.reducer;
61
62// /store/missions/selectors.js
63export const selectMissions = state => state.missions.list;
64export const selectActiveMission = state => state.missions.active;
65export const selectMissionsLoading = state => state.missions.loading;

Nowoczesne alternatywy

W ekosystemie React pojawiło się wiele alternatywnych bibliotek do zarządzania stanem, które starają się uprościć proces i zmniejszyć ilość kodu boilerplate. Oto kilka popularnych opcji:

1. Zustand

Zustand to minimalistyczna biblioteka, która łączy prostotę hooka z potęgą store:

1import create from 'zustand';
2
3// Utworzenie store
4const useSpaceStore = create((set) => ({
5  missions: [],
6  activeMissionId: null,
7  isLoading: false,
8
9  fetchMissions: async () => {
10    set({ isLoading: true });
11    try {
12      const missions = await api.fetchMissions();
13      set({ missions, isLoading: false });
14    } catch (error) {
15      set({ error: error.message, isLoading: false });
16    }
17  },
18
19  setActiveMission: (id) => set({ activeMissionId: id })
20}));
21
22// Użycie w komponencie
23function MissionPanel() {
24  // Selektywne pobieranie danych ze store
25  const missions = useSpaceStore(state => state.missions);
26  const isLoading = useSpaceStore(state => state.isLoading);
27  const fetchMissions = useSpaceStore(state => state.fetchMissions);
28
29  useEffect(() => {
30    fetchMissions();
31  }, [fetchMissions]);
32
33  if (isLoading) return <p>Ładowanie...</p>;
34
35  return (
36    <div>
37      <h2>Misje</h2>
38      <ul>
39        {missions.map(mission => (
40          <MissionItem key={mission.id} mission={mission} />
41        ))}
42      </ul>
43    </div>
44  );
45}

2. Recoil

Recoil to biblioteka stworzona przez Facebook, która wprowadza koncepcję "atomów" i "selektorów":

1import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
2
3// Definiowanie atomów (najmniejszych jednostek stanu)
4const missionsState = atom({
5  key: 'missionsState',
6  default: []
7});
8
9const missionsFilterState = atom({
10  key: 'missionsFilterState',
11  default: 'all' // 'all', 'active', 'completed'
12});
13
14// Selektor (pochodny stan)
15const filteredMissionsState = selector({
16  key: 'filteredMissionsState',
17  get: ({ get }) => {
18    const missions = get(missionsState);
19    const filter = get(missionsFilterState);
20
21    switch (filter) {
22      case 'active':
23        return missions.filter(mission => mission.status === 'active');
24      case 'completed':
25        return missions.filter(mission => mission.status === 'completed');
26      default:
27        return missions;
28    }
29  }
30});
31
32// Użycie w komponentach
33function MissionsFilter() {
34  const [filter, setFilter] = useRecoilState(missionsFilterState);
35
36  return (
37    <div>
38      <button
39        className={filter === 'all' ? 'active' : ''}
40        onClick={() => setFilter('all')}
41      >
42        Wszystkie
43      </button>
44      <button
45        className={filter === 'active' ? 'active' : ''}
46        onClick={() => setFilter('active')}
47      >
48        Aktywne
49      </button>
50      <button
51        className={filter === 'completed' ? 'active' : ''}
52        onClick={() => setFilter('completed')}
53      >
54        Zakończone
55      </button>
56    </div>
57  );
58}
59
60function MissionsList() {
61  // Używanie filtrowanej wartości
62  const missions = useRecoilValue(filteredMissionsState);
63
64  return (
65    <ul>
66      {missions.map(mission => (
67        <li key={mission.id}>{mission.name}</li>
68      ))}
69    </ul>
70  );
71}

3. Jotai

Jotai, podobnie jak Recoil, używa podejścia opartego na atomach, ale z bardziej minimalistycznym API:

1import { atom, useAtom } from 'jotai';
2
3// Definicja atomów
4const userAtom = atom(null);
5const missionsAtom = atom([]);
6const missionFilterAtom = atom('all');
7
8// Atom pochodny
9const filteredMissionsAtom = atom(
10  (get) => {
11    const missions = get(missionsAtom);
12    const filter = get(missionFilterAtom);
13
14    switch (filter) {
15      case 'active':
16        return missions.filter(m => m.status === 'active');
17      case 'completed':
18        return missions.filter(m => m.status === 'completed');
19      default:
20        return missions;
21    }
22  }
23);
24
25// Komponent
26function MissionDashboard() {
27  const [missions, setMissions] = useAtom(missionsAtom);
28  const [filter, setFilter] = useAtom(missionFilterAtom);
29  const [filteredMissions] = useAtom(filteredMissionsAtom);
30
31  // Logika komponentu...
32
33  return (
34    <div>
35      {/* UI */}
36    </div>
37  );
38}

Wybór odpowiedniego rozwiązania

Jak wybrać najlepsze rozwiązanie do zarządzania stanem dla swojej aplikacji? Oto kilka wskazówek:

  1. Rozmiar aplikacji:

    • Małe aplikacje: useState + useReducer + Context API
    • Średnie aplikacje: Zustand lub Jotai
    • Duże aplikacje: Redux (z Redux Toolkit) lub Recoil
  2. Złożoność stanu:

    • Prosty stan: useState
    • Średnio złożony stan: useReducer + Context API
    • Złożony stan z relacjami: Redux lub Recoil
  3. Wymagania dotyczące wydajności:

    • Jeśli wydajność renderowania jest kluczowa, rozważ biblioteki, które zapewniają granularną reaktywność (Recoil, Jotai)
  4. Doświadczenie zespołu:

    • Jeśli zespół zna Redux, może warto przy nim pozostać
    • Dla nowych projektów, prostsze API Zustand czy Jotai może przyspieszyć rozwój

Najlepsze praktyki

Niezależnie od wybranego rozwiązania, oto kilka ogólnych najlepszych praktyk dotyczących zarządzania globalnym stanem:

  1. Minimalizuj globalny stan - nie wszystko musi być globalne; używaj lokalnego stanu dla danych specyficznych dla komponentu

  2. Dziel stan według domen logicznych - grupuj powiązane dane i logikę razem

  3. Normalizuj dane - unikaj duplikacji i głębokich zagnieżdżeń

  4. Zachowuj niezmienność - nigdy nie modyfikuj stanu bezpośrednio

  5. Dokumentuj intencje - używaj opisowych nazw akcji i selektorów

  6. Używaj selektorów - do pobierania pochodnych danych

  7. Trzymaj logikę biznesową poza komponentami - w akcjach, reducerach lub dedykowanych hookach

1// Przykład: Hook z logiką biznesową
2function useMissionControl() {
3  const { state, dispatch } = useAppState();
4
5  const launchMission = useCallback(async (missionId) => {
6    // Sprawdzenie warunków biznesowych
7    if (!state.user || state.user.role !== 'commander') {
8      throw new Error('Tylko dowódca może rozpocząć misję');
9    }
10
11    if (state.weather === 'storm') {
12      throw new Error('Nie można wystartować podczas burzy');
13    }
14
15    // Logika biznesowa
16    dispatch({ type: 'MISSION_LAUNCH_START', payload: missionId });
17
18    try {
19      await missionService.launch(missionId);
20      dispatch({ type: 'MISSION_LAUNCH_SUCCESS', payload: missionId });
21    } catch (error) {
22      dispatch({ type: 'MISSION_LAUNCH_FAILURE', payload: error.message });
23      throw error;
24    }
25  }, [state.user, state.weather, dispatch]);
26
27  return {
28    missions: state.missions,
29    activeMission: state.activeMission,
30    isLaunching: state.isLaunching,
31    launchError: state.launchError,
32    launchMission
33  };
34}

Podsumowanie

Globalne zarządzanie stanem jest kluczowym aspektem budowania skalowalnych aplikacji React. W tym module poznaliśmy różne podejścia:

  1. Context API + useReducer - wbudowane rozwiązanie React, dobre dla mniejszych i średnich aplikacji

  2. Podział na wiele kontekstów - sposób na organizację globalnego stanu według domen

  3. Optymalizacja wydajności - techniki minimalizacji przerenderowywania

  4. Redux - tradycyjna biblioteka z ustrukturyzowanym podejściem do zarządzania stanem

  5. Nowoczesne alternatywy - Zustand, Recoil, Jotai oferujące prostsze API i mniej kodu

Wybór odpowiedniego rozwiązania zależy od wielkości aplikacji, złożoności stanu i preferencji zespołu. Niezależnie od wyboru, postępowanie zgodnie z najlepszymi praktykami pomoże utrzymać czysty, przewidywalny i wydajny kod.

Kod do tej lekcji: App.jsx
1import React, { useState, useEffect, useCallback, useRef } from 'react';
2
3// Custom Hook: useLocalStorage
4function useLocalStorage(key, initialValue) {
5  const [storedValue, setStoredValue] = useState(() => {
6    try {
7      const item = window.localStorage.getItem(key);
8      return item ? JSON.parse(item) : initialValue;
9    } catch (error) {
10      return initialValue;
11    }
12  });
13
14  const setValue = (value) => {
15    try {
16      const valueToStore = value instanceof Function ? value(storedValue) : value;
17      setStoredValue(valueToStore);
18      window.localStorage.setItem(key, JSON.stringify(valueToStore));
19    } catch (error) {
20      console.log(error);
21    }
22  };
23
24  return [storedValue, setValue];
25}
26
27// Custom Hook: useToggle
28function useToggle(initialValue = false) {
29  const [value, setValue] = useState(initialValue);
30  const toggle = useCallback(() => setValue(v => !v), []);
31  return [value, toggle];
32}
33
34// Custom Hook: useCounter
35function useCounter(initialValue = 0, step = 1) {
36  const [count, setCount] = useState(initialValue);
37
38  const increment = useCallback(() => setCount(c => c + step), [step]);
39  const decrement = useCallback(() => setCount(c => c - step), [step]);
40  const reset = useCallback(() => setCount(initialValue), [initialValue]);
41
42  return { count, increment, decrement, reset };
43}
44
45// Custom Hook: useFetch
46function useFetch(url) {
47  const [data, setData] = useState(null);
48  const [loading, setLoading] = useState(true);
49  const [error, setError] = useState(null);
50
51  useEffect(() => {
52    const fetchData = async () => {
53      setLoading(true);
54      // Symulacja fetch
55      setTimeout(() => {
56        setData({
57          ships: ['Explorer-1', 'Titan-7', 'Guardian-3'],
58          missions: 12,
59          crew: 45
60        });
61        setLoading(false);
62      }, 1500);
63    };
64    fetchData();
65  }, [url]);
66
67  return { data, loading, error };
68}
69
70// Custom Hook: useWindowSize
71function useWindowSize() {
72  const [size, setSize] = useState({
73    width: window.innerWidth,
74    height: window.innerHeight
75  });
76
77  useEffect(() => {
78    const handleResize = () => {
79      setSize({ width: window.innerWidth, height: window.innerHeight });
80    };
81    window.addEventListener('resize', handleResize);
82    return () => window.removeEventListener('resize', handleResize);
83  }, []);
84
85  return size;
86}
87
88// Custom Hook: usePrevious
89function usePrevious(value) {
90  const ref = useRef();
91  useEffect(() => {
92    ref.current = value;
93  }, [value]);
94  return ref.current;
95}
96
97// Demo Component
98function CustomHooksDemo() {
99  // useLocalStorage demo
100  const [name, setName] = useLocalStorage('astronaut-name', 'Kosmonauta');
101
102  // useToggle demo
103  const [isShieldActive, toggleShield] = useToggle(false);
104
105  // useCounter demo
106  const { count: fuel, increment: addFuel, decrement: useFuel, reset: resetFuel } = useCounter(50, 10);
107
108  // useFetch demo
109  const { data, loading, error } = useFetch('/api/station');
110
111  // useWindowSize demo
112  const { width, height } = useWindowSize();
113
114  // usePrevious demo
115  const previousFuel = usePrevious(fuel);
116
117  return (
118    <div style={{
119      fontFamily: 'Arial, sans-serif',
120      padding: '20px',
121      maxWidth: '800px',
122      margin: '0 auto',
123      backgroundColor: '#0a0a1a',
124      minHeight: '100vh',
125      color: 'white'
126    }}>
127      <h1 style={{ textAlign: 'center', color: '#00d4ff' }}>Custom Hooks</h1>
128
129      {/* useLocalStorage */}
130      <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
131        <h2 style={{ color: '#4caf50' }}>useLocalStorage</h2>
132        <p>Nazwa astronauty (zapisywana w localStorage):</p>
133        <input
134          type="text"
135          value={name}
136          onChange={(e) => setName(e.target.value)}
137          style={{
138            padding: '10px',
139            width: '100%',
140            boxSizing: 'border-box',
141            borderRadius: '8px',
142            border: '1px solid #333',
143            backgroundColor: '#252545',
144            color: 'white'
145          }}
146        />
147        <p style={{ color: '#888', fontSize: '14px' }}>Odśwież stronę - wartość zostanie zachowana!</p>
148      </section>
149
150      {/* useToggle */}
151      <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
152        <h2 style={{ color: '#ff9800' }}>useToggle</h2>
153        <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
154          <button
155            onClick={toggleShield}
156            style={{
157              padding: '12px 24px',
158              backgroundColor: isShieldActive ? '#4caf50' : '#f44336',
159              color: 'white',
160              border: 'none',
161              borderRadius: '8px',
162              cursor: 'pointer'
163            }}
164          >
165            {isShieldActive ? 'Tarcze AKTYWNE' : 'Tarcze WYŁĄCZONE'}
166          </button>
167        </div>
168      </section>
169
170      {/* useCounter */}
171      <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
172        <h2 style={{ color: '#2196f3' }}>useCounter</h2>
173        <div style={{ textAlign: 'center' }}>
174          <div style={{ fontSize: '48px', fontWeight: 'bold', color: fuel < 30 ? '#f44336' : '#4caf50' }}>
175            {fuel}%
176          </div>
177          <p>Paliwo (poprzednio: {previousFuel ?? '-'}%)</p>
178          <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
179            <button onClick={useFuel} style={{ padding: '10px 20px', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>-10</button>
180            <button onClick={resetFuel} style={{ padding: '10px 20px', backgroundColor: '#666', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>Reset</button>
181            <button onClick={addFuel} style={{ padding: '10px 20px', backgroundColor: '#4caf50', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>+10</button>
182          </div>
183        </div>
184      </section>
185
186      {/* useFetch */}
187      <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
188        <h2 style={{ color: '#9c27b0' }}>useFetch</h2>
189        {loading ? (
190          <p>Ładowanie danych stacji... ⏳</p>
191        ) : (
192          <div>
193            <p>Statki: {data?.ships?.join(', ')}</p>
194            <p>Aktywne misje: {data?.missions}</p>
195            <p>Członkowie załogi: {data?.crew}</p>
196          </div>
197        )}
198      </section>
199
200      {/* useWindowSize */}
201      <section style={{ padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
202        <h2 style={{ color: '#e91e63' }}>useWindowSize</h2>
203        <p>Rozmiar okna: {width} x {height} px</p>
204        <p style={{ color: '#888', fontSize: '14px' }}>Zmień rozmiar okna przeglądarki!</p>
205      </section>
206    </div>
207  );
208}
209
210export 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. Kiedy używana jest domyślna wartość kontekstu przekazana do createContext?

  2. 2. Co się stanie, gdy komponent znajdzie się między dwoma Provider'ami tego samego kontekstu?

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj co się dzieje gdy zmienia się wartość w Provider:

  • Edytor kodu

    Implementuj system inwentarza kosmicznego

  • Układanie w pionie

    Uporządkuj etapy tworzenia systemu zarządzania flotą galaktyczną:

  • Edytor kodu

    Zaimplementuj centrum dowodzenia flotą

Przydatne artykuły