Kurs JavaScript i React · Moduł 9: Nowoczesne hooki React
use - uniwersalny hook do obsługi Promise i Context
W tej lekcji6
Stacja orbitalna czeka na raport z sondy. Do tej pory w React oznaczało to trzy stany (loading, error, data), efekt z fetch i sporo warunków w JSX. Do tego zwykłe hooki nie mogą stać w if, więc kontekst odczytywałeś nawet wtedy, gdy go nie potrzebowałeś.
use to API React, które pozwala na "używanie" zasobów takich jak Promise czy Context. W React 19 jest stabilne (wcześniej było dostępne tylko w wersjach eksperymentalnych). Jest to pierwszy hook, który może być wywoływany warunkowo i w pętlach. Nadal jednak musi być wywołany wewnątrz komponentu lub hooka, a według react.dev nie wolno go umieszczać w bloku try/catch.
Podstawy use
1. Czytanie Promise z use
Gdy przekażesz do use obiekt Promise, komponent "zawiesza się", a najbliższa granica Suspense pokazuje fallback do czasu rozwiązania obietnicy:
1import { use, Suspense } from 'react';
2
3// Funkcja zwracająca Promise
4function fetchUser(id) {
5 return fetch(`/api/users/${id}`).then(res => res.json());
6}
7
8// Komponent używający use z Promise
9function UserProfile({ userPromise }) {
10 // use automatycznie "rozpakowuje" Promise
11 const user = use(userPromise);
12
13 return (
14 <div style={{
15 padding: '20px',
16 border: '1px solid #ddd',
17 borderRadius: '8px'
18 }}>
19 <h2>{user.name}</h2>
20 <p>Email: {user.email}</p>
21 <p>Dołączył: {new Date(user.joinedDate).toLocaleDateString('pl-PL')}</p>
22 </div>
23 );
24}
25
26// Komponent rodzic
27function UserDashboard({ userId }) {
28 // Uwaga: nowy Promise powstaje przy KAŻDYM renderze rodzica (rozwiązanie: cache, niżej)
29 const userPromise = fetchUser(userId);
30
31 return (
32 <div>
33 <h1>Panel użytkownika</h1>
34 <Suspense fallback={<div>Ładowanie danych użytkownika...</div>}>
35 <UserProfile userPromise={userPromise} />
36 </Suspense>
37 </div>
38 );
39}UserProfile czyta dane jak zwykłą wartość, bez stanu ładowania. Problem tkwi w rodzicu: fetchUser(userId) tworzy nowy Promise przy każdym renderze UserDashboard, więc każde przerysowanie wysyła nowe zapytanie. React zaleca Promise pochodzące z Server Component, z frameworka albo z cache. Takie cache zbudujemy w sekcji zaawansowanej.
2. Używanie use z Context
use odczytuje też kontekst, tak jak useContext, ale może stać w warunku. Zobacz trzy komponenty korzystające z dwóch kontekstów:
1import { use, createContext, useState } from 'react';
2
3// Tworzenie Context
4const ThemeContext = createContext(null);
5const UserContext = createContext(null);
6
7// Komponent używający use z Context
8function Header() {
9 // use może być używany warunkowo!
10 const theme = use(ThemeContext);
11 const user = use(UserContext);
12
13 return (
14 <header style={{
15 backgroundColor: theme === 'dark' ? '#333' : '#f8f9fa',
16 color: theme === 'dark' ? '#fff' : '#333',
17 padding: '20px'
18 }}>
19 <h1>Witaj, {user.name}!</h1>
20 <p>Aktualny motyw: {theme}</p>
21 </header>
22 );
23}
24
25// Możliwość warunkowego użycia
26function ConditionalComponent({ showUserInfo }) {
27 const theme = use(ThemeContext);
28
29 // use może być używany warunkowo - to niemożliwe z innymi hooks!
30 if (showUserInfo) {
31 const user = use(UserContext);
32 return <div>Użytkownik: {user.name}</div>;
33 }
34
35 return <div>Motyw: {theme}</div>;
36}
37
38// App z providers
39function App() {
40 const [theme, setTheme] = useState('light');
41 const [user] = useState({ name: 'Jan Kowalski', id: 1 });
42
43 return (
44 <ThemeContext.Provider value={theme}>
45 <UserContext.Provider value={user}>
46 <div>
47 <Header />
48 <ConditionalComponent showUserInfo={true} />
49 <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
50 Zmień motyw
51 </button>
52 </div>
53 </UserContext.Provider>
54 </ThemeContext.Provider>
55 );
56}ConditionalComponent sięga po UserContext tylko wtedy, gdy ma pokazać użytkownika. Dostawców zapisaliśmy jako ThemeContext.Provider, co nadal działa, ale od React 19 wystarczy krótszy zapis <ThemeContext value={theme}>, a Provider ma zostać w przyszłości wycofany.
3. use w pętlach
Hook może też wystąpić w pętli, co jest nie do pomyślenia przy useState. Kluczowe jest, by każdy Promise pochodził z cache:
1import { use, Suspense } from 'react';
2
3// Funkcja tworząca Promise dla każdego posta
4const postCache = new Map();
5
6function fetchPost(id) {
7 if (!postCache.has(id)) {
8 postCache.set(id, fetch(`/api/posts/${id}`).then(res => res.json()));
9 }
10 return postCache.get(id);
11}
12
13// Komponent wyświetlający wiele postów
14function PostList({ postIds }) {
15 const posts = [];
16 for (const id of postIds) {
17 // use może być wywołany w pętli!
18 posts.push(use(fetchPost(id)));
19 }
20
21 return (
22 <div>
23 {posts.map(post => (
24 <article
25 key={post.id}
26 style={{
27 marginBottom: '20px',
28 padding: '15px',
29 backgroundColor: '#f8f9fa',
30 borderRadius: '8px'
31 }}
32 >
33 <h3>{post.title}</h3>
34 <p>{post.content}</p>
35 <small>Autor: {post.author}</small>
36 </article>
37 ))}
38 </div>
39 );
40}
41
42// Wrapper z Suspense
43function BlogSection() {
44 const postIds = [1, 2, 3, 4, 5];
45
46 return (
47 <section>
48 <h2>Najnowsze posty</h2>
49 <Suspense fallback={<PostListSkeleton count={5} />}>
50 <PostList postIds={postIds} />
51 </Suspense>
52 </section>
53 );
54}
55
56function PostListSkeleton({ count }) {
57 return (
58 <div>
59 {Array.from({ length: count }).map((_, i) => (
60 <div
61 key={i}
62 style={{
63 marginBottom: '20px',
64 padding: '15px',
65 backgroundColor: '#e9ecef',
66 borderRadius: '8px',
67 height: '100px'
68 }}
69 />
70 ))}
71 </div>
72 );
73}Pętla for...of stoi wprost w ciele komponentu. Gdyby use siedział w callbacku przekazanym do map, wywołałaby go zwykła funkcja, a nie komponent, i linter Reacta zgłosiłby błąd. Funkcja fetchPost trzyma obietnice w postCache. Bez tego każda ponowna próba renderu po zawieszeniu tworzyłaby nowe obietnice i lista nigdy by się nie załadowała.
Zaawansowane wzorce z use
1. Cache dla Promise
Uogólnijmy ten pomysł. getCachedPromise przyjmuje klucz i fabrykę, a Promise powstaje tylko przy pierwszym zapytaniu o dany klucz:
1import { use, Suspense, useState } from 'react';
2
3// Prosty cache dla Promise
4const promiseCache = new Map();
5
6function getCachedPromise(key, promiseFactory) {
7 if (!promiseCache.has(key)) {
8 promiseCache.set(key, promiseFactory());
9 }
10 return promiseCache.get(key);
11}
12
13// API functions
14function fetchUserData(userId) {
15 const cached = getCachedPromise(
16 `user-${userId}`,
17 () => fetch(`/api/users/${userId}`).then(res => res.json())
18 );
19 return cached;
20}
21
22function fetchUserPosts(userId) {
23 const cached = getCachedPromise(
24 `posts-${userId}`,
25 () => fetch(`/api/users/${userId}/posts`).then(res => res.json())
26 );
27 return cached;
28}
29
30// Komponenty
31function UserInfo({ userId }) {
32 const userPromise = fetchUserData(userId);
33 const user = use(userPromise);
34
35 return (
36 <div style={{
37 padding: '20px',
38 backgroundColor: '#f8f9fa',
39 borderRadius: '8px',
40 marginBottom: '20px'
41 }}>
42 <h2>{user.name}</h2>
43 <p>{user.email}</p>
44 <p>{user.location}</p>
45 <p>Dołączył: {new Date(user.joinedDate).toLocaleDateString('pl-PL')}</p>
46 </div>
47 );
48}
49
50function UserPosts({ userId }) {
51 const postsPromise = fetchUserPosts(userId);
52 const posts = use(postsPromise);
53
54 return (
55 <div>
56 <h3>Posty użytkownika ({posts.length})</h3>
57 {posts.map(post => (
58 <article
59 key={post.id}
60 style={{
61 marginBottom: '15px',
62 padding: '15px',
63 border: '1px solid #e9ecef',
64 borderRadius: '6px'
65 }}
66 >
67 <h4>{post.title}</h4>
68 <p>{post.excerpt}</p>
69 <small>{new Date(post.date).toLocaleDateString('pl-PL')}</small>
70 </article>
71 ))}
72 </div>
73 );
74}
75
76function UserProfilePage() {
77 const [userId, setUserId] = useState(1);
78
79 return (
80 <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
81 <div style={{ marginBottom: '20px' }}>
82 <button onClick={() => setUserId(1)} disabled={userId === 1}>
83 Użytkownik 1
84 </button>
85 <button onClick={() => setUserId(2)} disabled={userId === 2}>
86 Użytkownik 2
87 </button>
88 <button onClick={() => promiseCache.clear()}>
89 Wyczyść cache
90 </button>
91 </div>
92
93 <Suspense fallback={<div>Ładowanie profilu...</div>}>
94 <UserInfo userId={userId} />
95 </Suspense>
96
97 <Suspense fallback={<div>Ładowanie postów...</div>}>
98 <UserPosts userId={userId} />
99 </Suspense>
100 </div>
101 );
102}Dwie osobne granice Suspense sprawiają, że profil i posty pojawiają się niezależnie. Przycisk "Wyczyść cache" usuwa obietnice, ale nie wymusza nowego renderu, więc nowe dane pobiorą się przy następnej zmianie użytkownika.
2. Równoległe ładowanie danych
Promise.all pobiera kilka zasobów jednocześnie, a use czeka na wszystkie naraz. Najpierw funkcja API i komponent dashboardu:
1import { use, Suspense } from 'react';
2
3// Funkcje API
4async function fetchDashboardData() {
5 const [stats, activities, notifications] = await Promise.all([
6 fetch('/api/stats').then(res => res.json()),
7 fetch('/api/activities').then(res => res.json()),
8 fetch('/api/notifications').then(res => res.json())
9 ]);
10
11 return { stats, activities, notifications };
12}
13
14// Komponent dashboardu
15function Dashboard({ dataPromise }) {
16 const { stats, activities, notifications } = use(dataPromise);
17
18 return (
19 <div style={{
20 display: 'grid',
21 gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
22 gap: '20px',
23 padding: '20px'
24 }}>
25 <StatsCard stats={stats} />
26 <ActivitiesCard activities={activities} />
27 <NotificationsCard notifications={notifications} />
28 </div>
29 );
30}
31Dane trafiają do trzech kart przez propsy, więc karty są zwykłymi komponentami. Oto ich implementacja:
1function StatsCard({ stats }) {
2 return (
3 <div style={{
4 padding: '20px',
5 backgroundColor: '#f8f9fa',
6 borderRadius: '8px',
7 boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
8 }}>
9 <h3>Statystyki</h3>
10 <div style={{ display: 'grid', gap: '10px' }}>
11 <div>
12 <strong>Użytkownicy:</strong> {stats.users.toLocaleString('pl-PL')}
13 </div>
14 <div>
15 <strong>Przychód:</strong> {stats.revenue.toLocaleString('pl-PL', {
16 style: 'currency',
17 currency: 'PLN'
18 })}
19 </div>
20 <div>
21 <strong>Wzrost:</strong>
22 <span style={{ color: stats.growth > 0 ? '#28a745' : '#dc3545' }}>
23 {stats.growth > 0 ? '+' : ''}{stats.growth}%
24 </span>
25 </div>
26 </div>
27 </div>
28 );
29}
30
31function ActivitiesCard({ activities }) {
32 return (
33 <div style={{
34 padding: '20px',
35 backgroundColor: '#f8f9fa',
36 borderRadius: '8px',
37 boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
38 }}>
39 <h3>Ostatnie aktywności</h3>
40 <ul style={{ listStyle: 'none', padding: 0 }}>
41 {activities.slice(0, 5).map(activity => (
42 <li
43 key={activity.id}
44 style={{
45 padding: '8px 0',
46 borderBottom: '1px solid #e9ecef'
47 }}
48 >
49 <div style={{ fontWeight: '500' }}>{activity.user}</div>
50 <div style={{ fontSize: '14px', color: '#6c757d' }}>
51 {activity.action}
52 </div>
53 <small style={{ color: '#6c757d' }}>
54 {new Date(activity.timestamp).toLocaleString('pl-PL')}
55 </small>
56 </li>
57 ))}
58 </ul>
59 </div>
60 );
61}
62
63function NotificationsCard({ notifications }) {
64 const unreadCount = notifications.filter(n => !n.read).length;
65
66 return (
67 <div style={{
68 padding: '20px',
69 backgroundColor: '#f8f9fa',
70 borderRadius: '8px',
71 boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
72 }}>
73 <h3>
74 Powiadomienia
75 {unreadCount > 0 && (
76 <span style={{
77 marginLeft: '10px',
78 padding: '2px 8px',
79 backgroundColor: '#dc3545',
80 color: 'white',
81 borderRadius: '12px',
82 fontSize: '14px'
83 }}>
84 {unreadCount}
85 </span>
86 )}
87 </h3>
88 <div style={{ maxHeight: '200px', overflowY: 'auto' }}>
89 {notifications.map(notification => (
90 <div
91 key={notification.id}
92 style={{
93 padding: '10px',
94 marginBottom: '8px',
95 backgroundColor: notification.read ? 'transparent' : '#e3f2fd',
96 borderRadius: '4px',
97 fontSize: '14px'
98 }}
99 >
100 <strong>{notification.title}</strong>
101 <p style={{ margin: '4px 0' }}>{notification.message}</p>
102 </div>
103 ))}
104 </div>
105 </div>
106 );
107}
108Żadna karta nie wie o Promise ani o Suspense. Na koniec komponent główny i szkielet:
1// Główny komponent
2function DashboardApp() {
3 const dataPromise = fetchDashboardData();
4
5 return (
6 <div>
7 <h1 style={{ padding: '20px' }}>Panel administracyjny</h1>
8 <Suspense fallback={<DashboardSkeleton />}>
9 <Dashboard dataPromise={dataPromise} />
10 </Suspense>
11 </div>
12 );
13}
14
15function DashboardSkeleton() {
16 return (
17 <div style={{
18 display: 'grid',
19 gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
20 gap: '20px',
21 padding: '20px'
22 }}>
23 {[1, 2, 3].map(i => (
24 <div
25 key={i}
26 style={{
27 height: '250px',
28 backgroundColor: '#e9ecef',
29 borderRadius: '8px',
30 animation: 'pulse 1.5s ease-in-out infinite'
31 }}
32 />
33 ))}
34 </div>
35 );
36}Tu znów Promise powstaje w renderze DashboardApp, więc w prawdziwej aplikacji przenieś go do cache albo do Server Component.
3. Error boundaries z use
Odrzucony Promise nie trafia do try/catch, tylko do najbliższego Error Boundary. Najpierw sama granica błędów, która wciąż wymaga komponentu klasowego:
1import { use, Suspense, Component, useState } from 'react';
2
3// Error Boundary
4class ErrorBoundary extends Component {
5 constructor(props) {
6 super(props);
7 this.state = { hasError: false, error: null };
8 }
9
10 static getDerivedStateFromError(error) {
11 return { hasError: true, error };
12 }
13
14 componentDidCatch(error, errorInfo) {
15 console.error('Error caught by boundary:', error, errorInfo);
16 }
17
18 render() {
19 if (this.state.hasError) {
20 return (
21 <div style={{
22 padding: '20px',
23 backgroundColor: '#f8d7da',
24 border: '1px solid #f5c6cb',
25 borderRadius: '8px',
26 color: '#721c24'
27 }}>
28 <h3>Coś poszło nie tak</h3>
29 <p>{this.state.error?.message || 'Nieznany błąd'}</p>
30 <button
31 onClick={() => this.setState({ hasError: false, error: null })}
32 style={{
33 marginTop: '10px',
34 padding: '8px 16px',
35 backgroundColor: '#721c24',
36 color: 'white',
37 border: 'none',
38 borderRadius: '4px',
39 cursor: 'pointer'
40 }}
41 >
42 Spróbuj ponownie
43 </button>
44 </div>
45 );
46 }
47
48 return this.props.children;
49 }
50}
51getDerivedStateFromError przełącza widok na komunikat, a componentDidCatch loguje błąd. Teraz demo, które potrafi symulować awarię:
1// Funkcja API która może zwrócić błąd
2function fetchDataWithError(shouldFail = false) {
3 return new Promise((resolve, reject) => {
4 setTimeout(() => {
5 if (shouldFail) {
6 reject(new Error('Błąd pobierania danych z serwera'));
7 } else {
8 resolve({
9 message: 'Dane załadowane pomyślnie!',
10 timestamp: new Date().toISOString()
11 });
12 }
13 }, 1000);
14 });
15}
16
17// Komponent używający use
18function DataDisplay({ dataPromise }) {
19 const data = use(dataPromise);
20
21 return (
22 <div style={{
23 padding: '20px',
24 backgroundColor: '#d4edda',
25 border: '1px solid #c3e6cb',
26 borderRadius: '8px',
27 color: '#155724'
28 }}>
29 <h3>Sukces!</h3>
30 <p>{data.message}</p>
31 <small>Załadowano: {new Date(data.timestamp).toLocaleString('pl-PL')}</small>
32 </div>
33 );
34}
35
36// Główny komponent
37function ErrorHandlingDemo() {
38 const [shouldFail, setShouldFail] = useState(false);
39 const [key, setKey] = useState(0);
40
41 const dataPromise = fetchDataWithError(shouldFail);
42
43 return (
44 <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
45 <h2>Obsługa błędów z use</h2>
46
47 <div style={{ marginBottom: '20px' }}>
48 <label>
49 <input
50 type="checkbox"
51 checked={shouldFail}
52 onChange={(e) => setShouldFail(e.target.checked)}
53 />
54 Symuluj błąd
55 </label>
56 <button
57 onClick={() => setKey(k => k + 1)}
58 style={{ marginLeft: '20px' }}
59 >
60 Odśwież
61 </button>
62 </div>
63
64 <ErrorBoundary key={key}>
65 <Suspense fallback={
66 <div style={{
67 padding: '20px',
68 backgroundColor: '#cfe2ff',
69 border: '1px solid #b6d4fe',
70 borderRadius: '8px',
71 color: '#084298'
72 }}>
73 Ładowanie danych...
74 </div>
75 }>
76 <DataDisplay dataPromise={dataPromise} />
77 </Suspense>
78 </ErrorBoundary>
79 </div>
80 );
81}Zmiana key na ErrorBoundary tworzy granicę od nowa, co resetuje błąd. Zamiast granicy możesz też dopisać do obietnicy .catch(), zwracający wartość zastępczą.
Praktyczne zastosowania use
1. System routingu z lazy loading
Prosty router łączy lazy, Suspense i kontekst odczytywany przez use:
1import { use, Suspense, lazy, useState, createContext } from 'react';
2
3// Symulacja systemu routingu
4const routes = {
5 '/': lazy(() => import('./pages/Home')),
6 '/about': lazy(() => import('./pages/About')),
7 '/contact': lazy(() => import('./pages/Contact')),
8 '/profile': lazy(() => import('./pages/Profile'))
9};
10
11// Context dla routingu
12const RouteContext = createContext(null);
13
14// Hook do nawigacji
15function useNavigation() {
16 const { navigate } = use(RouteContext);
17 return navigate;
18}
19
20// Komponent strony
21function Page({ path }) {
22 const Component = routes[path] || routes['/'];
23
24 return (
25 <Suspense fallback={<PageLoader />}>
26 <Component />
27 </Suspense>
28 );
29}
30
31function PageLoader() {
32 return (
33 <div style={{
34 display: 'flex',
35 justifyContent: 'center',
36 alignItems: 'center',
37 minHeight: '400px'
38 }}>
39 <div style={{ textAlign: 'center' }}>
40 <div style={{
41 width: '50px',
42 height: '50px',
43 border: '3px solid #f3f3f3',
44 borderTop: '3px solid #007bff',
45 borderRadius: '50%',
46 animation: 'spin 1s linear infinite',
47 margin: '0 auto 20px'
48 }} />
49 <p>Ładowanie strony...</p>
50 </div>
51 </div>
52 );
53}
54
55// Główna aplikacja
56function RouterApp() {
57 const [currentPath, setCurrentPath] = useState('/');
58
59 const navigate = (path) => {
60 setCurrentPath(path);
61 window.history.pushState({}, '', path);
62 };
63
64 return (
65 <RouteContext.Provider value={{ navigate, currentPath }}>
66 <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
67 <Navigation />
68 <main style={{ flex: 1, padding: '20px' }}>
69 <Page path={currentPath} />
70 </main>
71 </div>
72 </RouteContext.Provider>
73 );
74}
75
76function Navigation() {
77 const navigate = useNavigation();
78 const { currentPath } = use(RouteContext);
79
80 const links = [
81 { path: '/', label: 'Strona główna' },
82 { path: '/about', label: 'O nas' },
83 { path: '/contact', label: 'Kontakt' },
84 { path: '/profile', label: 'Profil' }
85 ];
86
87 return (
88 <nav style={{
89 backgroundColor: '#333',
90 padding: '10px 20px'
91 }}>
92 {links.map(link => (
93 <button
94 key={link.path}
95 onClick={() => navigate(link.path)}
96 style={{
97 marginRight: '10px',
98 padding: '8px 16px',
99 backgroundColor: currentPath === link.path ? '#007bff' : 'transparent',
100 color: 'white',
101 border: 'none',
102 borderRadius: '4px',
103 cursor: 'pointer'
104 }}
105 >
106 {link.label}
107 </button>
108 ))}
109 </nav>
110 );
111}lazy ładuje kod strony dopiero przy pierwszym wejściu, a Suspense pokazuje PageLoader. To tylko model, bo w praktyce użyjesz React Router lub routera frameworka.
2. Dynamiczne formularze z walidacją
Formularz trzyma stan w kontekście, a pola odczytują go przez use. Najpierw kontekst, hook pola i komponent FormField:
1import { use, createContext, useState } from 'react';
2
3// Context dla formularza
4const FormContext = createContext(null);
5
6// Hook do zarządzania formularzem
7function useFormField(name) {
8 const form = use(FormContext);
9
10 return {
11 value: form.values[name] || '',
12 error: form.errors[name],
13 onChange: (e) => form.setValue(name, e.target.value),
14 onBlur: () => form.validateField(name)
15 };
16}
17
18// Komponent pola formularza
19function FormField({ name, label, type = 'text', validate }) {
20 const field = useFormField(name);
21 const form = use(FormContext);
22
23 // Rejestracja walidatora
24 if (validate && !form.validators[name]) {
25 form.registerValidator(name, validate);
26 }
27
28 return (
29 <div style={{ marginBottom: '15px' }}>
30 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
31 {label}
32 </label>
33 <input
34 type={type}
35 value={field.value}
36 onChange={field.onChange}
37 onBlur={field.onBlur}
38 style={{
39 width: '100%',
40 padding: '8px',
41 border: `1px solid ${field.error ? '#dc3545' : '#ddd'}`,
42 borderRadius: '4px'
43 }}
44 />
45 {field.error && (
46 <div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
47 {field.error}
48 </div>
49 )}
50 </div>
51 );
52}
53Każde pole rejestruje swój walidator i pokazuje własny błąd. Teraz dostawca, który trzyma wartości i uruchamia walidację:
1// Provider formularza
2function FormProvider({ children, onSubmit }) {
3 const [values, setValues] = useState({});
4 const [errors, setErrors] = useState({});
5 const [validators] = useState({});
6
7 const setValue = (name, value) => {
8 setValues(prev => ({ ...prev, [name]: value }));
9 // Czyść błąd przy zmianie wartości
10 setErrors(prev => ({ ...prev, [name]: '' }));
11 };
12
13 const registerValidator = (name, validate) => {
14 validators[name] = validate;
15 };
16
17 const validateField = (name) => {
18 if (validators[name]) {
19 const error = validators[name](values[name], values);
20 setErrors(prev => ({ ...prev, [name]: error }));
21 return !error;
22 }
23 return true;
24 };
25
26 const validateAll = () => {
27 const newErrors = {};
28 let isValid = true;
29
30 Object.keys(validators).forEach(name => {
31 const error = validators[name](values[name], values);
32 if (error) {
33 newErrors[name] = error;
34 isValid = false;
35 }
36 });
37
38 setErrors(newErrors);
39 return isValid;
40 };
41
42 const handleSubmit = (e) => {
43 e.preventDefault();
44 if (validateAll()) {
45 onSubmit(values);
46 }
47 };
48
49 const formValue = {
50 values,
51 errors,
52 validators,
53 setValue,
54 registerValidator,
55 validateField,
56 handleSubmit
57 };
58
59 return (
60 <FormContext.Provider value={formValue}>
61 <form onSubmit={handleSubmit}>
62 {children}
63 </form>
64 </FormContext.Provider>
65 );
66}
67Walidatory dostają drugi argument, czyli wszystkie wartości formularza. To ważne w przykładzie użycia:
1// Przykład użycia
2function RegistrationFormExample() {
3 const handleSubmit = (values) => {
4 console.log('Form submitted:', values);
5 alert('Rejestracja zakończona sukcesem!');
6 };
7
8 return (
9 <div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
10 <h2>Formularz rejestracji</h2>
11
12 <FormProvider onSubmit={handleSubmit}>
13 <FormField
14 name="email"
15 label="Email"
16 type="email"
17 validate={(value) => {
18 if (!value) return 'Email jest wymagany';
19 if (!/\S+@\S+\.\S+/.test(value)) return 'Nieprawidłowy email';
20 return '';
21 }}
22 />
23
24 <FormField
25 name="password"
26 label="Hasło"
27 type="password"
28 validate={(value) => {
29 if (!value) return 'Hasło jest wymagane';
30 if (value.length < 8) return 'Hasło musi mieć minimum 8 znaków';
31 return '';
32 }}
33 />
34
35 <FormField
36 name="confirmPassword"
37 label="Potwierdź hasło"
38 type="password"
39 validate={(value, allValues) => {
40 if (value !== allValues.password) return 'Hasła nie są identyczne';
41 return '';
42 }}
43 />
44
45 <button
46 type="submit"
47 style={{
48 width: '100%',
49 padding: '10px',
50 backgroundColor: '#007bff',
51 color: 'white',
52 border: 'none',
53 borderRadius: '4px',
54 fontSize: '16px',
55 cursor: 'pointer'
56 }}
57 >
58 Zarejestruj się
59 </button>
60 </FormProvider>
61 </div>
62 );
63}Pole "Potwierdź hasło" porównuje wartość z allValues.password. Nie może zawołać use w walidatorze, bo use działa tylko podczas renderu komponentu lub hooka, a walidator uruchamia się w obsłudze zdarzenia.
Najlepsze praktyki z use
1. Kiedy używać use
Oto dobre i złe zastosowania w pigułce:
1// DOBRZE - warunkowe użycie
2function ConditionalData({ showDetails }) {
3 if (showDetails) {
4 const details = use(detailsPromise);
5 return <DetailView details={details} />;
6 }
7 return <Summary />;
8}
9
10// DOBRZE - w pętlach
11function MultipleResources({ resourceIds }) {
12 const resources = [];
13 for (const id of resourceIds) {
14 resources.push(use(fetchResource(id)));
15 }
16 return <ResourceList resources={resources} />;
17}
18
19// ŹLE - niepotrzebne użycie dla prostych wartości
20function BadExample() {
21 const value = use(Promise.resolve(42)); // Niepotrzebne!
22 return <div>{value}</div>;
23}W MultipleResources funkcja fetchResource musi zwracać Promise z cache, tak jak fetchPost wyżej.
2. Cache i optymalizacja
Cache można zamknąć we własnym hooku:
1// Implementacja prostego cache
2const cache = new Map();
3
4function useCachedResource(key, fetcher) {
5 if (!cache.has(key)) {
6 cache.set(key, fetcher());
7 }
8
9 const promise = cache.get(key);
10 const resource = use(promise);
11
12 return resource;
13}
14
15// Użycie
16function CachedDataComponent({ userId }) {
17 const userData = useCachedResource(
18 `user-${userId}`,
19 () => fetchUser(userId)
20 );
21
22 return <UserProfile user={userData} />;
23}useCachedResource to zwykły custom hook, a Map na poziomie modułu przetrwa między renderami.
3. Obsługa błędów
Ponawianie z wykładniczym odstępem da się zamknąć w fabryce obietnic:
1// Wrapper z retry logic
2function createRetryablePromise(fetcher, maxRetries = 3) {
3 return async () => {
4 let lastError;
5
6 for (let i = 0; i < maxRetries; i++) {
7 try {
8 return await fetcher();
9 } catch (error) {
10 lastError = error;
11 if (i < maxRetries - 1) {
12 // Exponential backoff
13 await new Promise(resolve =>
14 setTimeout(resolve, Math.pow(2, i) * 1000)
15 );
16 }
17 }
18 }
19
20 throw lastError;
21 };
22}
23
24// Użycie
25function RobustDataFetching() {
26 const dataPromise = createRetryablePromise(
27 () => fetch('/api/critical-data').then(res => res.json())
28 )();
29
30 return (
31 <ErrorBoundary>
32 <Suspense fallback={<div>Ładowanie...</div>}>
33 <DataComponent dataPromise={dataPromise} />
34 </Suspense>
35 </ErrorBoundary>
36 );
37}Tu także Promise powstaje w renderze, więc w produkcji połącz go z cache.
Powtórka przed zadaniami: memo z własnym porównaniem i proces optymalizacji
Zadania po tej lekcji wracają do memo. Domyślnie memo porównuje każdy props płytko, przez Object.is. Drugim argumentem możesz podać własną funkcję arePropsEqual(prevProps, nextProps): gdy zwróci true, React uzna propsy za równe i pominie render, a gdy zwróci false, wyrenderuje komponent:
1import { memo } from 'react';
2
3const TelemetryPanel = memo(function TelemetryPanel({ readings, title }) {
4 return <Chart data={readings} label={title} />;
5}, (prevProps, nextProps) =>
6 // true = propsy uznajemy za równe, więc React pominie render
7 prevProps.readings.length === nextProps.readings.length && prevProps.title === nextProps.title
8);Funkcja nie robi niczego sama z siebie, na przykład nie włącza głębokiego porównania obiektów: liczy się tylko to, co w niej napiszesz. Pomyłka w porównaniu oznacza nieaktualny ekran, dlatego sięgaj po nią rzadko.
Każda z tych technik to narzędzie, a nie cel. Optymalizację prowadź zawsze w tej samej kolejności: najpierw pomiar początkowy (baseline), potem profilowanie, które pokaże, gdzie ucieka czas renderu, następnie identyfikacja wąskiego gardła i implementacja jednej poprawki. Na końcu mierzysz wpływ zmiany i uruchamiasz testy regresji, żeby sprawdzić, czy aplikacja działa tak jak wcześniej. Te dwa ostatnie kroki możesz zrobić w dowolnej kolejności, byle żadnego nie pominąć.
Podsumowanie
Hook use otwiera nowe możliwości w React, pozwalając na bardziej elastyczne zarządzanie asynchronicznymi danymi i kontekstem. Jego unikalna zdolność do używania warunkowego i w pętlach czyni go potężnym narzędziem dla zaawansowanych aplikacji.
Moja rada: nie twórz Promise w komponencie klienckim bez cache. Pamiętaj: use to odbiornik stacji, który czeka na raport sondy, ale sam raport musi mieć jeden stały kanał.
Kod do tej lekcji: App.jsx
1import React, { use, createContext, useState, Suspense } from 'react';
2
3// Konteksty aplikacji kosmicznej
4const ThemeContext = createContext('dark');
5const CrewContext = createContext(null);
6
7// Symulacja pobierania danych z API (Promise)
8function fetchMissionData() {
9 return new Promise((resolve) => {
10 setTimeout(() => {
11 resolve({
12 name: 'Misja Andromeda',
13 status: 'W trakcie',
14 crew: 7,
15 distance: '2.537 mln ly',
16 fuelLevel: 78,
17 });
18 }, 1500);
19 });
20}
21
22// Komponent odczytujący dane z Context za pomocą use()
23function CrewPanel() {
24 // use() z Context - działa jak useContext, ale może być warunkowe!
25 const crew = use(CrewContext);
26 const theme = use(ThemeContext);
27
28 const isDark = theme === 'dark';
29 return (
30 <div style={{
31 padding: '16px', marginBottom: '16px',
32 background: isDark ? 'rgba(0, 212, 255, 0.08)' : '#f0f8ff',
33 borderRadius: '10px', border: `1px solid ${isDark ? '#1a3a5c' : '#b0d4f1'}`,
34 }}>
35 <h3 style={{ color: isDark ? '#00d4ff' : '#0066cc', margin: '0 0 8px' }}>
36 Załoga statku
37 </h3>
38 {crew.map((member) => (
39 <div key={member.id} style={{
40 display: 'flex', justifyContent: 'space-between',
41 padding: '6px 0', borderBottom: `1px solid ${isDark ? '#1a2a3c' : '#dde8f0'}`,
42 color: isDark ? '#c0c8d0' : '#333', fontSize: '14px',
43 }}>
44 <span>{member.name}</span>
45 <span style={{ color: isDark ? '#778da9' : '#666' }}>{member.role}</span>
46 </div>
47 ))}
48 </div>
49 );
50}
51
52// Komponent odczytujący Promise za pomocą use()
53function MissionDetails({ missionPromise }) {
54 // use() automatycznie "rozpakowuje" Promise
55 const mission = use(missionPromise);
56
57 return (
58 <div style={{
59 padding: '20px', background: 'rgba(255, 215, 0, 0.08)',
60 borderRadius: '10px', border: '1px solid #3a3a1c',
61 }}>
62 <h3 style={{ color: '#ffd700', margin: '0 0 12px' }}>
63 {mission.name}
64 </h3>
65 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
66 {[
67 ['Status', mission.status],
68 ['Załoga', `${mission.crew} osób`],
69 ['Dystans', mission.distance],
70 ['Paliwo', `${mission.fuelLevel}%`],
71 ].map(([label, value]) => (
72 <div key={label} style={{
73 padding: '10px', background: 'rgba(255, 255, 255, 0.03)',
74 borderRadius: '6px', fontSize: '13px',
75 }}>
76 <div style={{ color: '#778da9', marginBottom: '4px' }}>{label}</div>
77 <div style={{ color: '#e0e1dd', fontWeight: 'bold' }}>{value}</div>
78 </div>
79 ))}
80 </div>
81 </div>
82 );
83}
84
85// use() może stać w warunku: tu kontekst czytamy tylko wtedy, gdy szczegóły są widoczne.
86// Z useContext takie wywołanie po instrukcji return byłoby niedozwolone.
87function ConditionalInfo({ showDetails }) {
88 if (!showDetails) {
89 return (
90 <div style={{
91 padding: '12px', marginTop: '16px',
92 background: 'rgba(255, 255, 255, 0.03)',
93 borderRadius: '8px', color: '#778da9', fontSize: '13px',
94 }}>
95 Szczegóły ukryte
96 </div>
97 );
98 }
99 const crew = use(CrewContext);
100 return (
101 <div style={{
102 padding: '12px', marginTop: '16px',
103 background: 'rgba(0, 212, 255, 0.06)',
104 borderRadius: '8px', color: '#c0c8d0', fontSize: '13px',
105 }}>
106 Na pokładzie: {crew.length} członków załogi ({crew.map((member) => member.role).join(', ')})
107 </div>
108 );
109}
110
111function LoadingFallback() {
112 return (
113 <div style={{
114 padding: '40px', textAlign: 'center', color: '#778da9',
115 }}>
116 <div style={{ fontSize: '24px', marginBottom: '10px' }}>Ładowanie danych misji...</div>
117 <div style={{
118 width: '50px', height: '50px', margin: '0 auto',
119 border: '3px solid #1a3a5c', borderTop: '3px solid #00d4ff',
120 borderRadius: '50%', animation: 'spin 1s linear infinite',
121 }} />
122 </div>
123 );
124}
125
126export default function App() {
127 const [showDetails, setShowDetails] = useState(true);
128 const missionPromise = React.useMemo(() => fetchMissionData(), []);
129
130 const crewData = [
131 { id: 1, name: 'Kapitan Nova', role: 'Dowódca' },
132 { id: 2, name: 'Dr. Stella', role: 'Nawigator' },
133 { id: 3, name: 'Inż. Orion', role: 'Mechanik' },
134 { id: 4, name: 'Lt. Vega', role: 'Pilot' },
135 ];
136
137 return (
138 <ThemeContext.Provider value="dark">
139 <CrewContext.Provider value={crewData}>
140 <div style={{
141 minHeight: '100vh',
142 background: 'linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%)',
143 padding: '24px', fontFamily: 'monospace',
144 }}>
145 <h1 style={{ color: '#00d4ff', textAlign: 'center', marginBottom: '8px' }}>
146 Hook use() - Promise i Context
147 </h1>
148 <p style={{ color: '#778da9', textAlign: 'center', marginBottom: '24px', fontSize: '14px' }}>
149 Nowy sposób na odczytywanie danych w React
150 </p>
151
152 <div style={{ maxWidth: '600px', margin: '0 auto' }}>
153 <CrewPanel />
154
155 <Suspense fallback={<LoadingFallback />}>
156 <MissionDetails missionPromise={missionPromise} />
157 </Suspense>
158
159 <div style={{ marginTop: '16px', textAlign: 'center' }}>
160 <button
161 onClick={() => setShowDetails(d => !d)}
162 style={{
163 padding: '10px 20px', background: '#1a3a5c',
164 border: 'none', borderRadius: '6px', color: '#00d4ff',
165 cursor: 'pointer', fontSize: '13px',
166 }}
167 >
168 {showDetails ? 'Ukryj szczegóły' : 'Pokaż szczegóły'}
169 </button>
170 </div>
171
172 <ConditionalInfo showDetails={showDetails} />
173 </div>
174 </div>
175 </CrewContext.Provider>
176 </ThemeContext.Provider>
177 );
178}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki jest cel drugiego argumentu (custom comparator) w React.memo(Component, compareFn)?
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj workflow optymalizacji wydajności: