Kurs JavaScript i React · Moduł 4: Context API i useReducer
Globalne zarządzanie stanem
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:
- Współdzielenie danych - komponenty w różnych częściach drzewa komponentów mogą potrzebować dostępu do tych samych danych
- Synchronizacja - zmiany w jednym miejscu powinny być widoczne wszędzie, gdzie dane są używane
- Przewidywalność - aktualizacje stanu powinny być przewidywalne i łatwe do śledzenia
- Wydajność - niepotrzebne renderowanie powinno być minimalizowane
- 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ł
createStorei 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:
Rozmiar aplikacji:
- Małe aplikacje: useState + useReducer + Context API
- Średnie aplikacje: Zustand lub Jotai
- Duże aplikacje: Redux (z Redux Toolkit) lub Recoil
Złożoność stanu:
- Prosty stan: useState
- Średnio złożony stan: useReducer + Context API
- Złożony stan z relacjami: Redux lub Recoil
Wymagania dotyczące wydajności:
- Jeśli wydajność renderowania jest kluczowa, rozważ biblioteki, które zapewniają granularną reaktywność (Recoil, Jotai)
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:
Minimalizuj globalny stan - nie wszystko musi być globalne; używaj lokalnego stanu dla danych specyficznych dla komponentu
Dziel stan według domen logicznych - grupuj powiązane dane i logikę razem
Normalizuj dane - unikaj duplikacji i głębokich zagnieżdżeń
Zachowuj niezmienność - nigdy nie modyfikuj stanu bezpośrednio
Dokumentuj intencje - używaj opisowych nazw akcji i selektorów
Używaj selektorów - do pobierania pochodnych danych
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:
Context API + useReducer - wbudowane rozwiązanie React, dobre dla mniejszych i średnich aplikacji
Podział na wiele kontekstów - sposób na organizację globalnego stanu według domen
Optymalizacja wydajności - techniki minimalizacji przerenderowywania
Redux - tradycyjna biblioteka z ustrukturyzowanym podejściem do zarządzania stanem
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. Kiedy używana jest domyślna wartość kontekstu przekazana do createContext?
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ą